Compare commits

...

10 Commits

Author SHA1 Message Date
441e18e916 Update generated test output 2025-04-23 19:36:02 -04:00
1502f923ee KCL bugfix: CallExpressionKw was not usable as math operand (#6460)
Closes https://github.com/KittyCAD/modeling-app/issues/4992
2025-04-23 21:21:57 +00:00
f03a684eec Modeling view appearance final tweaks (#6425)
* Remove bug button from LowerRightControls

There is a "report a bug" button in the help menus, both native and
lower-right corner. This is overkill, and users are not using coredump
well.

* Remove coredump from refresh UI button

* Add a "Refresh app" command to palette

* Update snapshots

* Rework "Refresh and report bug" menu item to "Report a bug"

* Add refresh button to sidebar

* Convert upper-right refresh button to Share

* Tweak styles of command button

* Make anonymous user icon same size as known user image

* Remove ModelStateIndicator

* Use hotkeyDisplay for the sidebar too

* Update snapshots

* Remove tooltip from command bar open button

* tsc, lint, and fmt
2025-04-23 15:20:45 -04:00
45e17c50e7 docs: Add docs for creating simulation tests (#6453) 2025-04-23 11:59:07 -07:00
6bf74379a7 Kwargs: hollow (#6438) 2025-04-23 18:35:33 +00:00
01c6fd53fa Fix Cargo.lock (#6452) 2025-04-23 17:39:35 +00:00
f8306c0275 Try running with 8 macOS cores again (#6420)
* Revert "Disable all macOS testing for now (#6395)"

This reverts commit fbc3251c3f.

* Try running with 8 macOS cores again
2025-04-23 13:10:09 -04:00
900ef9e18d Release KCL 63 (#6446) 2025-04-23 12:35:16 -04:00
a46186573c Kwargs: profileStart/x/y (#6439) 2025-04-23 15:21:58 +00:00
90f6c1bb04 Fix Back button on SignIn page & Add Cancel button (#6415)
* Back button on SignIn page leads to Home page
Fixes #6413

* Add regression test for the bug, and for new cancel button

* Full sign in e2e test

* Good bot

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>

---------

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
2025-04-23 15:12:33 +00:00
80 changed files with 2166 additions and 471 deletions

View File

@ -285,8 +285,7 @@ jobs:
# TODO: enable namespace-profile-windows-latest once available
os:
- "runs-on=${{ github.run_id }}/family=i7ie.2xlarge/image=ubuntu22-full-x64"
# TODO: renable this when macoOS runner seem more stable
# - namespace-profile-macos-6-cores
- namespace-profile-macos-8-cores
- windows-latest-8-cores
shardIndex: [1, 2, 3, 4]
shardTotal: [4]
@ -296,7 +295,7 @@ jobs:
isScheduled:
- ${{ github.event_name == 'schedule' }}
exclude:
- os: namespace-profile-macos-6-cores
- os: namespace-profile-macos-8-cores
isScheduled: true
- os: windows-latest-8-cores
isScheduled: true

File diff suppressed because one or more lines are too long

View File

@ -9,7 +9,7 @@ Extract the provided 2-dimensional sketch's profile's origin value.
```js
profileStart(sketch: Sketch): [number]
profileStart(profile: Sketch): [number]
```
@ -17,7 +17,7 @@ profileStart(sketch: Sketch): [number]
| Name | Type | Description | Required |
|----------|------|-------------|----------|
| `sketch` | [`Sketch`](/docs/kcl/types/Sketch) | | Yes |
| `profile` | [`Sketch`](/docs/kcl/types/Sketch) | Profile whose start is being used | Yes |
### Returns

View File

@ -9,7 +9,7 @@ Extract the provided 2-dimensional sketch's profile's origin's 'x' value.
```js
profileStartX(sketch: Sketch): number
profileStartX(profile: Sketch): number
```
@ -17,7 +17,7 @@ profileStartX(sketch: Sketch): number
| Name | Type | Description | Required |
|----------|------|-------------|----------|
| `sketch` | [`Sketch`](/docs/kcl/types/Sketch) | | Yes |
| `profile` | [`Sketch`](/docs/kcl/types/Sketch) | Profile whose start is being used | Yes |
### Returns

View File

@ -9,7 +9,7 @@ Extract the provided 2-dimensional sketch's profile's origin's 'y' value.
```js
profileStartY(sketch: Sketch): number
profileStartY(profile: Sketch): number
```
@ -17,7 +17,7 @@ profileStartY(sketch: Sketch): number
| Name | Type | Description | Required |
|----------|------|-------------|----------|
| `sketch` | [`Sketch`](/docs/kcl/types/Sketch) | | Yes |
| `profile` | [`Sketch`](/docs/kcl/types/Sketch) | Profile whose start is being used | Yes |
### Returns

File diff suppressed because it is too large Load Diff

View File

@ -29,11 +29,11 @@ test.describe('Electron app header tests', () => {
test(
'User settings has correct shortcut',
{ tag: '@electron' },
async ({ page }, testInfo) => {
async ({ page, toolbar }, testInfo) => {
await page.setBodyDimensions({ width: 1200, height: 500 })
// Open the user sidebar menu.
await page.getByTestId('user-sidebar-toggle').click()
await toolbar.userSidebarButton.click()
// No space after "User settings" since it's textContent.
const text =

View File

@ -0,0 +1,53 @@
import { expect, test } from '@e2e/playwright/zoo-test'
// test file is for testing auth functionality
test.describe('Authentication tests', () => {
test(
`The user can sign out and back in`,
{ tag: ['@electron'] },
async ({ page, homePage, signInPage, toolbar, tronApp }) => {
if (!tronApp) {
fail()
}
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.projectSection.waitFor()
await test.step('Click on sign out and expect sign in page', async () => {
await toolbar.userSidebarButton.click()
await toolbar.signOutButton.click()
await expect(signInPage.signInButton).toBeVisible()
})
await test.step('Click on sign in and cancel, click again and expect different code', async () => {
await signInPage.signInButton.click()
await expect(signInPage.userCode).toBeVisible()
const firstUserCode = await signInPage.userCode.textContent()
await signInPage.cancelSignInButton.click()
await expect(signInPage.signInButton).toBeVisible()
await signInPage.signInButton.click()
await expect(signInPage.userCode).toBeVisible()
const secondUserCode = await signInPage.userCode.textContent()
expect(secondUserCode).not.toEqual(firstUserCode)
})
await test.step('Press back button and remain on home page', async () => {
await page.goBack()
await expect(homePage.projectSection).not.toBeVisible()
await expect(signInPage.signInButton).toBeVisible()
})
await test.step('Sign in, activate, and expect home page', async () => {
await signInPage.signInButton.click()
await expect(signInPage.userCode).toBeVisible()
const userCode = await signInPage.userCode.textContent()
expect(userCode).not.toBeNull()
await signInPage.verifyAndConfirmAuth(userCode!)
// Longer timeout than usual here for the wait on home page
await expect(homePage.projectSection).toBeVisible({ timeout: 10000 })
})
}
)
})

View File

@ -18,6 +18,7 @@ import type { Settings } from '@rust/kcl-lib/bindings/Settings'
import { CmdBarFixture } from '@e2e/playwright/fixtures/cmdBarFixture'
import { EditorFixture } from '@e2e/playwright/fixtures/editorFixture'
import { HomePageFixture } from '@e2e/playwright/fixtures/homePageFixture'
import { SignInPageFixture } from '@e2e/playwright/fixtures/signInPageFixture'
import { SceneFixture } from '@e2e/playwright/fixtures/sceneFixture'
import { ToolbarFixture } from '@e2e/playwright/fixtures/toolbarFixture'
@ -66,6 +67,7 @@ export interface Fixtures {
toolbar: ToolbarFixture
scene: SceneFixture
homePage: HomePageFixture
signInPage: SignInPageFixture
}
export class ElectronZoo {
@ -387,6 +389,9 @@ const fixturesBasedOnProcessEnvPlatform = {
homePage: async ({ page }: { page: Page }, use: FnUse) => {
await use(new HomePageFixture(page))
},
signInPage: async ({ page }: { page: Page }, use: FnUse) => {
await use(new SignInPageFixture(page))
},
}
if (process.env.PLATFORM === 'web') {

View File

@ -0,0 +1,48 @@
import type { Locator, Page } from '@playwright/test'
import { secrets } from '@e2e/playwright/secrets'
export class SignInPageFixture {
public page: Page
signInButton!: Locator
cancelSignInButton!: Locator
userCode!: Locator
apiBaseUrl!: string
constructor(page: Page) {
this.page = page
this.signInButton = this.page.getByTestId('sign-in-button')
this.cancelSignInButton = this.page.getByTestId('cancel-sign-in-button')
this.userCode = this.page.getByTestId('sign-in-user-code')
// TODO: set this thru env var
this.apiBaseUrl = 'https://api.dev.zoo.dev'
}
async verifyAndConfirmAuth(userCode: string) {
// Device flow: stolen from the tauri days
// https://github.com/KittyCAD/modeling-app/blob/d916c7987452e480719004e6d11fd2e595c7d0eb/e2e/tauri/specs/app.spec.ts#L19
const headers = {
Authorization: `Bearer ${secrets.token}`,
Accept: 'application/json',
'Content-Type': 'application/json',
}
const verifyUrl = `${this.apiBaseUrl}/oauth2/device/verify?user_code=${userCode}`
console.log(`GET ${verifyUrl}`)
const vr = await fetch(verifyUrl, { headers })
console.log(vr.status)
// Device flow: confirm
const confirmUrl = `${this.apiBaseUrl}/oauth2/device/confirm`
const data = JSON.stringify({ user_code: userCode })
console.log(`POST ${confirmUrl} ${data}`)
const cr = await fetch(confirmUrl, {
headers,
method: 'POST',
body: data,
})
console.log(cr.status)
}
}

View File

@ -46,6 +46,9 @@ export class ToolbarFixture {
gizmo!: Locator
gizmoDisabled!: Locator
loadButton!: Locator
/** User button for the user sidebar menu */
userSidebarButton!: Locator
signOutButton!: Locator
constructor(page: Page) {
this.page = page
@ -82,6 +85,9 @@ export class ToolbarFixture {
// element or two different elements can represent these states.
this.gizmo = page.getByTestId('gizmo')
this.gizmoDisabled = page.getByTestId('gizmo-disabled')
this.userSidebarButton = page.getByTestId('user-sidebar-toggle')
this.signOutButton = page.getByTestId('user-sidebar-sign-out')
}
get logoLink() {

View File

@ -409,11 +409,7 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => {
)
.toBe(true)
})
test('Home.Help.Refresh and report a bug', async ({
tronApp,
cmdBar,
page,
}) => {
test('Home.Help.Report a bug', async ({ tronApp, cmdBar, page }) => {
if (!tronApp) fail()
// Run electron snippet to find the Menu!
await page.waitForTimeout(100) // wait for createModelingPageMenu() to run
@ -424,9 +420,8 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => {
if (!app || !app.applicationMenu) {
return false
}
const menu = app.applicationMenu.getMenuItemById(
'Help.Refresh and report a bug'
)
const menu =
app.applicationMenu.getMenuItemById('Help.Report a bug')
if (!menu) return false
menu.click()
return true
@ -2291,7 +2286,7 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => {
if (!menu) fail()
})
})
test('Modeling.Help.Refresh and report a bug', async ({
test('Modeling.Help.Report a bug', async ({
tronApp,
cmdBar,
page,
@ -2315,9 +2310,8 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => {
async () =>
await tronApp.electron.evaluate(async ({ app }) => {
if (!app || !app.applicationMenu) return false
const menu = app.applicationMenu.getMenuItemById(
'Help.Refresh and report a bug'
)
const menu =
app.applicationMenu.getMenuItemById('Help.Report a bug')
if (!menu) return false
menu.click()
return true

View File

@ -331,6 +331,7 @@ test.describe('Onboarding tests', () => {
test('Avatar text updates depending on image load success', async ({
context,
page,
toolbar,
homePage,
tronApp,
}) => {
@ -362,7 +363,7 @@ test.describe('Onboarding tests', () => {
await homePage.goToModelingScene()
// Test that the text in this step is correct
const avatarLocator = page.getByTestId('user-sidebar-toggle').locator('img')
const avatarLocator = toolbar.userSidebarButton.locator('img')
const onboardingOverlayLocator = page
.getByTestId('onboarding-content')
.locator('div')
@ -404,6 +405,7 @@ test.describe('Onboarding tests', () => {
test("Avatar text doesn't mention avatar when no avatar", async ({
context,
page,
toolbar,
homePage,
tronApp,
}) => {
@ -435,7 +437,7 @@ test.describe('Onboarding tests', () => {
await homePage.goToModelingScene()
// Test that the text in this step is correct
const sidebar = page.getByTestId('user-sidebar-toggle')
const sidebar = toolbar.userSidebarButton
const avatar = sidebar.locator('img')
const onboardingOverlayLocator = page
.getByTestId('onboarding-content')
@ -464,6 +466,7 @@ test.describe('Onboarding tests', () => {
test('Restarting onboarding on desktop takes one attempt', async ({
context,
page,
toolbar,
tronApp,
}) => {
test.fixme(orRunWhenFullSuiteEnabled())
@ -502,7 +505,7 @@ test('Restarting onboarding on desktop takes one attempt', async ({
.filter({ hasText: 'Tutorial Project 00' })
const tutorialModalText = page.getByText('Welcome to Design Studio!')
const tutorialDismissButton = page.getByRole('button', { name: 'Dismiss' })
const userMenuButton = page.getByTestId('user-sidebar-toggle')
const userMenuButton = toolbar.userSidebarButton
const userMenuSettingsButton = page.getByRole('button', {
name: 'User settings',
})

View File

@ -400,11 +400,6 @@ test(
await expect(page.getByText('broken-code')).toBeVisible()
await page.getByText('broken-code').click()
// Gotcha: You can not use scene.settled() since the KCL code is going to fail
await expect(
page.getByTestId('model-state-indicator-playing')
).toBeAttached()
// Gotcha: Scroll to the text content in code mirror because CodeMirror lazy loads DOM content
await editor.scrollToText(
"|> line(end = [0, wallMountL], tag = 'outerEdge')"
@ -779,7 +774,9 @@ test.describe(`Project management commands`, () => {
// Constants and locators
const projectHomeLink = page.getByTestId('project-link')
const commandButton = page.getByRole('button', { name: 'Commands' })
const commandOption = page.getByRole('option', { name: 'rename project' })
const commandOption = page.getByRole('option', {
name: 'rename project',
})
const projectNameOption = page.getByRole('option', { name: projectName })
const projectRenamedName = `untitled`
// const projectMenuButton = page.getByTestId('project-sidebar-toggle')
@ -839,7 +836,9 @@ test.describe(`Project management commands`, () => {
// Constants and locators
const projectHomeLink = page.getByTestId('project-link')
const commandButton = page.getByRole('button', { name: 'Commands' })
const commandOption = page.getByRole('option', { name: 'delete project' })
const commandOption = page.getByRole('option', {
name: 'delete project',
})
const projectNameOption = page.getByRole('option', { name: projectName })
const commandWarning = page.getByText('Are you sure you want to delete?')
const commandSubmitButton = page.getByRole('button', {
@ -891,7 +890,9 @@ test.describe(`Project management commands`, () => {
// Constants and locators
const projectHomeLink = page.getByTestId('project-link')
const commandButton = page.getByRole('button', { name: 'Commands' })
const commandOption = page.getByRole('option', { name: 'rename project' })
const commandOption = page.getByRole('option', {
name: 'rename project',
})
const projectNameOption = page.getByRole('option', { name: projectName })
const projectRenamedName = `untitled`
const commandContinueButton = page.getByRole('button', {
@ -947,7 +948,9 @@ test.describe(`Project management commands`, () => {
// Constants and locators
const projectHomeLink = page.getByTestId('project-link')
const commandButton = page.getByRole('button', { name: 'Commands' })
const commandOption = page.getByRole('option', { name: 'delete project' })
const commandOption = page.getByRole('option', {
name: 'delete project',
})
const projectNameOption = page.getByRole('option', { name: projectName })
const commandWarning = page.getByText('Are you sure you want to delete?')
const commandSubmitButton = page.getByRole('button', {
@ -1962,13 +1965,13 @@ test(
test(
'Settings persist across restarts',
{ tag: '@electron' },
async ({ page, scene, cmdBar }, testInfo) => {
async ({ page, toolbar }, testInfo) => {
await test.step('We can change a user setting like theme', async () => {
await page.setBodyDimensions({ width: 1200, height: 500 })
page.on('console', console.log)
await page.getByTestId('user-sidebar-toggle').click()
await toolbar.userSidebarButton.click()
await page.getByTestId('user-settings').click()
@ -1995,7 +1998,7 @@ test(
test(
'Original project name persist after onboarding',
{ tag: '@electron' },
async ({ page }, testInfo) => {
async ({ page, toolbar }, testInfo) => {
test.fixme(orRunWhenFullSuiteEnabled())
await page.setBodyDimensions({ width: 1200, height: 500 })
@ -2007,7 +2010,7 @@ test(
})
await test.step('Should go through onboarding', async () => {
await page.getByTestId('user-sidebar-toggle').click()
await toolbar.userSidebarButton.click()
await page.getByTestId('user-settings').click()
await page.getByRole('button', { name: 'Replay Onboarding' }).click()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View File

@ -36,7 +36,6 @@ export const headerMasks = (page: Page) => [
]
export const networkingMasks = (page: Page) => [
page.getByTestId('model-state-indicator'),
page.getByTestId('network-toggle'),
]
@ -85,12 +84,6 @@ async function waitForPageLoadWithRetry(page: Page) {
await expect(async () => {
await page.goto('/')
const errorMessage = 'App failed to load - 🔃 Retrying ...'
await expect(
page.getByTestId('model-state-indicator-playing'),
errorMessage
).toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'sketch Start Sketch' }),
@ -103,11 +96,6 @@ async function waitForPageLoadWithRetry(page: Page) {
// lee: This needs to be replaced by scene.settled() eventually.
async function waitForPageLoad(page: Page) {
// wait for all spinners to be gone
await expect(page.getByTestId('model-state-indicator-playing')).toBeVisible({
timeout: 20_000,
})
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeEnabled({
timeout: 20_000,
})

20
rust/Cargo.lock generated
View File

@ -1792,7 +1792,7 @@ dependencies = [
[[package]]
name = "kcl-bumper"
version = "0.1.62"
version = "0.1.63"
dependencies = [
"anyhow",
"clap",
@ -1803,7 +1803,7 @@ dependencies = [
[[package]]
name = "kcl-derive-docs"
version = "0.1.62"
version = "0.1.63"
dependencies = [
"Inflector",
"anyhow",
@ -1822,7 +1822,7 @@ dependencies = [
[[package]]
name = "kcl-directory-test-macro"
version = "0.1.62"
version = "0.1.63"
dependencies = [
"proc-macro2",
"quote",
@ -1831,7 +1831,7 @@ dependencies = [
[[package]]
name = "kcl-language-server"
version = "0.2.62"
version = "0.2.63"
dependencies = [
"anyhow",
"clap",
@ -1852,7 +1852,7 @@ dependencies = [
[[package]]
name = "kcl-language-server-release"
version = "0.1.62"
version = "0.1.63"
dependencies = [
"anyhow",
"clap",
@ -1872,7 +1872,7 @@ dependencies = [
[[package]]
name = "kcl-lib"
version = "0.2.62"
version = "0.2.63"
dependencies = [
"anyhow",
"approx 0.5.1",
@ -1943,7 +1943,7 @@ dependencies = [
[[package]]
name = "kcl-python-bindings"
version = "0.3.62"
version = "0.3.63"
dependencies = [
"anyhow",
"kcl-lib",
@ -1958,7 +1958,7 @@ dependencies = [
[[package]]
name = "kcl-test-server"
version = "0.1.62"
version = "0.1.63"
dependencies = [
"anyhow",
"hyper 0.14.32",
@ -1971,7 +1971,7 @@ dependencies = [
[[package]]
name = "kcl-to-core"
version = "0.1.62"
version = "0.1.63"
dependencies = [
"anyhow",
"async-trait",
@ -1985,7 +1985,7 @@ dependencies = [
[[package]]
name = "kcl-wasm-lib"
version = "0.1.62"
version = "0.1.63"
dependencies = [
"bson",
"console_error_panic_hook",

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-bumper"
version = "0.1.62"
version = "0.1.63"
edition = "2021"
repository = "https://github.com/KittyCAD/modeling-api"
rust-version = "1.76"

View File

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

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-directory-test-macro"
description = "A tool for generating tests from a directory of kcl files"
version = "0.1.62"
version = "0.1.63"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -1,6 +1,6 @@
[package]
name = "kcl-language-server-release"
version = "0.1.62"
version = "0.1.63"
edition = "2021"
authors = ["KittyCAD Inc <kcl@kittycad.io>"]
publish = false

View File

@ -2,7 +2,7 @@
name = "kcl-language-server"
description = "A language server for KCL."
authors = ["KittyCAD Inc <kcl@kittycad.io>"]
version = "0.2.62"
version = "0.2.63"
edition = "2021"
license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-lib"
description = "KittyCAD Language implementation and tools"
version = "0.2.62"
version = "0.2.63"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -19,6 +19,30 @@ We've built a lot of tooling to make contributing to KCL easier. If you are inte
11. Run `just redo-kcl-stdlib-docs` to generate new Markdown documentation for your function that will be used [to generate docs on our website](https://zoo.dev/docs/kcl).
12. Create a PR in GitHub.
## Making a Simulation Test
If you have KCL code that you want to test, simulation tests are the preferred way to do that.
Make a new sim test. Replace `foo_bar` with the snake case name of your test. The name needs to be unique.
```shell
just new-sim-test foo_bar
```
It will show the commands it ran, including the path to a new file `foo_bar/input.kcl`. Edit that with your KCL. If you need additional KCL files to import, include them in this directory.
Then run it.
```shell
just overwrite-sim-test foo_bar
```
The above should create a bunch of output files in the same directory.
Make sure you actually look at them. Specifically, if there's an `execution_error.snap`, it means the execution failed. Depending on the test, this may be what you expect. But if it's not, delete the snap file and run it again.
When it looks good, commit all the files, including `input.kcl`, generated output files in the test directory, and changes to `simulation_tests.rs`.
## Bumping the version
If you bump the version of kcl-lib and push it to crates, be sure to update the repos we own that use it as well. These are:

View File

@ -1820,18 +1820,6 @@ const bracket = startSketchOn(XY)
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_bad_arg_count_std() {
let ast = "startSketchOn(XY)
|> startProfileAt([0, 0], %)
|> profileStartX()";
assert!(parse_execute(ast)
.await
.unwrap_err()
.message()
.contains("Expected a sketch argument"));
}
#[tokio::test(flavor = "multi_thread")]
async fn test_unary_operator_not_succeeds() {
let ast = r#"

View File

@ -2074,6 +2074,7 @@ fn possible_operands(i: &mut TokenSlice) -> PResult<Expr> {
member_expression.map(Box::new).map(Expr::MemberExpression),
literal.map(Expr::Literal),
fn_call.map(Box::new).map(Expr::CallExpression),
fn_call_kw.map(Box::new).map(Expr::CallExpressionKw),
name.map(Box::new).map(Expr::Name),
binary_expr_in_parens.map(Box::new).map(Expr::BinaryExpression),
unnecessarily_bracketed,
@ -3254,6 +3255,14 @@ mod tests {
assert_eq!(err.message, "Unexpected end of file. The compiler expected )");
}
#[test]
fn kw_call_as_operand() {
let tokens = crate::parsing::token::lex("f(x = 1)", ModuleId::default()).unwrap();
let tokens = tokens.as_slice();
let op = operand.parse(tokens).unwrap();
println!("{op:#?}");
}
#[test]
fn weird_program_just_a_pipe() {
let tokens = crate::parsing::token::lex("|", ModuleId::default()).unwrap();
@ -5389,6 +5398,7 @@ my14 = 4 ^ 2 - 3 ^ 2 * 2
bar = x,
)"#
);
snapshot_test!(kw_function_in_binary_op, r#"val = f(x = 1) + 1"#);
}
#[allow(unused)]

View File

@ -0,0 +1,99 @@
---
source: kcl-lib/src/parsing/parser.rs
expression: actual
---
{
"body": [
{
"commentStart": 0,
"declaration": {
"commentStart": 0,
"end": 18,
"id": {
"commentStart": 0,
"end": 3,
"name": "val",
"start": 0,
"type": "Identifier"
},
"init": {
"commentStart": 6,
"end": 18,
"left": {
"arguments": [
{
"type": "LabeledArg",
"label": {
"commentStart": 8,
"end": 9,
"name": "x",
"start": 8,
"type": "Identifier"
},
"arg": {
"commentStart": 12,
"end": 13,
"raw": "1",
"start": 12,
"type": "Literal",
"type": "Literal",
"value": {
"value": 1.0,
"suffix": "None"
}
}
}
],
"callee": {
"abs_path": false,
"commentStart": 6,
"end": 7,
"name": {
"commentStart": 6,
"end": 7,
"name": "f",
"start": 6,
"type": "Identifier"
},
"path": [],
"start": 6,
"type": "Name"
},
"commentStart": 6,
"end": 14,
"start": 6,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": null
},
"operator": "+",
"right": {
"commentStart": 17,
"end": 18,
"raw": "1",
"start": 17,
"type": "Literal",
"type": "Literal",
"value": {
"value": 1.0,
"suffix": "None"
}
},
"start": 6,
"type": "BinaryExpression",
"type": "BinaryExpression"
},
"start": 0,
"type": "VariableDeclarator"
},
"end": 18,
"kind": "const",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
}
],
"commentStart": 0,
"end": 18,
"start": 0
}

View File

@ -675,28 +675,6 @@ impl Args {
Ok((sketches, sketch))
}
pub(crate) fn get_sketch(&self, exec_state: &mut ExecState) -> Result<Sketch, KclError> {
let Some(arg0) = self.args.first() else {
return Err(KclError::Semantic(KclErrorDetails {
message: "Expected a sketch argument".to_owned(),
source_ranges: vec![self.source_range],
}));
};
let sarg = arg0
.value
.coerce(&RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)
.map_err(|_| {
KclError::Type(KclErrorDetails {
message: format!("Expected a sketch, found {}", arg0.value.human_friendly_type()),
source_ranges: vec![self.source_range],
})
})?;
match sarg {
KclValue::Sketch { value } => Ok(*value),
_ => unreachable!(),
}
}
pub(crate) fn get_data<'a, T>(&'a self) -> Result<T, KclError>
where
T: FromArgs<'a>,
@ -708,49 +686,6 @@ impl Args {
FromArgs::from_args(self, 0)
}
pub(crate) fn get_length_and_solid(&self, exec_state: &mut ExecState) -> Result<(TyF64, Box<Solid>), KclError> {
let Some(arg0) = self.args.first() else {
return Err(KclError::Semantic(KclErrorDetails {
message: "Expected a `number(Length)` for first argument".to_owned(),
source_ranges: vec![self.source_range],
}));
};
let val0 = arg0.value.coerce(&RuntimeType::length(), exec_state).map_err(|_| {
KclError::Type(KclErrorDetails {
message: format!(
"Expected a `number(Length)` for first argument, found {}",
arg0.value.human_friendly_type()
),
source_ranges: vec![self.source_range],
})
})?;
let data = TyF64::from_kcl_val(&val0).unwrap();
let Some(arg1) = self.args.get(1) else {
return Err(KclError::Semantic(KclErrorDetails {
message: "Expected a solid for second argument".to_owned(),
source_ranges: vec![self.source_range],
}));
};
let sarg = arg1
.value
.coerce(&RuntimeType::Primitive(PrimitiveType::Solid), exec_state)
.map_err(|_| {
KclError::Type(KclErrorDetails {
message: format!(
"Expected a solid for second argument, found {}",
arg1.value.human_friendly_type()
),
source_ranges: vec![self.source_range],
})
})?;
let solid = match sarg {
KclValue::Solid { value } => value,
_ => unreachable!(),
};
Ok((data, solid))
}
pub(crate) fn get_tag_to_number_sketch(&self) -> Result<(TagIdentifier, TyF64, Sketch), KclError> {
FromArgs::from_args(self, 0)
}

View File

@ -247,9 +247,10 @@ async fn inner_shell(
/// Make the inside of a 3D object hollow.
pub async fn hollow(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (thickness, solid) = args.get_length_and_solid(exec_state)?;
let solid = args.get_unlabeled_kw_arg_typed("solid", &RuntimeType::solid(), exec_state)?;
let thickness: TyF64 = args.get_kw_arg_typed("thickness", &RuntimeType::length(), exec_state)?;
let value = inner_hollow(thickness, solid, exec_state, args).await?;
let value = inner_hollow(solid, thickness, exec_state, args).await?;
Ok(KclValue::Solid { value })
}
@ -267,7 +268,7 @@ pub async fn hollow(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
/// |> line(end = [-24, 0])
/// |> close()
/// |> extrude(length = 6)
/// |> hollow (0.25, %)
/// |> hollow(thickness = 0.25)
/// ```
///
/// ```no_run
@ -279,7 +280,7 @@ pub async fn hollow(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
/// |> line(end = [-24, 0])
/// |> close()
/// |> extrude(length = 6)
/// |> hollow (0.5, %)
/// |> hollow(thickness = 0.5)
/// ```
///
/// ```no_run
@ -301,15 +302,21 @@ pub async fn hollow(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
/// |> circle( center = [size / 2, -size / 2], radius = 25 )
/// |> extrude(length = 50)
///
/// hollow(0.5, case)
/// hollow(case, thickness = 0.5)
/// ```
#[stdlib {
name = "hollow",
feature_tree_operation = true,
keywords = true,
unlabeled_first = true,
args = {
solid = { docs = "Which solid to shell out" },
thickness = {docs = "The thickness of the shell" },
}
}]
async fn inner_hollow(
thickness: TyF64,
solid: Box<Solid>,
thickness: TyF64,
exec_state: &mut ExecState,
args: Args,
) -> Result<Box<Solid>, KclError> {

View File

@ -1454,7 +1454,7 @@ pub(crate) async fn inner_start_profile_at(
/// Returns the X component of the sketch profile start point.
pub async fn profile_start_x(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch: Sketch = args.get_sketch(exec_state)?;
let sketch: Sketch = args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::sketch(), exec_state)?;
let ty = sketch.units.into();
let x = inner_profile_start_x(sketch)?;
Ok(args.make_user_val_from_f64_with_type(TyF64::new(x, ty)))
@ -1471,15 +1471,20 @@ pub async fn profile_start_x(exec_state: &mut ExecState, args: Args) -> Result<K
/// |> angledLine(angle = 30, endAbsoluteX = profileStartX(%))
/// ```
#[stdlib {
name = "profileStartX"
name = "profileStartX",
keywords = true,
unlabeled_first = true,
args = {
profile = {docs = "Profile whose start is being used"},
}
}]
pub(crate) fn inner_profile_start_x(sketch: Sketch) -> Result<f64, KclError> {
Ok(sketch.start.to[0])
pub(crate) fn inner_profile_start_x(profile: Sketch) -> Result<f64, KclError> {
Ok(profile.start.to[0])
}
/// Returns the Y component of the sketch profile start point.
pub async fn profile_start_y(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch: Sketch = args.get_sketch(exec_state)?;
let sketch: Sketch = args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::sketch(), exec_state)?;
let ty = sketch.units.into();
let x = inner_profile_start_y(sketch)?;
Ok(args.make_user_val_from_f64_with_type(TyF64::new(x, ty)))
@ -1495,15 +1500,20 @@ pub async fn profile_start_y(exec_state: &mut ExecState, args: Args) -> Result<K
/// |> angledLine(angle = 30, endAbsoluteY = profileStartY(%))
/// ```
#[stdlib {
name = "profileStartY"
name = "profileStartY",
keywords = true,
unlabeled_first = true,
args = {
profile = {docs = "Profile whose start is being used"},
}
}]
pub(crate) fn inner_profile_start_y(sketch: Sketch) -> Result<f64, KclError> {
Ok(sketch.start.to[1])
pub(crate) fn inner_profile_start_y(profile: Sketch) -> Result<f64, KclError> {
Ok(profile.start.to[1])
}
/// Returns the sketch profile start point.
pub async fn profile_start(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch: Sketch = args.get_sketch(exec_state)?;
let sketch: Sketch = args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::sketch(), exec_state)?;
let ty = sketch.units.into();
let point = inner_profile_start(sketch)?;
Ok(KclValue::from_point2d(point, ty, args.into()))
@ -1522,10 +1532,15 @@ pub async fn profile_start(exec_state: &mut ExecState, args: Args) -> Result<Kcl
/// |> extrude(length = 20)
/// ```
#[stdlib {
name = "profileStart"
name = "profileStart",
keywords = true,
unlabeled_first = true,
args = {
profile = {docs = "Profile whose start is being used"},
}
}]
pub(crate) fn inner_profile_start(sketch: Sketch) -> Result<[f64; 2], KclError> {
Ok(sketch.start.to)
pub(crate) fn inner_profile_start(profile: Sketch) -> Result<[f64; 2], KclError> {
Ok(profile.start.to)
}
/// Close the current sketch.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 67 KiB

View File

@ -1,6 +1,6 @@
[package]
name = "kcl-python-bindings"
version = "0.3.62"
version = "0.3.63"
edition = "2021"
repository = "https://github.com/kittycad/modeling-app"
exclude = ["tests/*", "files/*", "venv/*"]

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-test-server"
description = "A test server for KCL"
version = "0.1.62"
version = "0.1.63"
edition = "2021"
license = "MIT"

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-to-core"
description = "Utility methods to convert kcl to engine core executable tests"
version = "0.1.62"
version = "0.1.63"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -1,6 +1,6 @@
[package]
name = "kcl-wasm-lib"
version = "0.1.62"
version = "0.1.63"
edition = "2021"
repository = "https://github.com/KittyCAD/modeling-app"
rust-version = "1.83"

View File

@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef } from 'react'
import { useEffect, useRef } from 'react'
import toast from 'react-hot-toast'
import { useHotkeys } from 'react-hotkeys-hook'
import ModalContainer from 'react-modal-promise'
@ -21,20 +21,14 @@ import { useAbsoluteFilePath } from '@src/hooks/useAbsoluteFilePath'
import { useCreateFileLinkQuery } from '@src/hooks/useCreateFileLinkQueryWatcher'
import { useEngineConnectionSubscriptions } from '@src/hooks/useEngineConnectionSubscriptions'
import { useHotKeyListener } from '@src/hooks/useHotKeyListener'
import { CoreDumpManager } from '@src/lib/coredump'
import { writeProjectThumbnailFile } from '@src/lib/desktop'
import useHotkeyWrapper from '@src/lib/hotkeyWrapper'
import { isDesktop } from '@src/lib/isDesktop'
import { PATHS } from '@src/lib/paths'
import { takeScreenshotOfVideoStreamCanvas } from '@src/lib/screenshot'
import {
codeManager,
engineCommandManager,
rustContext,
sceneInfra,
} from '@src/lib/singletons'
import { sceneInfra } from '@src/lib/singletons'
import { maybeWriteToDisk } from '@src/lib/telemetry'
import { type IndexLoaderData } from '@src/lib/types'
import type { IndexLoaderData } from '@src/lib/types'
import {
engineStreamActor,
useSettings,
@ -43,6 +37,8 @@ import {
import { commandBarActor } from '@src/machines/commandBarMachine'
import { EngineStreamTransition } from '@src/machines/engineStreamMachine'
import { onboardingPaths } from '@src/routes/Onboarding/paths'
import { CommandBarOpenButton } from '@src/components/CommandBarOpenButton'
import { ShareButton } from '@src/components/ShareButton'
// CYCLIC REF
sceneInfra.camControls.engineStreamActor = engineStreamActor
@ -93,17 +89,6 @@ export function App() {
const settings = useSettings()
const authToken = useToken()
const coreDumpManager = useMemo(
() =>
new CoreDumpManager(
engineCommandManager,
codeManager,
rustContext,
authToken
),
[]
)
const {
app: { onboardingStatus },
} = settings
@ -163,15 +148,18 @@ export function App() {
return (
<div className="relative h-full flex flex-col" ref={ref}>
<AppHeader
className={'transition-opacity transition-duration-75 ' + paneOpacity}
className={`transition-opacity transition-duration-75 ${paneOpacity}`}
project={{ project, file }}
enableMenu={true}
/>
>
<CommandBarOpenButton />
<ShareButton />
</AppHeader>
<ModalContainer />
<ModelingSidebar paneOpacity={paneOpacity} />
<EngineStream pool={pool} authToken={authToken} />
{/* <CamToggle /> */}
<LowerRightControls coreDumpManager={coreDumpManager}>
<LowerRightControls>
<UnitsMenu />
<Gizmo />
</LowerRightControls>

View File

@ -1013,13 +1013,18 @@ export class SceneEntities {
// Snapping logic for the profile start handle
if (intersectsProfileStart) {
const originCoords = createArrayExpression([
createCallExpressionStdLib('profileStartX', [
createCallExpressionStdLibKw(
'profileStartX',
createPipeSubstitution(),
]),
createCallExpressionStdLib('profileStartY', [
[]
),
createCallExpressionStdLibKw(
'profileStartY',
createPipeSubstitution(),
]),
[]
),
])
modifiedAst = addCallExpressionsToPipe({
node: this.kclManager.ast,
variables: this.kclManager.variables,
@ -2213,13 +2218,18 @@ export class SceneEntities {
modded = moddedResult.modifiedAst
if (intersectsProfileStart) {
const originCoords = createArrayExpression([
createCallExpressionStdLib('profileStartX', [
createCallExpressionStdLibKw(
'profileStartX',
createPipeSubstitution(),
]),
createCallExpressionStdLib('profileStartY', [
[]
),
createCallExpressionStdLibKw(
'profileStartY',
createPipeSubstitution(),
]),
[]
),
])
const arcToCallExp = getNodeFromPath<CallExpression>(
modded,
mod.pathToNode,

View File

@ -1,7 +1,6 @@
import { Toolbar } from '@src/Toolbar'
import { CommandBarOpenButton } from '@src/components/CommandBarOpenButton'
import ProjectSidebarMenu from '@src/components/ProjectSidebarMenu'
import { RefreshButton } from '@src/components/RefreshButton'
import UserSidebarMenu from '@src/components/UserSidebarMenu'
import { isDesktop } from '@src/lib/isDesktop'
import { type IndexLoaderData } from '@src/lib/types'
@ -49,14 +48,9 @@ export const AppHeader = ({
<div className="flex-grow flex justify-center max-w-lg md:max-w-xl lg:max-w-2xl xl:max-w-4xl 2xl:max-w-5xl">
{showToolbar && <Toolbar />}
</div>
<div className="flex items-center gap-1 py-1 ml-auto">
<div className="flex items-center gap-2 py-1 ml-auto">
{/* If there are children, show them, otherwise show User menu */}
{children || (
<>
<CommandBarOpenButton />
<RefreshButton />
</>
)}
{children || <CommandBarOpenButton />}
<UserSidebarMenu user={user} />
</div>
</header>

View File

@ -2,18 +2,21 @@ import { COMMAND_PALETTE_HOTKEY } from '@src/components/CommandBar/CommandBar'
import usePlatform from '@src/hooks/usePlatform'
import { hotkeyDisplay } from '@src/lib/hotkeyWrapper'
import { commandBarActor } from '@src/machines/commandBarMachine'
import { CustomIcon } from '@src/components/CustomIcon'
export function CommandBarOpenButton() {
const platform = usePlatform()
return (
<button
className="group rounded-full flex items-center justify-center gap-2 px-2 py-1 bg-primary/10 dark:bg-chalkboard-90 dark:backdrop-blur-sm border-primary hover:border-primary dark:border-chalkboard-50 dark:hover:border-inherit text-primary dark:text-inherit"
type="button"
className="flex gap-1 items-center py-0 px-0.5 m-0 text-primary dark:text-inherit bg-chalkboard-10/80 dark:bg-chalkboard-100/50 hover:bg-chalkboard-10 dark:hover:bg-chalkboard-100 border border-solid border-primary/50 hover:border-primary active:border-primary"
onClick={() => commandBarActor.send({ type: 'Open' })}
data-testid="command-bar-open-button"
>
<CustomIcon name="command" className="w-5 h-5" />
<span>Commands</span>
<kbd className="bg-primary/10 dark:bg-chalkboard-80 dark:group-hover:bg-primary font-mono rounded-sm dark:text-inherit inline-block px-1 border-primary dark:border-chalkboard-90">
<kbd className="dark:bg-chalkboard-80 font-mono rounded-sm text-primary/70 dark:text-inherit inline-block px-1">
{hotkeyDisplay(COMMAND_PALETTE_HOTKEY, platform)}
</kbd>
</button>

View File

@ -311,6 +311,22 @@ const CustomIconMap = {
/>
</svg>
),
command: (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M4.70711 6L8.20711 9.5L8.56066 9.85355L8.20711 10.2071L4.70711 13.7071L4 13L7.14645 9.85355L4 6.70711L4.70711 6ZM15.3536 11.3536H9.35356V12.3536H15.3536V11.3536Z"
fill="currentColor"
/>
</svg>
),
dimension: (
<svg
viewBox="0 0 20 20"

View File

@ -1,26 +1,19 @@
import toast from 'react-hot-toast'
import { Link, useLocation } from 'react-router-dom'
import { CustomIcon } from '@src/components/CustomIcon'
import { HelpMenu } from '@src/components/HelpMenu'
import { ModelStateIndicator } from '@src/components/ModelStateIndicator'
import { NetworkHealthIndicator } from '@src/components/NetworkHealthIndicator'
import { NetworkMachineIndicator } from '@src/components/NetworkMachineIndicator'
import Tooltip from '@src/components/Tooltip'
import { useAbsoluteFilePath } from '@src/hooks/useAbsoluteFilePath'
import { coreDump } from '@src/lang/wasm'
import type { CoreDumpManager } from '@src/lib/coredump'
import openWindow, { openExternalBrowserIfDesktop } from '@src/lib/openWindow'
import { openExternalBrowserIfDesktop } from '@src/lib/openWindow'
import { PATHS } from '@src/lib/paths'
import { reportRejection } from '@src/lib/trap'
import { APP_VERSION, getReleaseUrl } from '@src/routes/utils'
export function LowerRightControls({
children,
coreDumpManager,
}: {
children?: React.ReactNode
coreDumpManager?: CoreDumpManager
}) {
const location = useLocation()
const filePath = useAbsoluteFilePath()
@ -28,50 +21,10 @@ export function LowerRightControls({
const linkOverrideClassName =
'!text-chalkboard-70 hover:!text-chalkboard-80 dark:!text-chalkboard-40 dark:hover:!text-chalkboard-30'
function reportbug(event: {
preventDefault: () => void
stopPropagation: () => void
}) {
event?.preventDefault()
event?.stopPropagation()
if (!coreDumpManager) {
// open default reporting option
openWindow(
'https://github.com/KittyCAD/modeling-app/issues/new/choose'
).catch(reportRejection)
} else {
toast
.promise(
coreDump(coreDumpManager, true),
{
loading: 'Preparing bug report...',
success: 'Bug report opened in new window',
error: 'Unable to export a core dump. Using default reporting.',
},
{
success: {
// Note: this extended duration is especially important for Playwright e2e testing
// default duration is 2000 - https://react-hot-toast.com/docs/toast#default-durations
duration: 6000,
},
}
)
.catch((err: Error) => {
if (err) {
openWindow(
'https://github.com/KittyCAD/modeling-app/issues/new/choose'
).catch(reportRejection)
}
})
}
}
return (
<section className="fixed bottom-2 right-2 flex flex-col items-end gap-3 pointer-events-none">
{children}
<menu className="flex items-center justify-end gap-3 pointer-events-auto">
{!location.pathname.startsWith(PATHS.HOME) && <ModelStateIndicator />}
<a
onClick={openExternalBrowserIfDesktop(getReleaseUrl())}
href={getReleaseUrl()}
@ -81,20 +34,6 @@ export function LowerRightControls({
>
v{APP_VERSION}
</a>
<a
onClick={reportbug}
href="https://github.com/KittyCAD/modeling-app/issues/new/choose"
target="_blank"
rel="noopener noreferrer"
>
<CustomIcon
name="bug"
className={`w-5 h-5 ${linkOverrideClassName}`}
/>
<Tooltip position="top" contentClassName="text-xs">
Report a bug
</Tooltip>
</a>
<Link
to={
location.pathname.includes(PATHS.FILE)

View File

@ -1,52 +0,0 @@
import { engineStreamActor } from '@src/machines/appMachine'
import { EngineStreamState } from '@src/machines/engineStreamMachine'
import { useSelector } from '@xstate/react'
import { faPause, faPlay, faSpinner } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
export const ModelStateIndicator = () => {
const engineStreamState = useSelector(engineStreamActor, (state) => state)
let className = 'w-6 h-6 '
let icon = <div className={className}></div>
let dataTestId = 'model-state-indicator'
if (engineStreamState.value === EngineStreamState.Paused) {
className += 'text-secondary'
icon = (
<FontAwesomeIcon
data-testid={dataTestId + '-paused'}
icon={faPause}
width="20"
height="20"
/>
)
} else if (engineStreamState.value === EngineStreamState.Playing) {
className += 'text-secondary'
icon = (
<FontAwesomeIcon
data-testid={dataTestId + '-playing'}
icon={faPlay}
width="20"
height="20"
/>
)
} else {
className += 'text-secondary'
icon = (
<FontAwesomeIcon
data-testid={dataTestId + '-resuming'}
icon={faSpinner}
width="20"
height="20"
/>
)
}
return (
<div className={className} data-testid="model-state-indicator">
{icon}
</div>
)
}

View File

@ -25,6 +25,10 @@ import { isDesktop } from '@src/lib/isDesktop'
import { useSettings } from '@src/machines/appMachine'
import { commandBarActor } from '@src/machines/commandBarMachine'
import { onboardingPaths } from '@src/routes/Onboarding/paths'
import { reportRejection } from '@src/lib/trap'
import { refreshPage } from '@src/lib/utils'
import { hotkeyDisplay } from '@src/lib/hotkeyWrapper'
import usePlatform from '@src/hooks/usePlatform'
interface ModelingSidebarProps {
paneOpacity: '' | 'opacity-20' | 'opacity-40'
@ -86,18 +90,6 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
data: { name: 'load-external-model', groupId: 'code' },
}),
},
{
id: 'share-link',
title: 'Share part via Zoo link',
sidebarName: 'Share part via Zoo link',
icon: 'link',
keybinding: 'Mod + Alt + S',
action: () =>
commandBarActor.send({
type: 'Find and select command',
data: { name: 'share-file-link', groupId: 'code' },
}),
},
{
id: 'export',
title: 'Export part',
@ -130,6 +122,17 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
return machineManager.noMachinesReason()
},
},
{
id: 'refresh',
title: 'Refresh app',
sidebarName: 'Refresh app',
icon: 'arrowRotateRight',
keybinding: 'Mod + R',
// eslint-disable-next-line @typescript-eslint/no-misused-promises
action: async () => {
refreshPage('Sidebar button').catch(reportRejection)
},
},
]
const filteredActions: SidebarAction[] = sidebarActions.filter(
(action) =>
@ -340,6 +343,7 @@ function ModelingPaneButton({
disabledText,
...props
}: ModelingPaneButtonProps) {
const platform = usePlatform()
useHotkeys(paneConfig.keybinding, onClick, {
scopes: ['modeling'],
})
@ -379,7 +383,7 @@ function ModelingPaneButton({
{paneIsOpen !== undefined ? ` pane` : ''}
</span>
<kbd className="hotkey text-xs capitalize">
{paneConfig.keybinding}
{hotkeyDisplay(paneConfig.keybinding, platform)}
</kbd>
</Tooltip>
</button>

View File

@ -1,90 +0,0 @@
import React, { useMemo } from 'react'
import toast from 'react-hot-toast'
import { CustomIcon } from '@src/components/CustomIcon'
import Tooltip from '@src/components/Tooltip'
import { useMenuListener } from '@src/hooks/useMenu'
import { coreDump } from '@src/lang/wasm'
import { CoreDumpManager } from '@src/lib/coredump'
import {
codeManager,
engineCommandManager,
rustContext,
} from '@src/lib/singletons'
import { reportRejection } from '@src/lib/trap'
import { toSync } from '@src/lib/utils'
import { useToken } from '@src/machines/appMachine'
import type { WebContentSendPayload } from '@src/menu/channels'
export const RefreshButton = ({ children }: React.PropsWithChildren) => {
const token = useToken()
const coreDumpManager = useMemo(
() =>
new CoreDumpManager(
engineCommandManager,
codeManager,
rustContext,
token
),
[]
)
async function refresh() {
if (window && 'plausible' in window) {
const p = window.plausible as (
event: string,
options?: { props: Record<string, string> }
) => Promise<void>
// Send a refresh event to Plausible so we can track how often users get stuck
await p('Refresh', {
props: {
method: 'UI button',
// TODO: add more coredump data here
},
})
}
toast
.promise(
coreDump(coreDumpManager, true),
{
loading: 'Starting core dump...',
success: 'Core dump completed successfully',
error: 'Error while exporting core dump',
},
{
success: {
// Note: this extended duration is especially important for Playwright e2e testing
// default duration is 2000 - https://react-hot-toast.com/docs/toast#default-durations
duration: 6000,
},
}
)
.then(() => {
// Window may not be available in some environments
window?.location.reload()
})
.catch(reportRejection)
}
const cb = (data: WebContentSendPayload) => {
if (data.menuLabel === 'Help.Refresh and report a bug') {
refresh().catch(reportRejection)
}
}
useMenuListener(cb)
return (
<button
onClick={toSync(refresh, reportRejection)}
className="p-1 m-0 bg-chalkboard-10/80 dark:bg-chalkboard-100/50 hover:bg-chalkboard-10 dark:hover:bg-chalkboard-100 rounded-full border border-solid border-chalkboard-20 dark:border-chalkboard-90"
>
<CustomIcon name="exclamationMark" className="w-5 h-5" />
<Tooltip position="bottom-right">
<span>Refresh and report</span>
<br />
<span className="text-xs">Send us data on how you got stuck</span>
</Tooltip>
</button>
)
}

View File

@ -0,0 +1,41 @@
import { CustomIcon } from '@src/components/CustomIcon'
import Tooltip from '@src/components/Tooltip'
import usePlatform from '@src/hooks/usePlatform'
import { hotkeyDisplay } from '@src/lib/hotkeyWrapper'
import { commandBarActor } from '@src/machines/commandBarMachine'
import { useHotkeys } from 'react-hotkeys-hook'
const shareHotkey = 'mod+alt+s'
const onShareClick = () =>
commandBarActor.send({
type: 'Find and select command',
data: { name: 'share-file-link', groupId: 'code' },
})
/** Share Zoo link button shown in the upper-right of the modeling view */
export const ShareButton = () => {
const platform = usePlatform()
useHotkeys(shareHotkey, onShareClick, {
scopes: ['modeling'],
})
return (
<button
type="button"
onClick={onShareClick}
className="flex gap-1 items-center py-0 pl-0.5 pr-1.5 m-0 bg-chalkboard-10/80 dark:bg-chalkboard-100/50 hover:bg-chalkboard-10 dark:hover:bg-chalkboard-100 border border-solid active:border-primary"
>
<CustomIcon name="link" className="w-5 h-5" />
<span className="flex-1">Share</span>
<Tooltip
position="bottom-right"
contentClassName="max-w-none flex items-center gap-4"
>
<span className="flex-1">Share part via Zoo link</span>
<kbd className="hotkey text-xs capitalize">
{hotkeyDisplay(shareHotkey, platform)}
</kbd>
</Tooltip>
</button>
)
}

View File

@ -186,7 +186,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
) : (
<CustomIcon
name="person"
className="w-5 h-5 text-chalkboard-70 dark:text-chalkboard-40 bg-chalkboard-20 dark:bg-chalkboard-80"
className="w-7 h-7 text-chalkboard-70 dark:text-chalkboard-40 bg-chalkboard-20 dark:bg-chalkboard-80"
/>
)}
</div>

View File

@ -15,6 +15,7 @@ export function useAuthNavigation() {
// Subscribe to the auth state of the app and navigate accordingly.
useEffect(() => {
console.log('authState', authState.value)
if (
authState.matches('loggedIn') &&
location.pathname.includes(PATHS.SIGN_IN)
@ -26,5 +27,5 @@ export function useAuthNavigation() {
) {
navigate(PATHS.SIGN_IN)
}
}, [authState])
}, [authState, location.pathname])
}

View File

@ -1,6 +1,8 @@
import type { Command } from '@src/lib/commandTypes'
import { authActor } from '@src/machines/appMachine'
import { ACTOR_IDS } from '@src/machines/machineConstants'
import { refreshPage } from '@src/lib/utils'
import { reportRejection } from '@src/lib/trap'
export const authCommands: Command[] = [
{
@ -11,4 +13,14 @@ export const authCommands: Command[] = [
needsReview: false,
onSubmit: () => authActor.send({ type: 'Log out' }),
},
{
groupId: ACTOR_IDS.AUTH,
name: 'refresh',
displayName: 'Refresh app',
icon: 'arrowRotateRight',
needsReview: false,
onSubmit: () => {
refreshPage('Command palette').catch(reportRejection)
},
},
]

View File

@ -60,6 +60,7 @@ export function hotkeyDisplay(hotkey: string, platform: Platform): string {
// Capitalize letters. We want Ctrl+K, not Ctrl+k, since Shift should be
// shown as a separate modifier.
.split('+')
.map((word) => word.trim().toLocaleLowerCase())
.map((word) => {
if (word.length === 1 && LOWER_CASE_LETTER.test(word)) {
return word.toUpperCase()

View File

@ -81,12 +81,16 @@ export const getRectangleCallExpressions = (
createLabeledArg(
ARG_END_ABSOLUTE,
createArrayExpression([
createCallExpressionStdLib('profileStartX', [
createCallExpressionStdLibKw(
'profileStartX',
createPipeSubstitution(),
]),
createCallExpressionStdLib('profileStartY', [
[]
),
createCallExpressionStdLibKw(
'profileStartY',
createPipeSubstitution(),
]),
[]
),
])
),
]), // close the rectangle

View File

@ -8,6 +8,28 @@ import type { AsyncFn } from '@src/lib/types'
export const uuidv4 = v4
/**
* Refresh the browser page after reporting to Plausible.
*/
export async function refreshPage(method = 'UI button') {
if (window && 'plausible' in window) {
const p = window.plausible as (
event: string,
options?: { props: Record<string, string> }
) => Promise<void>
// Send a refresh event to Plausible so we can track how often users get stuck
await p('Refresh', {
props: {
method,
// optionally add more data here
},
})
}
// Window may not be available in some environments
window?.location.reload()
}
/**
* Get all labels for a keyword call expression.
*/

View File

@ -5,7 +5,7 @@ import type { Channel } from '@src/channels'
// types for knowing what menu sends what webContent payload
export type MenuLabels =
| 'Help.Command Palette...'
| 'Help.Refresh and report a bug'
| 'Help.Report a bug'
| 'Help.Reset onboarding'
| 'Edit.Rename project'
| 'Edit.Delete project'

View File

@ -62,12 +62,14 @@ export const helpRole = (
},
{ type: 'separator' },
{
label: 'Refresh and report a bug',
id: 'Help.Refresh and report a bug',
label: 'Report a bug',
id: 'Help.Report a bug',
click: () => {
typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', {
menuLabel: 'Help.Refresh and report a bug',
})
shell
.openExternal(
'https://github.com/KittyCAD/modeling-app/issues/new?template=bug_report.yml'
)
.catch(reportRejection)
},
},
{

View File

@ -39,7 +39,7 @@ type EditRoleLabel =
| 'Format code'
type HelpRoleLabel =
| 'Refresh and report a bug'
| 'Report a bug'
| 'Request a feature'
| 'Ask the community discord'
| 'Ask the community discourse'

View File

@ -76,6 +76,11 @@ const SignIn = () => {
authActor.send({ type: 'Log in', token })
}
const cancelSignIn = async () => {
authActor.send({ type: 'Log out' })
setUserCode('')
}
return (
<main
className="bg-primary h-screen grid place-items-stretch m-0 p-2"
@ -128,7 +133,10 @@ const SignIn = () => {
<p className="text-xs">
You should see the following code in your browser
</p>
<p className="text-lg font-bold inline-flex gap-1">
<p
className="text-lg font-bold inline-flex gap-1"
data-testid="sign-in-user-code"
>
{userCode.split('').map((char, i) => (
<span
key={i}
@ -141,6 +149,17 @@ const SignIn = () => {
</span>
))}
</p>
<button
onClick={toSync(cancelSignIn, reportRejection)}
className={
'm-0 mt-8 w-fit flex gap-4 items-center px-3 py-1 ' +
'!border-transparent !text-lg !text-chalkboard-10 !bg-primary hover:hue-rotate-15'
}
data-testid="cancel-sign-in-button"
>
<CustomIcon name="arrowLeft" className="w-6 h-6" />
Cancel
</button>
</>
)}
</div>
@ -187,7 +206,7 @@ const SignIn = () => {
'm-0 mt-8 flex gap-4 items-center px-3 py-1 ' +
'!border-transparent !text-lg !text-chalkboard-10 !bg-primary hover:hue-rotate-15'
}
data-testid="sign-in-button"
data-testid="view-sample-button"
>
View this sample
<CustomIcon name="arrowRight" className="w-6 h-6" />