Compare commits
10 Commits
pierremtb/
...
jtran/upda
Author | SHA1 | Date | |
---|---|---|---|
441e18e916 | |||
1502f923ee | |||
f03a684eec | |||
45e17c50e7 | |||
6bf74379a7 | |||
01c6fd53fa | |||
f8306c0275 | |||
900ef9e18d | |||
a46186573c | |||
90f6c1bb04 |
5
.github/workflows/e2e-tests.yml
vendored
@ -285,8 +285,7 @@ jobs:
|
|||||||
# TODO: enable namespace-profile-windows-latest once available
|
# TODO: enable namespace-profile-windows-latest once available
|
||||||
os:
|
os:
|
||||||
- "runs-on=${{ github.run_id }}/family=i7ie.2xlarge/image=ubuntu22-full-x64"
|
- "runs-on=${{ github.run_id }}/family=i7ie.2xlarge/image=ubuntu22-full-x64"
|
||||||
# TODO: renable this when macoOS runner seem more stable
|
- namespace-profile-macos-8-cores
|
||||||
# - namespace-profile-macos-6-cores
|
|
||||||
- windows-latest-8-cores
|
- windows-latest-8-cores
|
||||||
shardIndex: [1, 2, 3, 4]
|
shardIndex: [1, 2, 3, 4]
|
||||||
shardTotal: [4]
|
shardTotal: [4]
|
||||||
@ -296,7 +295,7 @@ jobs:
|
|||||||
isScheduled:
|
isScheduled:
|
||||||
- ${{ github.event_name == 'schedule' }}
|
- ${{ github.event_name == 'schedule' }}
|
||||||
exclude:
|
exclude:
|
||||||
- os: namespace-profile-macos-6-cores
|
- os: namespace-profile-macos-8-cores
|
||||||
isScheduled: true
|
isScheduled: true
|
||||||
- os: windows-latest-8-cores
|
- os: windows-latest-8-cores
|
||||||
isScheduled: true
|
isScheduled: true
|
||||||
|
@ -9,7 +9,7 @@ Extract the provided 2-dimensional sketch's profile's origin value.
|
|||||||
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
profileStart(sketch: Sketch): [number]
|
profileStart(profile: Sketch): [number]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ profileStart(sketch: Sketch): [number]
|
|||||||
|
|
||||||
| Name | Type | Description | Required |
|
| 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
|
### Returns
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ Extract the provided 2-dimensional sketch's profile's origin's 'x' value.
|
|||||||
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
profileStartX(sketch: Sketch): number
|
profileStartX(profile: Sketch): number
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ profileStartX(sketch: Sketch): number
|
|||||||
|
|
||||||
| Name | Type | Description | Required |
|
| 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
|
### Returns
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ Extract the provided 2-dimensional sketch's profile's origin's 'y' value.
|
|||||||
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
profileStartY(sketch: Sketch): number
|
profileStartY(profile: Sketch): number
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ profileStartY(sketch: Sketch): number
|
|||||||
|
|
||||||
| Name | Type | Description | Required |
|
| 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
|
### Returns
|
||||||
|
|
||||||
|
1656
docs/kcl/std.json
@ -29,11 +29,11 @@ test.describe('Electron app header tests', () => {
|
|||||||
test(
|
test(
|
||||||
'User settings has correct shortcut',
|
'User settings has correct shortcut',
|
||||||
{ tag: '@electron' },
|
{ tag: '@electron' },
|
||||||
async ({ page }, testInfo) => {
|
async ({ page, toolbar }, testInfo) => {
|
||||||
await page.setBodyDimensions({ width: 1200, height: 500 })
|
await page.setBodyDimensions({ width: 1200, height: 500 })
|
||||||
|
|
||||||
// Open the user sidebar menu.
|
// 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.
|
// No space after "User settings" since it's textContent.
|
||||||
const text =
|
const text =
|
||||||
|
53
e2e/playwright/auth.spec.ts
Normal 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 })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
@ -18,6 +18,7 @@ import type { Settings } from '@rust/kcl-lib/bindings/Settings'
|
|||||||
import { CmdBarFixture } from '@e2e/playwright/fixtures/cmdBarFixture'
|
import { CmdBarFixture } from '@e2e/playwright/fixtures/cmdBarFixture'
|
||||||
import { EditorFixture } from '@e2e/playwright/fixtures/editorFixture'
|
import { EditorFixture } from '@e2e/playwright/fixtures/editorFixture'
|
||||||
import { HomePageFixture } from '@e2e/playwright/fixtures/homePageFixture'
|
import { HomePageFixture } from '@e2e/playwright/fixtures/homePageFixture'
|
||||||
|
import { SignInPageFixture } from '@e2e/playwright/fixtures/signInPageFixture'
|
||||||
import { SceneFixture } from '@e2e/playwright/fixtures/sceneFixture'
|
import { SceneFixture } from '@e2e/playwright/fixtures/sceneFixture'
|
||||||
import { ToolbarFixture } from '@e2e/playwright/fixtures/toolbarFixture'
|
import { ToolbarFixture } from '@e2e/playwright/fixtures/toolbarFixture'
|
||||||
|
|
||||||
@ -66,6 +67,7 @@ export interface Fixtures {
|
|||||||
toolbar: ToolbarFixture
|
toolbar: ToolbarFixture
|
||||||
scene: SceneFixture
|
scene: SceneFixture
|
||||||
homePage: HomePageFixture
|
homePage: HomePageFixture
|
||||||
|
signInPage: SignInPageFixture
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ElectronZoo {
|
export class ElectronZoo {
|
||||||
@ -387,6 +389,9 @@ const fixturesBasedOnProcessEnvPlatform = {
|
|||||||
homePage: async ({ page }: { page: Page }, use: FnUse) => {
|
homePage: async ({ page }: { page: Page }, use: FnUse) => {
|
||||||
await use(new HomePageFixture(page))
|
await use(new HomePageFixture(page))
|
||||||
},
|
},
|
||||||
|
signInPage: async ({ page }: { page: Page }, use: FnUse) => {
|
||||||
|
await use(new SignInPageFixture(page))
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.PLATFORM === 'web') {
|
if (process.env.PLATFORM === 'web') {
|
||||||
|
48
e2e/playwright/fixtures/signInPageFixture.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -46,6 +46,9 @@ export class ToolbarFixture {
|
|||||||
gizmo!: Locator
|
gizmo!: Locator
|
||||||
gizmoDisabled!: Locator
|
gizmoDisabled!: Locator
|
||||||
loadButton!: Locator
|
loadButton!: Locator
|
||||||
|
/** User button for the user sidebar menu */
|
||||||
|
userSidebarButton!: Locator
|
||||||
|
signOutButton!: Locator
|
||||||
|
|
||||||
constructor(page: Page) {
|
constructor(page: Page) {
|
||||||
this.page = page
|
this.page = page
|
||||||
@ -82,6 +85,9 @@ export class ToolbarFixture {
|
|||||||
// element or two different elements can represent these states.
|
// element or two different elements can represent these states.
|
||||||
this.gizmo = page.getByTestId('gizmo')
|
this.gizmo = page.getByTestId('gizmo')
|
||||||
this.gizmoDisabled = page.getByTestId('gizmo-disabled')
|
this.gizmoDisabled = page.getByTestId('gizmo-disabled')
|
||||||
|
|
||||||
|
this.userSidebarButton = page.getByTestId('user-sidebar-toggle')
|
||||||
|
this.signOutButton = page.getByTestId('user-sidebar-sign-out')
|
||||||
}
|
}
|
||||||
|
|
||||||
get logoLink() {
|
get logoLink() {
|
||||||
|
@ -409,11 +409,7 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => {
|
|||||||
)
|
)
|
||||||
.toBe(true)
|
.toBe(true)
|
||||||
})
|
})
|
||||||
test('Home.Help.Refresh and report a bug', async ({
|
test('Home.Help.Report a bug', async ({ tronApp, cmdBar, page }) => {
|
||||||
tronApp,
|
|
||||||
cmdBar,
|
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
if (!tronApp) fail()
|
if (!tronApp) fail()
|
||||||
// Run electron snippet to find the Menu!
|
// Run electron snippet to find the Menu!
|
||||||
await page.waitForTimeout(100) // wait for createModelingPageMenu() to run
|
await page.waitForTimeout(100) // wait for createModelingPageMenu() to run
|
||||||
@ -424,9 +420,8 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => {
|
|||||||
if (!app || !app.applicationMenu) {
|
if (!app || !app.applicationMenu) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
const menu = app.applicationMenu.getMenuItemById(
|
const menu =
|
||||||
'Help.Refresh and report a bug'
|
app.applicationMenu.getMenuItemById('Help.Report a bug')
|
||||||
)
|
|
||||||
if (!menu) return false
|
if (!menu) return false
|
||||||
menu.click()
|
menu.click()
|
||||||
return true
|
return true
|
||||||
@ -2291,7 +2286,7 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => {
|
|||||||
if (!menu) fail()
|
if (!menu) fail()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
test('Modeling.Help.Refresh and report a bug', async ({
|
test('Modeling.Help.Report a bug', async ({
|
||||||
tronApp,
|
tronApp,
|
||||||
cmdBar,
|
cmdBar,
|
||||||
page,
|
page,
|
||||||
@ -2315,9 +2310,8 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => {
|
|||||||
async () =>
|
async () =>
|
||||||
await tronApp.electron.evaluate(async ({ app }) => {
|
await tronApp.electron.evaluate(async ({ app }) => {
|
||||||
if (!app || !app.applicationMenu) return false
|
if (!app || !app.applicationMenu) return false
|
||||||
const menu = app.applicationMenu.getMenuItemById(
|
const menu =
|
||||||
'Help.Refresh and report a bug'
|
app.applicationMenu.getMenuItemById('Help.Report a bug')
|
||||||
)
|
|
||||||
if (!menu) return false
|
if (!menu) return false
|
||||||
menu.click()
|
menu.click()
|
||||||
return true
|
return true
|
||||||
|
@ -331,6 +331,7 @@ test.describe('Onboarding tests', () => {
|
|||||||
test('Avatar text updates depending on image load success', async ({
|
test('Avatar text updates depending on image load success', async ({
|
||||||
context,
|
context,
|
||||||
page,
|
page,
|
||||||
|
toolbar,
|
||||||
homePage,
|
homePage,
|
||||||
tronApp,
|
tronApp,
|
||||||
}) => {
|
}) => {
|
||||||
@ -362,7 +363,7 @@ test.describe('Onboarding tests', () => {
|
|||||||
await homePage.goToModelingScene()
|
await homePage.goToModelingScene()
|
||||||
|
|
||||||
// Test that the text in this step is correct
|
// 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
|
const onboardingOverlayLocator = page
|
||||||
.getByTestId('onboarding-content')
|
.getByTestId('onboarding-content')
|
||||||
.locator('div')
|
.locator('div')
|
||||||
@ -404,6 +405,7 @@ test.describe('Onboarding tests', () => {
|
|||||||
test("Avatar text doesn't mention avatar when no avatar", async ({
|
test("Avatar text doesn't mention avatar when no avatar", async ({
|
||||||
context,
|
context,
|
||||||
page,
|
page,
|
||||||
|
toolbar,
|
||||||
homePage,
|
homePage,
|
||||||
tronApp,
|
tronApp,
|
||||||
}) => {
|
}) => {
|
||||||
@ -435,7 +437,7 @@ test.describe('Onboarding tests', () => {
|
|||||||
await homePage.goToModelingScene()
|
await homePage.goToModelingScene()
|
||||||
|
|
||||||
// Test that the text in this step is correct
|
// 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 avatar = sidebar.locator('img')
|
||||||
const onboardingOverlayLocator = page
|
const onboardingOverlayLocator = page
|
||||||
.getByTestId('onboarding-content')
|
.getByTestId('onboarding-content')
|
||||||
@ -464,6 +466,7 @@ test.describe('Onboarding tests', () => {
|
|||||||
test('Restarting onboarding on desktop takes one attempt', async ({
|
test('Restarting onboarding on desktop takes one attempt', async ({
|
||||||
context,
|
context,
|
||||||
page,
|
page,
|
||||||
|
toolbar,
|
||||||
tronApp,
|
tronApp,
|
||||||
}) => {
|
}) => {
|
||||||
test.fixme(orRunWhenFullSuiteEnabled())
|
test.fixme(orRunWhenFullSuiteEnabled())
|
||||||
@ -502,7 +505,7 @@ test('Restarting onboarding on desktop takes one attempt', async ({
|
|||||||
.filter({ hasText: 'Tutorial Project 00' })
|
.filter({ hasText: 'Tutorial Project 00' })
|
||||||
const tutorialModalText = page.getByText('Welcome to Design Studio!')
|
const tutorialModalText = page.getByText('Welcome to Design Studio!')
|
||||||
const tutorialDismissButton = page.getByRole('button', { name: 'Dismiss' })
|
const tutorialDismissButton = page.getByRole('button', { name: 'Dismiss' })
|
||||||
const userMenuButton = page.getByTestId('user-sidebar-toggle')
|
const userMenuButton = toolbar.userSidebarButton
|
||||||
const userMenuSettingsButton = page.getByRole('button', {
|
const userMenuSettingsButton = page.getByRole('button', {
|
||||||
name: 'User settings',
|
name: 'User settings',
|
||||||
})
|
})
|
||||||
|
@ -400,11 +400,6 @@ test(
|
|||||||
await expect(page.getByText('broken-code')).toBeVisible()
|
await expect(page.getByText('broken-code')).toBeVisible()
|
||||||
await page.getByText('broken-code').click()
|
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
|
// Gotcha: Scroll to the text content in code mirror because CodeMirror lazy loads DOM content
|
||||||
await editor.scrollToText(
|
await editor.scrollToText(
|
||||||
"|> line(end = [0, wallMountL], tag = 'outerEdge')"
|
"|> line(end = [0, wallMountL], tag = 'outerEdge')"
|
||||||
@ -779,7 +774,9 @@ test.describe(`Project management commands`, () => {
|
|||||||
// Constants and locators
|
// Constants and locators
|
||||||
const projectHomeLink = page.getByTestId('project-link')
|
const projectHomeLink = page.getByTestId('project-link')
|
||||||
const commandButton = page.getByRole('button', { name: 'Commands' })
|
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 projectNameOption = page.getByRole('option', { name: projectName })
|
||||||
const projectRenamedName = `untitled`
|
const projectRenamedName = `untitled`
|
||||||
// const projectMenuButton = page.getByTestId('project-sidebar-toggle')
|
// const projectMenuButton = page.getByTestId('project-sidebar-toggle')
|
||||||
@ -839,7 +836,9 @@ test.describe(`Project management commands`, () => {
|
|||||||
// Constants and locators
|
// Constants and locators
|
||||||
const projectHomeLink = page.getByTestId('project-link')
|
const projectHomeLink = page.getByTestId('project-link')
|
||||||
const commandButton = page.getByRole('button', { name: 'Commands' })
|
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 projectNameOption = page.getByRole('option', { name: projectName })
|
||||||
const commandWarning = page.getByText('Are you sure you want to delete?')
|
const commandWarning = page.getByText('Are you sure you want to delete?')
|
||||||
const commandSubmitButton = page.getByRole('button', {
|
const commandSubmitButton = page.getByRole('button', {
|
||||||
@ -891,7 +890,9 @@ test.describe(`Project management commands`, () => {
|
|||||||
// Constants and locators
|
// Constants and locators
|
||||||
const projectHomeLink = page.getByTestId('project-link')
|
const projectHomeLink = page.getByTestId('project-link')
|
||||||
const commandButton = page.getByRole('button', { name: 'Commands' })
|
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 projectNameOption = page.getByRole('option', { name: projectName })
|
||||||
const projectRenamedName = `untitled`
|
const projectRenamedName = `untitled`
|
||||||
const commandContinueButton = page.getByRole('button', {
|
const commandContinueButton = page.getByRole('button', {
|
||||||
@ -947,7 +948,9 @@ test.describe(`Project management commands`, () => {
|
|||||||
// Constants and locators
|
// Constants and locators
|
||||||
const projectHomeLink = page.getByTestId('project-link')
|
const projectHomeLink = page.getByTestId('project-link')
|
||||||
const commandButton = page.getByRole('button', { name: 'Commands' })
|
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 projectNameOption = page.getByRole('option', { name: projectName })
|
||||||
const commandWarning = page.getByText('Are you sure you want to delete?')
|
const commandWarning = page.getByText('Are you sure you want to delete?')
|
||||||
const commandSubmitButton = page.getByRole('button', {
|
const commandSubmitButton = page.getByRole('button', {
|
||||||
@ -1962,13 +1965,13 @@ test(
|
|||||||
test(
|
test(
|
||||||
'Settings persist across restarts',
|
'Settings persist across restarts',
|
||||||
{ tag: '@electron' },
|
{ tag: '@electron' },
|
||||||
async ({ page, scene, cmdBar }, testInfo) => {
|
async ({ page, toolbar }, testInfo) => {
|
||||||
await test.step('We can change a user setting like theme', async () => {
|
await test.step('We can change a user setting like theme', async () => {
|
||||||
await page.setBodyDimensions({ width: 1200, height: 500 })
|
await page.setBodyDimensions({ width: 1200, height: 500 })
|
||||||
|
|
||||||
page.on('console', console.log)
|
page.on('console', console.log)
|
||||||
|
|
||||||
await page.getByTestId('user-sidebar-toggle').click()
|
await toolbar.userSidebarButton.click()
|
||||||
|
|
||||||
await page.getByTestId('user-settings').click()
|
await page.getByTestId('user-settings').click()
|
||||||
|
|
||||||
@ -1995,7 +1998,7 @@ test(
|
|||||||
test(
|
test(
|
||||||
'Original project name persist after onboarding',
|
'Original project name persist after onboarding',
|
||||||
{ tag: '@electron' },
|
{ tag: '@electron' },
|
||||||
async ({ page }, testInfo) => {
|
async ({ page, toolbar }, testInfo) => {
|
||||||
test.fixme(orRunWhenFullSuiteEnabled())
|
test.fixme(orRunWhenFullSuiteEnabled())
|
||||||
await page.setBodyDimensions({ width: 1200, height: 500 })
|
await page.setBodyDimensions({ width: 1200, height: 500 })
|
||||||
|
|
||||||
@ -2007,7 +2010,7 @@ test(
|
|||||||
})
|
})
|
||||||
|
|
||||||
await test.step('Should go through onboarding', async () => {
|
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.getByTestId('user-settings').click()
|
||||||
await page.getByRole('button', { name: 'Replay Onboarding' }).click()
|
await page.getByRole('button', { name: 'Replay Onboarding' }).click()
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 134 KiB |
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 117 KiB |
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
@ -36,7 +36,6 @@ export const headerMasks = (page: Page) => [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export const networkingMasks = (page: Page) => [
|
export const networkingMasks = (page: Page) => [
|
||||||
page.getByTestId('model-state-indicator'),
|
|
||||||
page.getByTestId('network-toggle'),
|
page.getByTestId('network-toggle'),
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -85,12 +84,6 @@ async function waitForPageLoadWithRetry(page: Page) {
|
|||||||
await expect(async () => {
|
await expect(async () => {
|
||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
const errorMessage = 'App failed to load - 🔃 Retrying ...'
|
const errorMessage = 'App failed to load - 🔃 Retrying ...'
|
||||||
await expect(
|
|
||||||
page.getByTestId('model-state-indicator-playing'),
|
|
||||||
errorMessage
|
|
||||||
).toBeAttached({
|
|
||||||
timeout: 20_000,
|
|
||||||
})
|
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'sketch Start Sketch' }),
|
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.
|
// lee: This needs to be replaced by scene.settled() eventually.
|
||||||
async function waitForPageLoad(page: Page) {
|
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({
|
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeEnabled({
|
||||||
timeout: 20_000,
|
timeout: 20_000,
|
||||||
})
|
})
|
||||||
|
20
rust/Cargo.lock
generated
@ -1792,7 +1792,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kcl-bumper"
|
name = "kcl-bumper"
|
||||||
version = "0.1.62"
|
version = "0.1.63"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
@ -1803,7 +1803,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kcl-derive-docs"
|
name = "kcl-derive-docs"
|
||||||
version = "0.1.62"
|
version = "0.1.63"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"Inflector",
|
"Inflector",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@ -1822,7 +1822,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kcl-directory-test-macro"
|
name = "kcl-directory-test-macro"
|
||||||
version = "0.1.62"
|
version = "0.1.63"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -1831,7 +1831,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kcl-language-server"
|
name = "kcl-language-server"
|
||||||
version = "0.2.62"
|
version = "0.2.63"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
@ -1852,7 +1852,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kcl-language-server-release"
|
name = "kcl-language-server-release"
|
||||||
version = "0.1.62"
|
version = "0.1.63"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
@ -1872,7 +1872,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kcl-lib"
|
name = "kcl-lib"
|
||||||
version = "0.2.62"
|
version = "0.2.63"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"approx 0.5.1",
|
"approx 0.5.1",
|
||||||
@ -1943,7 +1943,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kcl-python-bindings"
|
name = "kcl-python-bindings"
|
||||||
version = "0.3.62"
|
version = "0.3.63"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"kcl-lib",
|
"kcl-lib",
|
||||||
@ -1958,7 +1958,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kcl-test-server"
|
name = "kcl-test-server"
|
||||||
version = "0.1.62"
|
version = "0.1.63"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"hyper 0.14.32",
|
"hyper 0.14.32",
|
||||||
@ -1971,7 +1971,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kcl-to-core"
|
name = "kcl-to-core"
|
||||||
version = "0.1.62"
|
version = "0.1.63"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@ -1985,7 +1985,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kcl-wasm-lib"
|
name = "kcl-wasm-lib"
|
||||||
version = "0.1.62"
|
version = "0.1.63"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bson",
|
"bson",
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "kcl-bumper"
|
name = "kcl-bumper"
|
||||||
version = "0.1.62"
|
version = "0.1.63"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
repository = "https://github.com/KittyCAD/modeling-api"
|
repository = "https://github.com/KittyCAD/modeling-api"
|
||||||
rust-version = "1.76"
|
rust-version = "1.76"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "kcl-derive-docs"
|
name = "kcl-derive-docs"
|
||||||
description = "A tool for generating documentation from Rust derive macros"
|
description = "A tool for generating documentation from Rust derive macros"
|
||||||
version = "0.1.62"
|
version = "0.1.63"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/KittyCAD/modeling-app"
|
repository = "https://github.com/KittyCAD/modeling-app"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "kcl-directory-test-macro"
|
name = "kcl-directory-test-macro"
|
||||||
description = "A tool for generating tests from a directory of kcl files"
|
description = "A tool for generating tests from a directory of kcl files"
|
||||||
version = "0.1.62"
|
version = "0.1.63"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/KittyCAD/modeling-app"
|
repository = "https://github.com/KittyCAD/modeling-app"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "kcl-language-server-release"
|
name = "kcl-language-server-release"
|
||||||
version = "0.1.62"
|
version = "0.1.63"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["KittyCAD Inc <kcl@kittycad.io>"]
|
authors = ["KittyCAD Inc <kcl@kittycad.io>"]
|
||||||
publish = false
|
publish = false
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
name = "kcl-language-server"
|
name = "kcl-language-server"
|
||||||
description = "A language server for KCL."
|
description = "A language server for KCL."
|
||||||
authors = ["KittyCAD Inc <kcl@kittycad.io>"]
|
authors = ["KittyCAD Inc <kcl@kittycad.io>"]
|
||||||
version = "0.2.62"
|
version = "0.2.63"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "kcl-lib"
|
name = "kcl-lib"
|
||||||
description = "KittyCAD Language implementation and tools"
|
description = "KittyCAD Language implementation and tools"
|
||||||
version = "0.2.62"
|
version = "0.2.63"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/KittyCAD/modeling-app"
|
repository = "https://github.com/KittyCAD/modeling-app"
|
||||||
|
@ -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).
|
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.
|
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
|
## 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:
|
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:
|
||||||
|
@ -1820,18 +1820,6 @@ const bracket = startSketchOn(XY)
|
|||||||
parse_execute(ast).await.unwrap();
|
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")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_unary_operator_not_succeeds() {
|
async fn test_unary_operator_not_succeeds() {
|
||||||
let ast = r#"
|
let ast = r#"
|
||||||
|
@ -2074,6 +2074,7 @@ fn possible_operands(i: &mut TokenSlice) -> PResult<Expr> {
|
|||||||
member_expression.map(Box::new).map(Expr::MemberExpression),
|
member_expression.map(Box::new).map(Expr::MemberExpression),
|
||||||
literal.map(Expr::Literal),
|
literal.map(Expr::Literal),
|
||||||
fn_call.map(Box::new).map(Expr::CallExpression),
|
fn_call.map(Box::new).map(Expr::CallExpression),
|
||||||
|
fn_call_kw.map(Box::new).map(Expr::CallExpressionKw),
|
||||||
name.map(Box::new).map(Expr::Name),
|
name.map(Box::new).map(Expr::Name),
|
||||||
binary_expr_in_parens.map(Box::new).map(Expr::BinaryExpression),
|
binary_expr_in_parens.map(Box::new).map(Expr::BinaryExpression),
|
||||||
unnecessarily_bracketed,
|
unnecessarily_bracketed,
|
||||||
@ -3254,6 +3255,14 @@ mod tests {
|
|||||||
assert_eq!(err.message, "Unexpected end of file. The compiler expected )");
|
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]
|
#[test]
|
||||||
fn weird_program_just_a_pipe() {
|
fn weird_program_just_a_pipe() {
|
||||||
let tokens = crate::parsing::token::lex("|", ModuleId::default()).unwrap();
|
let tokens = crate::parsing::token::lex("|", ModuleId::default()).unwrap();
|
||||||
@ -5389,6 +5398,7 @@ my14 = 4 ^ 2 - 3 ^ 2 * 2
|
|||||||
bar = x,
|
bar = x,
|
||||||
)"#
|
)"#
|
||||||
);
|
);
|
||||||
|
snapshot_test!(kw_function_in_binary_op, r#"val = f(x = 1) + 1"#);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
|
@ -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
|
||||||
|
}
|
@ -675,28 +675,6 @@ impl Args {
|
|||||||
Ok((sketches, sketch))
|
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>
|
pub(crate) fn get_data<'a, T>(&'a self) -> Result<T, KclError>
|
||||||
where
|
where
|
||||||
T: FromArgs<'a>,
|
T: FromArgs<'a>,
|
||||||
@ -708,49 +686,6 @@ impl Args {
|
|||||||
FromArgs::from_args(self, 0)
|
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> {
|
pub(crate) fn get_tag_to_number_sketch(&self) -> Result<(TagIdentifier, TyF64, Sketch), KclError> {
|
||||||
FromArgs::from_args(self, 0)
|
FromArgs::from_args(self, 0)
|
||||||
}
|
}
|
||||||
|
@ -247,9 +247,10 @@ async fn inner_shell(
|
|||||||
|
|
||||||
/// Make the inside of a 3D object hollow.
|
/// Make the inside of a 3D object hollow.
|
||||||
pub async fn hollow(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
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 })
|
Ok(KclValue::Solid { value })
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -267,7 +268,7 @@ pub async fn hollow(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
|
|||||||
/// |> line(end = [-24, 0])
|
/// |> line(end = [-24, 0])
|
||||||
/// |> close()
|
/// |> close()
|
||||||
/// |> extrude(length = 6)
|
/// |> extrude(length = 6)
|
||||||
/// |> hollow (0.25, %)
|
/// |> hollow(thickness = 0.25)
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
@ -279,7 +280,7 @@ pub async fn hollow(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
|
|||||||
/// |> line(end = [-24, 0])
|
/// |> line(end = [-24, 0])
|
||||||
/// |> close()
|
/// |> close()
|
||||||
/// |> extrude(length = 6)
|
/// |> extrude(length = 6)
|
||||||
/// |> hollow (0.5, %)
|
/// |> hollow(thickness = 0.5)
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```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 )
|
/// |> circle( center = [size / 2, -size / 2], radius = 25 )
|
||||||
/// |> extrude(length = 50)
|
/// |> extrude(length = 50)
|
||||||
///
|
///
|
||||||
/// hollow(0.5, case)
|
/// hollow(case, thickness = 0.5)
|
||||||
/// ```
|
/// ```
|
||||||
#[stdlib {
|
#[stdlib {
|
||||||
name = "hollow",
|
name = "hollow",
|
||||||
feature_tree_operation = true,
|
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(
|
async fn inner_hollow(
|
||||||
thickness: TyF64,
|
|
||||||
solid: Box<Solid>,
|
solid: Box<Solid>,
|
||||||
|
thickness: TyF64,
|
||||||
exec_state: &mut ExecState,
|
exec_state: &mut ExecState,
|
||||||
args: Args,
|
args: Args,
|
||||||
) -> Result<Box<Solid>, KclError> {
|
) -> Result<Box<Solid>, KclError> {
|
||||||
|
@ -1454,7 +1454,7 @@ pub(crate) async fn inner_start_profile_at(
|
|||||||
|
|
||||||
/// Returns the X component of the sketch profile start point.
|
/// 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> {
|
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 ty = sketch.units.into();
|
||||||
let x = inner_profile_start_x(sketch)?;
|
let x = inner_profile_start_x(sketch)?;
|
||||||
Ok(args.make_user_val_from_f64_with_type(TyF64::new(x, ty)))
|
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(%))
|
/// |> angledLine(angle = 30, endAbsoluteX = profileStartX(%))
|
||||||
/// ```
|
/// ```
|
||||||
#[stdlib {
|
#[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> {
|
pub(crate) fn inner_profile_start_x(profile: Sketch) -> Result<f64, KclError> {
|
||||||
Ok(sketch.start.to[0])
|
Ok(profile.start.to[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the Y component of the sketch profile start point.
|
/// 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> {
|
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 ty = sketch.units.into();
|
||||||
let x = inner_profile_start_y(sketch)?;
|
let x = inner_profile_start_y(sketch)?;
|
||||||
Ok(args.make_user_val_from_f64_with_type(TyF64::new(x, ty)))
|
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(%))
|
/// |> angledLine(angle = 30, endAbsoluteY = profileStartY(%))
|
||||||
/// ```
|
/// ```
|
||||||
#[stdlib {
|
#[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> {
|
pub(crate) fn inner_profile_start_y(profile: Sketch) -> Result<f64, KclError> {
|
||||||
Ok(sketch.start.to[1])
|
Ok(profile.start.to[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the sketch profile start point.
|
/// Returns the sketch profile start point.
|
||||||
pub async fn profile_start(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
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 ty = sketch.units.into();
|
||||||
let point = inner_profile_start(sketch)?;
|
let point = inner_profile_start(sketch)?;
|
||||||
Ok(KclValue::from_point2d(point, ty, args.into()))
|
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)
|
/// |> extrude(length = 20)
|
||||||
/// ```
|
/// ```
|
||||||
#[stdlib {
|
#[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> {
|
pub(crate) fn inner_profile_start(profile: Sketch) -> Result<[f64; 2], KclError> {
|
||||||
Ok(sketch.start.to)
|
Ok(profile.start.to)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Close the current sketch.
|
/// Close the current sketch.
|
||||||
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 67 KiB |
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "kcl-python-bindings"
|
name = "kcl-python-bindings"
|
||||||
version = "0.3.62"
|
version = "0.3.63"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
repository = "https://github.com/kittycad/modeling-app"
|
repository = "https://github.com/kittycad/modeling-app"
|
||||||
exclude = ["tests/*", "files/*", "venv/*"]
|
exclude = ["tests/*", "files/*", "venv/*"]
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "kcl-test-server"
|
name = "kcl-test-server"
|
||||||
description = "A test server for KCL"
|
description = "A test server for KCL"
|
||||||
version = "0.1.62"
|
version = "0.1.63"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "kcl-to-core"
|
name = "kcl-to-core"
|
||||||
description = "Utility methods to convert kcl to engine core executable tests"
|
description = "Utility methods to convert kcl to engine core executable tests"
|
||||||
version = "0.1.62"
|
version = "0.1.63"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/KittyCAD/modeling-app"
|
repository = "https://github.com/KittyCAD/modeling-app"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "kcl-wasm-lib"
|
name = "kcl-wasm-lib"
|
||||||
version = "0.1.62"
|
version = "0.1.63"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
repository = "https://github.com/KittyCAD/modeling-app"
|
repository = "https://github.com/KittyCAD/modeling-app"
|
||||||
rust-version = "1.83"
|
rust-version = "1.83"
|
||||||
|
34
src/App.tsx
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useMemo, useRef } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import ModalContainer from 'react-modal-promise'
|
import ModalContainer from 'react-modal-promise'
|
||||||
@ -21,20 +21,14 @@ import { useAbsoluteFilePath } from '@src/hooks/useAbsoluteFilePath'
|
|||||||
import { useCreateFileLinkQuery } from '@src/hooks/useCreateFileLinkQueryWatcher'
|
import { useCreateFileLinkQuery } from '@src/hooks/useCreateFileLinkQueryWatcher'
|
||||||
import { useEngineConnectionSubscriptions } from '@src/hooks/useEngineConnectionSubscriptions'
|
import { useEngineConnectionSubscriptions } from '@src/hooks/useEngineConnectionSubscriptions'
|
||||||
import { useHotKeyListener } from '@src/hooks/useHotKeyListener'
|
import { useHotKeyListener } from '@src/hooks/useHotKeyListener'
|
||||||
import { CoreDumpManager } from '@src/lib/coredump'
|
|
||||||
import { writeProjectThumbnailFile } from '@src/lib/desktop'
|
import { writeProjectThumbnailFile } from '@src/lib/desktop'
|
||||||
import useHotkeyWrapper from '@src/lib/hotkeyWrapper'
|
import useHotkeyWrapper from '@src/lib/hotkeyWrapper'
|
||||||
import { isDesktop } from '@src/lib/isDesktop'
|
import { isDesktop } from '@src/lib/isDesktop'
|
||||||
import { PATHS } from '@src/lib/paths'
|
import { PATHS } from '@src/lib/paths'
|
||||||
import { takeScreenshotOfVideoStreamCanvas } from '@src/lib/screenshot'
|
import { takeScreenshotOfVideoStreamCanvas } from '@src/lib/screenshot'
|
||||||
import {
|
import { sceneInfra } from '@src/lib/singletons'
|
||||||
codeManager,
|
|
||||||
engineCommandManager,
|
|
||||||
rustContext,
|
|
||||||
sceneInfra,
|
|
||||||
} from '@src/lib/singletons'
|
|
||||||
import { maybeWriteToDisk } from '@src/lib/telemetry'
|
import { maybeWriteToDisk } from '@src/lib/telemetry'
|
||||||
import { type IndexLoaderData } from '@src/lib/types'
|
import type { IndexLoaderData } from '@src/lib/types'
|
||||||
import {
|
import {
|
||||||
engineStreamActor,
|
engineStreamActor,
|
||||||
useSettings,
|
useSettings,
|
||||||
@ -43,6 +37,8 @@ import {
|
|||||||
import { commandBarActor } from '@src/machines/commandBarMachine'
|
import { commandBarActor } from '@src/machines/commandBarMachine'
|
||||||
import { EngineStreamTransition } from '@src/machines/engineStreamMachine'
|
import { EngineStreamTransition } from '@src/machines/engineStreamMachine'
|
||||||
import { onboardingPaths } from '@src/routes/Onboarding/paths'
|
import { onboardingPaths } from '@src/routes/Onboarding/paths'
|
||||||
|
import { CommandBarOpenButton } from '@src/components/CommandBarOpenButton'
|
||||||
|
import { ShareButton } from '@src/components/ShareButton'
|
||||||
|
|
||||||
// CYCLIC REF
|
// CYCLIC REF
|
||||||
sceneInfra.camControls.engineStreamActor = engineStreamActor
|
sceneInfra.camControls.engineStreamActor = engineStreamActor
|
||||||
@ -93,17 +89,6 @@ export function App() {
|
|||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
const authToken = useToken()
|
const authToken = useToken()
|
||||||
|
|
||||||
const coreDumpManager = useMemo(
|
|
||||||
() =>
|
|
||||||
new CoreDumpManager(
|
|
||||||
engineCommandManager,
|
|
||||||
codeManager,
|
|
||||||
rustContext,
|
|
||||||
authToken
|
|
||||||
),
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
app: { onboardingStatus },
|
app: { onboardingStatus },
|
||||||
} = settings
|
} = settings
|
||||||
@ -163,15 +148,18 @@ export function App() {
|
|||||||
return (
|
return (
|
||||||
<div className="relative h-full flex flex-col" ref={ref}>
|
<div className="relative h-full flex flex-col" ref={ref}>
|
||||||
<AppHeader
|
<AppHeader
|
||||||
className={'transition-opacity transition-duration-75 ' + paneOpacity}
|
className={`transition-opacity transition-duration-75 ${paneOpacity}`}
|
||||||
project={{ project, file }}
|
project={{ project, file }}
|
||||||
enableMenu={true}
|
enableMenu={true}
|
||||||
/>
|
>
|
||||||
|
<CommandBarOpenButton />
|
||||||
|
<ShareButton />
|
||||||
|
</AppHeader>
|
||||||
<ModalContainer />
|
<ModalContainer />
|
||||||
<ModelingSidebar paneOpacity={paneOpacity} />
|
<ModelingSidebar paneOpacity={paneOpacity} />
|
||||||
<EngineStream pool={pool} authToken={authToken} />
|
<EngineStream pool={pool} authToken={authToken} />
|
||||||
{/* <CamToggle /> */}
|
{/* <CamToggle /> */}
|
||||||
<LowerRightControls coreDumpManager={coreDumpManager}>
|
<LowerRightControls>
|
||||||
<UnitsMenu />
|
<UnitsMenu />
|
||||||
<Gizmo />
|
<Gizmo />
|
||||||
</LowerRightControls>
|
</LowerRightControls>
|
||||||
|
@ -1013,13 +1013,18 @@ export class SceneEntities {
|
|||||||
// Snapping logic for the profile start handle
|
// Snapping logic for the profile start handle
|
||||||
if (intersectsProfileStart) {
|
if (intersectsProfileStart) {
|
||||||
const originCoords = createArrayExpression([
|
const originCoords = createArrayExpression([
|
||||||
createCallExpressionStdLib('profileStartX', [
|
createCallExpressionStdLibKw(
|
||||||
|
'profileStartX',
|
||||||
createPipeSubstitution(),
|
createPipeSubstitution(),
|
||||||
]),
|
[]
|
||||||
createCallExpressionStdLib('profileStartY', [
|
),
|
||||||
|
createCallExpressionStdLibKw(
|
||||||
|
'profileStartY',
|
||||||
createPipeSubstitution(),
|
createPipeSubstitution(),
|
||||||
]),
|
[]
|
||||||
|
),
|
||||||
])
|
])
|
||||||
|
|
||||||
modifiedAst = addCallExpressionsToPipe({
|
modifiedAst = addCallExpressionsToPipe({
|
||||||
node: this.kclManager.ast,
|
node: this.kclManager.ast,
|
||||||
variables: this.kclManager.variables,
|
variables: this.kclManager.variables,
|
||||||
@ -2213,13 +2218,18 @@ export class SceneEntities {
|
|||||||
modded = moddedResult.modifiedAst
|
modded = moddedResult.modifiedAst
|
||||||
if (intersectsProfileStart) {
|
if (intersectsProfileStart) {
|
||||||
const originCoords = createArrayExpression([
|
const originCoords = createArrayExpression([
|
||||||
createCallExpressionStdLib('profileStartX', [
|
createCallExpressionStdLibKw(
|
||||||
|
'profileStartX',
|
||||||
createPipeSubstitution(),
|
createPipeSubstitution(),
|
||||||
]),
|
[]
|
||||||
createCallExpressionStdLib('profileStartY', [
|
),
|
||||||
|
createCallExpressionStdLibKw(
|
||||||
|
'profileStartY',
|
||||||
createPipeSubstitution(),
|
createPipeSubstitution(),
|
||||||
]),
|
[]
|
||||||
|
),
|
||||||
])
|
])
|
||||||
|
|
||||||
const arcToCallExp = getNodeFromPath<CallExpression>(
|
const arcToCallExp = getNodeFromPath<CallExpression>(
|
||||||
modded,
|
modded,
|
||||||
mod.pathToNode,
|
mod.pathToNode,
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { Toolbar } from '@src/Toolbar'
|
import { Toolbar } from '@src/Toolbar'
|
||||||
import { CommandBarOpenButton } from '@src/components/CommandBarOpenButton'
|
import { CommandBarOpenButton } from '@src/components/CommandBarOpenButton'
|
||||||
import ProjectSidebarMenu from '@src/components/ProjectSidebarMenu'
|
import ProjectSidebarMenu from '@src/components/ProjectSidebarMenu'
|
||||||
import { RefreshButton } from '@src/components/RefreshButton'
|
|
||||||
import UserSidebarMenu from '@src/components/UserSidebarMenu'
|
import UserSidebarMenu from '@src/components/UserSidebarMenu'
|
||||||
import { isDesktop } from '@src/lib/isDesktop'
|
import { isDesktop } from '@src/lib/isDesktop'
|
||||||
import { type IndexLoaderData } from '@src/lib/types'
|
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">
|
<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 />}
|
{showToolbar && <Toolbar />}
|
||||||
</div>
|
</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 */}
|
{/* If there are children, show them, otherwise show User menu */}
|
||||||
{children || (
|
{children || <CommandBarOpenButton />}
|
||||||
<>
|
|
||||||
<CommandBarOpenButton />
|
|
||||||
<RefreshButton />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<UserSidebarMenu user={user} />
|
<UserSidebarMenu user={user} />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
@ -2,18 +2,21 @@ import { COMMAND_PALETTE_HOTKEY } from '@src/components/CommandBar/CommandBar'
|
|||||||
import usePlatform from '@src/hooks/usePlatform'
|
import usePlatform from '@src/hooks/usePlatform'
|
||||||
import { hotkeyDisplay } from '@src/lib/hotkeyWrapper'
|
import { hotkeyDisplay } from '@src/lib/hotkeyWrapper'
|
||||||
import { commandBarActor } from '@src/machines/commandBarMachine'
|
import { commandBarActor } from '@src/machines/commandBarMachine'
|
||||||
|
import { CustomIcon } from '@src/components/CustomIcon'
|
||||||
|
|
||||||
export function CommandBarOpenButton() {
|
export function CommandBarOpenButton() {
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<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' })}
|
onClick={() => commandBarActor.send({ type: 'Open' })}
|
||||||
data-testid="command-bar-open-button"
|
data-testid="command-bar-open-button"
|
||||||
>
|
>
|
||||||
|
<CustomIcon name="command" className="w-5 h-5" />
|
||||||
<span>Commands</span>
|
<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)}
|
{hotkeyDisplay(COMMAND_PALETTE_HOTKEY, platform)}
|
||||||
</kbd>
|
</kbd>
|
||||||
</button>
|
</button>
|
||||||
|
@ -311,6 +311,22 @@ const CustomIconMap = {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</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: (
|
dimension: (
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 20 20"
|
viewBox="0 0 20 20"
|
||||||
|
@ -1,26 +1,19 @@
|
|||||||
import toast from 'react-hot-toast'
|
|
||||||
import { Link, useLocation } from 'react-router-dom'
|
import { Link, useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
import { CustomIcon } from '@src/components/CustomIcon'
|
import { CustomIcon } from '@src/components/CustomIcon'
|
||||||
import { HelpMenu } from '@src/components/HelpMenu'
|
import { HelpMenu } from '@src/components/HelpMenu'
|
||||||
import { ModelStateIndicator } from '@src/components/ModelStateIndicator'
|
|
||||||
import { NetworkHealthIndicator } from '@src/components/NetworkHealthIndicator'
|
import { NetworkHealthIndicator } from '@src/components/NetworkHealthIndicator'
|
||||||
import { NetworkMachineIndicator } from '@src/components/NetworkMachineIndicator'
|
import { NetworkMachineIndicator } from '@src/components/NetworkMachineIndicator'
|
||||||
import Tooltip from '@src/components/Tooltip'
|
import Tooltip from '@src/components/Tooltip'
|
||||||
import { useAbsoluteFilePath } from '@src/hooks/useAbsoluteFilePath'
|
import { useAbsoluteFilePath } from '@src/hooks/useAbsoluteFilePath'
|
||||||
import { coreDump } from '@src/lang/wasm'
|
import { openExternalBrowserIfDesktop } from '@src/lib/openWindow'
|
||||||
import type { CoreDumpManager } from '@src/lib/coredump'
|
|
||||||
import openWindow, { openExternalBrowserIfDesktop } from '@src/lib/openWindow'
|
|
||||||
import { PATHS } from '@src/lib/paths'
|
import { PATHS } from '@src/lib/paths'
|
||||||
import { reportRejection } from '@src/lib/trap'
|
|
||||||
import { APP_VERSION, getReleaseUrl } from '@src/routes/utils'
|
import { APP_VERSION, getReleaseUrl } from '@src/routes/utils'
|
||||||
|
|
||||||
export function LowerRightControls({
|
export function LowerRightControls({
|
||||||
children,
|
children,
|
||||||
coreDumpManager,
|
|
||||||
}: {
|
}: {
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
coreDumpManager?: CoreDumpManager
|
|
||||||
}) {
|
}) {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const filePath = useAbsoluteFilePath()
|
const filePath = useAbsoluteFilePath()
|
||||||
@ -28,50 +21,10 @@ export function LowerRightControls({
|
|||||||
const linkOverrideClassName =
|
const linkOverrideClassName =
|
||||||
'!text-chalkboard-70 hover:!text-chalkboard-80 dark:!text-chalkboard-40 dark:hover:!text-chalkboard-30'
|
'!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 (
|
return (
|
||||||
<section className="fixed bottom-2 right-2 flex flex-col items-end gap-3 pointer-events-none">
|
<section className="fixed bottom-2 right-2 flex flex-col items-end gap-3 pointer-events-none">
|
||||||
{children}
|
{children}
|
||||||
<menu className="flex items-center justify-end gap-3 pointer-events-auto">
|
<menu className="flex items-center justify-end gap-3 pointer-events-auto">
|
||||||
{!location.pathname.startsWith(PATHS.HOME) && <ModelStateIndicator />}
|
|
||||||
<a
|
<a
|
||||||
onClick={openExternalBrowserIfDesktop(getReleaseUrl())}
|
onClick={openExternalBrowserIfDesktop(getReleaseUrl())}
|
||||||
href={getReleaseUrl()}
|
href={getReleaseUrl()}
|
||||||
@ -81,20 +34,6 @@ export function LowerRightControls({
|
|||||||
>
|
>
|
||||||
v{APP_VERSION}
|
v{APP_VERSION}
|
||||||
</a>
|
</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
|
<Link
|
||||||
to={
|
to={
|
||||||
location.pathname.includes(PATHS.FILE)
|
location.pathname.includes(PATHS.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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -25,6 +25,10 @@ import { isDesktop } from '@src/lib/isDesktop'
|
|||||||
import { useSettings } from '@src/machines/appMachine'
|
import { useSettings } from '@src/machines/appMachine'
|
||||||
import { commandBarActor } from '@src/machines/commandBarMachine'
|
import { commandBarActor } from '@src/machines/commandBarMachine'
|
||||||
import { onboardingPaths } from '@src/routes/Onboarding/paths'
|
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 {
|
interface ModelingSidebarProps {
|
||||||
paneOpacity: '' | 'opacity-20' | 'opacity-40'
|
paneOpacity: '' | 'opacity-20' | 'opacity-40'
|
||||||
@ -86,18 +90,6 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
|||||||
data: { name: 'load-external-model', groupId: 'code' },
|
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',
|
id: 'export',
|
||||||
title: 'Export part',
|
title: 'Export part',
|
||||||
@ -130,6 +122,17 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
|||||||
return machineManager.noMachinesReason()
|
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(
|
const filteredActions: SidebarAction[] = sidebarActions.filter(
|
||||||
(action) =>
|
(action) =>
|
||||||
@ -340,6 +343,7 @@ function ModelingPaneButton({
|
|||||||
disabledText,
|
disabledText,
|
||||||
...props
|
...props
|
||||||
}: ModelingPaneButtonProps) {
|
}: ModelingPaneButtonProps) {
|
||||||
|
const platform = usePlatform()
|
||||||
useHotkeys(paneConfig.keybinding, onClick, {
|
useHotkeys(paneConfig.keybinding, onClick, {
|
||||||
scopes: ['modeling'],
|
scopes: ['modeling'],
|
||||||
})
|
})
|
||||||
@ -379,7 +383,7 @@ function ModelingPaneButton({
|
|||||||
{paneIsOpen !== undefined ? ` pane` : ''}
|
{paneIsOpen !== undefined ? ` pane` : ''}
|
||||||
</span>
|
</span>
|
||||||
<kbd className="hotkey text-xs capitalize">
|
<kbd className="hotkey text-xs capitalize">
|
||||||
{paneConfig.keybinding}
|
{hotkeyDisplay(paneConfig.keybinding, platform)}
|
||||||
</kbd>
|
</kbd>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</button>
|
</button>
|
||||||
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
41
src/components/ShareButton.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -186,7 +186,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
|
|||||||
) : (
|
) : (
|
||||||
<CustomIcon
|
<CustomIcon
|
||||||
name="person"
|
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>
|
</div>
|
||||||
|
@ -15,6 +15,7 @@ export function useAuthNavigation() {
|
|||||||
|
|
||||||
// Subscribe to the auth state of the app and navigate accordingly.
|
// Subscribe to the auth state of the app and navigate accordingly.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
console.log('authState', authState.value)
|
||||||
if (
|
if (
|
||||||
authState.matches('loggedIn') &&
|
authState.matches('loggedIn') &&
|
||||||
location.pathname.includes(PATHS.SIGN_IN)
|
location.pathname.includes(PATHS.SIGN_IN)
|
||||||
@ -26,5 +27,5 @@ export function useAuthNavigation() {
|
|||||||
) {
|
) {
|
||||||
navigate(PATHS.SIGN_IN)
|
navigate(PATHS.SIGN_IN)
|
||||||
}
|
}
|
||||||
}, [authState])
|
}, [authState, location.pathname])
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import type { Command } from '@src/lib/commandTypes'
|
import type { Command } from '@src/lib/commandTypes'
|
||||||
import { authActor } from '@src/machines/appMachine'
|
import { authActor } from '@src/machines/appMachine'
|
||||||
import { ACTOR_IDS } from '@src/machines/machineConstants'
|
import { ACTOR_IDS } from '@src/machines/machineConstants'
|
||||||
|
import { refreshPage } from '@src/lib/utils'
|
||||||
|
import { reportRejection } from '@src/lib/trap'
|
||||||
|
|
||||||
export const authCommands: Command[] = [
|
export const authCommands: Command[] = [
|
||||||
{
|
{
|
||||||
@ -11,4 +13,14 @@ export const authCommands: Command[] = [
|
|||||||
needsReview: false,
|
needsReview: false,
|
||||||
onSubmit: () => authActor.send({ type: 'Log out' }),
|
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)
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
@ -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
|
// Capitalize letters. We want Ctrl+K, not Ctrl+k, since Shift should be
|
||||||
// shown as a separate modifier.
|
// shown as a separate modifier.
|
||||||
.split('+')
|
.split('+')
|
||||||
|
.map((word) => word.trim().toLocaleLowerCase())
|
||||||
.map((word) => {
|
.map((word) => {
|
||||||
if (word.length === 1 && LOWER_CASE_LETTER.test(word)) {
|
if (word.length === 1 && LOWER_CASE_LETTER.test(word)) {
|
||||||
return word.toUpperCase()
|
return word.toUpperCase()
|
||||||
|
@ -81,12 +81,16 @@ export const getRectangleCallExpressions = (
|
|||||||
createLabeledArg(
|
createLabeledArg(
|
||||||
ARG_END_ABSOLUTE,
|
ARG_END_ABSOLUTE,
|
||||||
createArrayExpression([
|
createArrayExpression([
|
||||||
createCallExpressionStdLib('profileStartX', [
|
createCallExpressionStdLibKw(
|
||||||
|
'profileStartX',
|
||||||
createPipeSubstitution(),
|
createPipeSubstitution(),
|
||||||
]),
|
[]
|
||||||
createCallExpressionStdLib('profileStartY', [
|
),
|
||||||
|
createCallExpressionStdLibKw(
|
||||||
|
'profileStartY',
|
||||||
createPipeSubstitution(),
|
createPipeSubstitution(),
|
||||||
]),
|
[]
|
||||||
|
),
|
||||||
])
|
])
|
||||||
),
|
),
|
||||||
]), // close the rectangle
|
]), // close the rectangle
|
||||||
|
@ -8,6 +8,28 @@ import type { AsyncFn } from '@src/lib/types'
|
|||||||
|
|
||||||
export const uuidv4 = v4
|
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.
|
* Get all labels for a keyword call expression.
|
||||||
*/
|
*/
|
||||||
|
@ -5,7 +5,7 @@ import type { Channel } from '@src/channels'
|
|||||||
// types for knowing what menu sends what webContent payload
|
// types for knowing what menu sends what webContent payload
|
||||||
export type MenuLabels =
|
export type MenuLabels =
|
||||||
| 'Help.Command Palette...'
|
| 'Help.Command Palette...'
|
||||||
| 'Help.Refresh and report a bug'
|
| 'Help.Report a bug'
|
||||||
| 'Help.Reset onboarding'
|
| 'Help.Reset onboarding'
|
||||||
| 'Edit.Rename project'
|
| 'Edit.Rename project'
|
||||||
| 'Edit.Delete project'
|
| 'Edit.Delete project'
|
||||||
|
@ -62,12 +62,14 @@ export const helpRole = (
|
|||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
label: 'Refresh and report a bug',
|
label: 'Report a bug',
|
||||||
id: 'Help.Refresh and report a bug',
|
id: 'Help.Report a bug',
|
||||||
click: () => {
|
click: () => {
|
||||||
typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', {
|
shell
|
||||||
menuLabel: 'Help.Refresh and report a bug',
|
.openExternal(
|
||||||
})
|
'https://github.com/KittyCAD/modeling-app/issues/new?template=bug_report.yml'
|
||||||
|
)
|
||||||
|
.catch(reportRejection)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -39,7 +39,7 @@ type EditRoleLabel =
|
|||||||
| 'Format code'
|
| 'Format code'
|
||||||
|
|
||||||
type HelpRoleLabel =
|
type HelpRoleLabel =
|
||||||
| 'Refresh and report a bug'
|
| 'Report a bug'
|
||||||
| 'Request a feature'
|
| 'Request a feature'
|
||||||
| 'Ask the community discord'
|
| 'Ask the community discord'
|
||||||
| 'Ask the community discourse'
|
| 'Ask the community discourse'
|
||||||
|
@ -76,6 +76,11 @@ const SignIn = () => {
|
|||||||
authActor.send({ type: 'Log in', token })
|
authActor.send({ type: 'Log in', token })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cancelSignIn = async () => {
|
||||||
|
authActor.send({ type: 'Log out' })
|
||||||
|
setUserCode('')
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main
|
<main
|
||||||
className="bg-primary h-screen grid place-items-stretch m-0 p-2"
|
className="bg-primary h-screen grid place-items-stretch m-0 p-2"
|
||||||
@ -128,7 +133,10 @@ const SignIn = () => {
|
|||||||
<p className="text-xs">
|
<p className="text-xs">
|
||||||
You should see the following code in your browser
|
You should see the following code in your browser
|
||||||
</p>
|
</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) => (
|
{userCode.split('').map((char, i) => (
|
||||||
<span
|
<span
|
||||||
key={i}
|
key={i}
|
||||||
@ -141,6 +149,17 @@ const SignIn = () => {
|
|||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</p>
|
</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>
|
</div>
|
||||||
@ -187,7 +206,7 @@ const SignIn = () => {
|
|||||||
'm-0 mt-8 flex gap-4 items-center px-3 py-1 ' +
|
'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'
|
'!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
|
View this sample
|
||||||
<CustomIcon name="arrowRight" className="w-6 h-6" />
|
<CustomIcon name="arrowRight" className="w-6 h-6" />
|
||||||
|