Compare commits
9 Commits
kurt-text-
...
jtran/recu
Author | SHA1 | Date | |
---|---|---|---|
55a6155400 | |||
d41972ee4d | |||
2d6c8cfe32 | |||
37c6730c02 | |||
337f828aa4 | |||
d845e7c38d | |||
7f50294936 | |||
73bbd3f5b7 | |||
295b98c021 |
@ -1,6 +1,6 @@
|
|||||||
import { test, expect, Page } from '@playwright/test'
|
import { test, expect, Page } from '@playwright/test'
|
||||||
|
import * as fsp from 'fs/promises'
|
||||||
import { getUtils, setup, tearDown } from './test-utils'
|
import { getUtils, setup, setupElectron, tearDown } from './test-utils'
|
||||||
import { TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR } from './storageStates'
|
import { TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR } from './storageStates'
|
||||||
import { bracket } from 'lib/exampleKcl'
|
import { bracket } from 'lib/exampleKcl'
|
||||||
|
|
||||||
@ -415,6 +415,52 @@ const sketch001 = startSketchAt([-0, -0])
|
|||||||
await expect(successToastMessage).toBeVisible()
|
await expect(successToastMessage).toBeVisible()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test(
|
||||||
|
`Network health indicator only appears in modeling view`,
|
||||||
|
{ tag: '@electron' },
|
||||||
|
async ({ browserName: _ }, testInfo) => {
|
||||||
|
const { electronApp, page } = await setupElectron({
|
||||||
|
testInfo,
|
||||||
|
folderSetupFn: async (dir) => {
|
||||||
|
await fsp.mkdir(`${dir}/bracket`, { recursive: true })
|
||||||
|
await fsp.copyFile(
|
||||||
|
'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl',
|
||||||
|
`${dir}/bracket/main.kcl`
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
const u = await getUtils(page)
|
||||||
|
|
||||||
|
// Locators
|
||||||
|
const projectsHeading = page.getByRole('heading', {
|
||||||
|
name: 'Your projects',
|
||||||
|
})
|
||||||
|
const projectLink = page.getByRole('link', { name: 'bracket' })
|
||||||
|
const networkHealthIndicator = page.getByTestId('network-toggle')
|
||||||
|
|
||||||
|
await test.step('Check the home page', async () => {
|
||||||
|
await expect(projectsHeading).toBeVisible()
|
||||||
|
await expect(projectLink).toBeVisible()
|
||||||
|
await expect(networkHealthIndicator).not.toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Open the project', async () => {
|
||||||
|
await projectLink.click()
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Check the modeling view', async () => {
|
||||||
|
await expect(networkHealthIndicator).toBeVisible()
|
||||||
|
await expect(networkHealthIndicator).toContainText('Problem')
|
||||||
|
await u.waitForPageLoad()
|
||||||
|
await expect(networkHealthIndicator).toContainText('Connected')
|
||||||
|
})
|
||||||
|
|
||||||
|
await electronApp.close()
|
||||||
|
}
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
async function clickExportButton(page: Page) {
|
async function clickExportButton(page: Page) {
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
@ -1,9 +1,10 @@
|
|||||||
import { test, expect } from '@playwright/test'
|
import { test, expect } from '@playwright/test'
|
||||||
|
import * as fsp from 'fs/promises'
|
||||||
import { getUtils, setup, tearDown } from './test-utils'
|
import { getUtils, setup, setupElectron, tearDown } from './test-utils'
|
||||||
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
|
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
|
||||||
import { TEST_SETTINGS_KEY, TEST_SETTINGS_CORRUPTED } from './storageStates'
|
import { TEST_SETTINGS_KEY, TEST_SETTINGS_CORRUPTED } from './storageStates'
|
||||||
import * as TOML from '@iarna/toml'
|
import * as TOML from '@iarna/toml'
|
||||||
|
import { APP_NAME } from 'lib/constants'
|
||||||
|
|
||||||
test.beforeEach(async ({ context, page }) => {
|
test.beforeEach(async ({ context, page }) => {
|
||||||
await setup(context, page)
|
await setup(context, page)
|
||||||
@ -187,4 +188,74 @@ test.describe('Testing settings', () => {
|
|||||||
await expect(themeColorSetting).toHaveValue(settingValues.default)
|
await expect(themeColorSetting).toHaveValue(settingValues.default)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test(
|
||||||
|
`Project settings override user settings on desktop`,
|
||||||
|
{ tag: '@electron' },
|
||||||
|
async ({ browser: _ }, testInfo) => {
|
||||||
|
const { electronApp, page } = await setupElectron({
|
||||||
|
testInfo,
|
||||||
|
folderSetupFn: async (dir) => {
|
||||||
|
await fsp.mkdir(`${dir}/bracket`, { recursive: true })
|
||||||
|
await fsp.copyFile(
|
||||||
|
'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl',
|
||||||
|
`${dir}/bracket/main.kcl`
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
const u = await getUtils(page)
|
||||||
|
|
||||||
|
page.on('console', console.log)
|
||||||
|
|
||||||
|
// Selectors and constants
|
||||||
|
const userThemeColor = '120'
|
||||||
|
const projectThemeColor = '50'
|
||||||
|
const settingsOpenButton = page.getByRole('link', {
|
||||||
|
name: 'settings Settings',
|
||||||
|
})
|
||||||
|
const themeColorSetting = page.locator('#themeColor').getByRole('slider')
|
||||||
|
const projectSettingsTab = page.getByRole('radio', { name: 'Project' })
|
||||||
|
const userSettingsTab = page.getByRole('radio', { name: 'User' })
|
||||||
|
const settingsCloseButton = page.getByTestId('settings-close-button')
|
||||||
|
const projectLink = page.getByText('bracket')
|
||||||
|
const logoLink = page.getByTestId('app-logo')
|
||||||
|
|
||||||
|
// Open the app and set the user theme color
|
||||||
|
await test.step('Set user theme color on home', async () => {
|
||||||
|
await expect(settingsOpenButton).toBeVisible()
|
||||||
|
await settingsOpenButton.click()
|
||||||
|
// The user tab should be selected by default on home
|
||||||
|
await expect(userSettingsTab).toBeChecked()
|
||||||
|
await themeColorSetting.fill(userThemeColor)
|
||||||
|
await expect(logoLink).toHaveCSS('--primary-hue', userThemeColor)
|
||||||
|
await settingsCloseButton.click()
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Set project theme color', async () => {
|
||||||
|
// Open the project
|
||||||
|
await projectLink.click()
|
||||||
|
await settingsOpenButton.click()
|
||||||
|
// The project tab should be selected by default within a project
|
||||||
|
await expect(projectSettingsTab).toBeChecked()
|
||||||
|
await themeColorSetting.fill(projectThemeColor)
|
||||||
|
await expect(logoLink).toHaveCSS('--primary-hue', projectThemeColor)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Refresh the application and see project setting applied', async () => {
|
||||||
|
await page.reload()
|
||||||
|
|
||||||
|
await expect(logoLink).toHaveCSS('--primary-hue', projectThemeColor)
|
||||||
|
await settingsCloseButton.click()
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step(`Navigate back to the home view and see user setting applied`, async () => {
|
||||||
|
await logoLink.click()
|
||||||
|
await expect(logoLink).toHaveCSS('--primary-hue', userThemeColor)
|
||||||
|
})
|
||||||
|
|
||||||
|
await electronApp.close()
|
||||||
|
}
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
@ -31,15 +31,13 @@ test.describe('Text-to-CAD tests', () => {
|
|||||||
)
|
)
|
||||||
await expect(submittingToastMessage).toBeVisible()
|
await expect(submittingToastMessage).toBeVisible()
|
||||||
|
|
||||||
await page.waitForTimeout(5000)
|
|
||||||
|
|
||||||
const generatingToastMessage = page.getByText(
|
const generatingToastMessage = page.getByText(
|
||||||
`Generating parametric model...`
|
`Generating parametric model...`
|
||||||
)
|
)
|
||||||
await expect(generatingToastMessage).toBeVisible()
|
await expect(generatingToastMessage).toBeVisible({ timeout: 10000 })
|
||||||
|
|
||||||
const successToastMessage = page.getByText(`Text-to-CAD successful`)
|
const successToastMessage = page.getByText(`Text-to-CAD successful`)
|
||||||
await expect(successToastMessage).toBeVisible()
|
await expect(successToastMessage).toBeVisible({ timeout: 15000 })
|
||||||
|
|
||||||
await expect(page.getByText('Copied')).not.toBeVisible()
|
await expect(page.getByText('Copied')).not.toBeVisible()
|
||||||
|
|
||||||
@ -101,15 +99,13 @@ test.describe('Text-to-CAD tests', () => {
|
|||||||
)
|
)
|
||||||
await expect(submittingToastMessage).toBeVisible()
|
await expect(submittingToastMessage).toBeVisible()
|
||||||
|
|
||||||
await page.waitForTimeout(5000)
|
|
||||||
|
|
||||||
const generatingToastMessage = page.getByText(
|
const generatingToastMessage = page.getByText(
|
||||||
`Generating parametric model...`
|
`Generating parametric model...`
|
||||||
)
|
)
|
||||||
await expect(generatingToastMessage).toBeVisible()
|
await expect(generatingToastMessage).toBeVisible({ timeout: 10000 })
|
||||||
|
|
||||||
const successToastMessage = page.getByText(`Text-to-CAD successful`)
|
const successToastMessage = page.getByText(`Text-to-CAD successful`)
|
||||||
await expect(successToastMessage).toBeVisible()
|
await expect(successToastMessage).toBeVisible({ timeout: 15000 })
|
||||||
|
|
||||||
await expect(page.getByText('Copied')).not.toBeVisible()
|
await expect(page.getByText('Copied')).not.toBeVisible()
|
||||||
|
|
||||||
@ -121,13 +117,12 @@ test.describe('Text-to-CAD tests', () => {
|
|||||||
// Find the toast.
|
// Find the toast.
|
||||||
// Look out for the toast message
|
// Look out for the toast message
|
||||||
await expect(submittingToastMessage).toBeVisible()
|
await expect(submittingToastMessage).toBeVisible()
|
||||||
|
await expect(generatingToastMessage).toBeVisible({ timeout: 10000 })
|
||||||
await page.waitForTimeout(5000)
|
|
||||||
|
|
||||||
await expect(generatingToastMessage).toBeVisible()
|
|
||||||
|
|
||||||
// Expect 2 success toasts.
|
// Expect 2 success toasts.
|
||||||
await expect(successToastMessage).toHaveCount(2)
|
await expect(successToastMessage).toHaveCount(2, {
|
||||||
|
timeout: 15000,
|
||||||
|
})
|
||||||
await expect(page.getByText('a 2x4 lego')).toBeVisible()
|
await expect(page.getByText('a 2x4 lego')).toBeVisible()
|
||||||
await expect(page.getByText('a 2x6 lego')).toBeVisible()
|
await expect(page.getByText('a 2x6 lego')).toBeVisible()
|
||||||
})
|
})
|
||||||
@ -150,15 +145,13 @@ test.describe('Text-to-CAD tests', () => {
|
|||||||
)
|
)
|
||||||
await expect(submittingToastMessage).toBeVisible()
|
await expect(submittingToastMessage).toBeVisible()
|
||||||
|
|
||||||
await page.waitForTimeout(5000)
|
|
||||||
|
|
||||||
const generatingToastMessage = page.getByText(
|
const generatingToastMessage = page.getByText(
|
||||||
`Generating parametric model...`
|
`Generating parametric model...`
|
||||||
)
|
)
|
||||||
await expect(generatingToastMessage).toBeVisible()
|
await expect(generatingToastMessage).toBeVisible({ timeout: 10000 })
|
||||||
|
|
||||||
const successToastMessage = page.getByText(`Text-to-CAD successful`)
|
const successToastMessage = page.getByText(`Text-to-CAD successful`)
|
||||||
await expect(successToastMessage).toBeVisible()
|
await expect(successToastMessage).toBeVisible({ timeout: 15000 })
|
||||||
|
|
||||||
// Hit copy to clipboard.
|
// Hit copy to clipboard.
|
||||||
const rejectButton = page.getByRole('button', { name: 'Reject' })
|
const rejectButton = page.getByRole('button', { name: 'Reject' })
|
||||||
@ -319,11 +312,9 @@ test.describe('Text-to-CAD tests', () => {
|
|||||||
// Look out for the toast message
|
// Look out for the toast message
|
||||||
await expect(submittingToastMessage).toBeVisible()
|
await expect(submittingToastMessage).toBeVisible()
|
||||||
|
|
||||||
await page.waitForTimeout(5000)
|
await expect(generatingToastMessage).toBeVisible({ timeout: 10000 })
|
||||||
|
|
||||||
await expect(generatingToastMessage).toBeVisible()
|
await expect(successToastMessage).toBeVisible({ timeout: 15000 })
|
||||||
|
|
||||||
await expect(successToastMessage).toBeVisible()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('sending a bad prompt fails, can ignore toast, can start over from command bar', async ({
|
test('sending a bad prompt fails, can ignore toast, can start over from command bar', async ({
|
||||||
@ -353,7 +344,7 @@ test.describe('Text-to-CAD tests', () => {
|
|||||||
const prompt = page.getByText('Prompt')
|
const prompt = page.getByText('Prompt')
|
||||||
await expect(prompt.first()).toBeVisible()
|
await expect(prompt.first()).toBeVisible()
|
||||||
|
|
||||||
const badPrompt = 'akjsndladf lajbhflauweyfa;wieufjn---4;'
|
const badPrompt = 'akjsndladflajbhflauweyf15;'
|
||||||
|
|
||||||
// Type the prompt.
|
// Type the prompt.
|
||||||
await page.keyboard.type(badPrompt)
|
await page.keyboard.type(badPrompt)
|
||||||
@ -391,11 +382,9 @@ test.describe('Text-to-CAD tests', () => {
|
|||||||
// Look out for the toast message
|
// Look out for the toast message
|
||||||
await expect(submittingToastMessage).toBeVisible()
|
await expect(submittingToastMessage).toBeVisible()
|
||||||
|
|
||||||
await page.waitForTimeout(5000)
|
await expect(generatingToastMessage).toBeVisible({ timeout: 10000 })
|
||||||
|
|
||||||
await expect(generatingToastMessage).toBeVisible()
|
await expect(successToastMessage).toBeVisible({ timeout: 15000 })
|
||||||
|
|
||||||
await expect(successToastMessage).toBeVisible()
|
|
||||||
|
|
||||||
await expect(page.getByText('Copied')).not.toBeVisible()
|
await expect(page.getByText('Copied')).not.toBeVisible()
|
||||||
|
|
||||||
@ -448,16 +437,13 @@ test.describe('Text-to-CAD tests', () => {
|
|||||||
)
|
)
|
||||||
await expect(submittingToastMessage).toBeVisible()
|
await expect(submittingToastMessage).toBeVisible()
|
||||||
|
|
||||||
await page.waitForTimeout(1000)
|
|
||||||
|
|
||||||
const generatingToastMessage = page.getByText(
|
const generatingToastMessage = page.getByText(
|
||||||
`Generating parametric model...`
|
`Generating parametric model...`
|
||||||
)
|
)
|
||||||
await expect(generatingToastMessage).toBeVisible()
|
await expect(generatingToastMessage).toBeVisible({ timeout: 10000 })
|
||||||
await page.waitForTimeout(5000)
|
|
||||||
|
|
||||||
const successToastMessage = page.getByText(`Text-to-CAD successful`)
|
const successToastMessage = page.getByText(`Text-to-CAD successful`)
|
||||||
await expect(successToastMessage).toBeVisible()
|
await expect(successToastMessage).toBeVisible({ timeout: 15000 })
|
||||||
|
|
||||||
await expect(page.getByText(promptWithNewline)).toBeVisible()
|
await expect(page.getByText(promptWithNewline)).toBeVisible()
|
||||||
})
|
})
|
||||||
@ -465,6 +451,8 @@ test.describe('Text-to-CAD tests', () => {
|
|||||||
test('can do many at once and get many prompts back, and interact with many', async ({
|
test('can do many at once and get many prompts back, and interact with many', async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
|
// Let this test run longer since we've seen it timeout.
|
||||||
|
test.setTimeout(180_000)
|
||||||
// skip on windows
|
// skip on windows
|
||||||
test.skip(
|
test.skip(
|
||||||
process.platform === 'win32',
|
process.platform === 'win32',
|
||||||
@ -493,11 +481,13 @@ test.describe('Text-to-CAD tests', () => {
|
|||||||
const generatingToastMessage = page.getByText(
|
const generatingToastMessage = page.getByText(
|
||||||
`Generating parametric model...`
|
`Generating parametric model...`
|
||||||
)
|
)
|
||||||
await expect(generatingToastMessage.first()).toBeVisible({ timeout: 10000 })
|
await expect(generatingToastMessage.first()).toBeVisible({
|
||||||
|
timeout: 10_000,
|
||||||
|
})
|
||||||
|
|
||||||
const successToastMessage = page.getByText(`Text-to-CAD successful`)
|
const successToastMessage = page.getByText(`Text-to-CAD successful`)
|
||||||
// We should have three success toasts.
|
// We should have three success toasts.
|
||||||
await expect(successToastMessage).toHaveCount(3, { timeout: 15000 })
|
await expect(successToastMessage).toHaveCount(3, { timeout: 25_000 })
|
||||||
|
|
||||||
await expect(page.getByText('Copied')).not.toBeVisible()
|
await expect(page.getByText('Copied')).not.toBeVisible()
|
||||||
|
|
||||||
@ -539,12 +529,7 @@ test.describe('Text-to-CAD tests', () => {
|
|||||||
await expect(page.locator('.cm-content')).toContainText(`2x8`)
|
await expect(page.locator('.cm-content')).toContainText(`2x8`)
|
||||||
|
|
||||||
// Find the toast close button.
|
// Find the toast close button.
|
||||||
const closeButton = page
|
const closeButton = page.locator('[data-negative-button="close"]').first()
|
||||||
.getByRole('status')
|
|
||||||
.locator('div')
|
|
||||||
.filter({ hasText: 'Text-to-CAD successfulPrompt' })
|
|
||||||
.first()
|
|
||||||
.getByRole('button', { name: 'Close' })
|
|
||||||
await expect(closeButton).toBeVisible()
|
await expect(closeButton).toBeVisible()
|
||||||
await closeButton.click()
|
await closeButton.click()
|
||||||
|
|
||||||
@ -697,7 +682,7 @@ async function sendPromptFromCommandBar(page: Page, promptStr: string) {
|
|||||||
|
|
||||||
// Type the prompt.
|
// Type the prompt.
|
||||||
await page.keyboard.type(promptStr)
|
await page.keyboard.type(promptStr)
|
||||||
await page.waitForTimeout(1000)
|
await page.waitForTimeout(200)
|
||||||
await page.keyboard.press('Enter')
|
await page.keyboard.press('Enter')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@ export const FileMachineProvider = ({
|
|||||||
if (event.data && 'name' in event.data) {
|
if (event.data && 'name' in event.data) {
|
||||||
commandBarSend({ type: 'Close' })
|
commandBarSend({ type: 'Close' })
|
||||||
navigate(
|
navigate(
|
||||||
`${PATHS.FILE}/${encodeURIComponent(
|
`..${PATHS.FILE}/${encodeURIComponent(
|
||||||
context.selectedDirectory +
|
context.selectedDirectory +
|
||||||
window.electron.path.sep +
|
window.electron.path.sep +
|
||||||
event.data.name
|
event.data.name
|
||||||
@ -61,7 +61,7 @@ export const FileMachineProvider = ({
|
|||||||
event.data.path.endsWith(FILE_EXT)
|
event.data.path.endsWith(FILE_EXT)
|
||||||
) {
|
) {
|
||||||
// Don't navigate to newly created directories
|
// Don't navigate to newly created directories
|
||||||
navigate(`${PATHS.FILE}/${encodeURIComponent(event.data.path)}`)
|
navigate(`..${PATHS.FILE}/${encodeURIComponent(event.data.path)}`)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
addFileToRenamingQueue: assign({
|
addFileToRenamingQueue: assign({
|
||||||
@ -100,7 +100,7 @@ export const FileMachineProvider = ({
|
|||||||
let createdPath: string
|
let createdPath: string
|
||||||
|
|
||||||
if (event.data.makeDir) {
|
if (event.data.makeDir) {
|
||||||
let { name, path } = await getNextDirName({
|
let { name, path } = getNextDirName({
|
||||||
entryName: createdName,
|
entryName: createdName,
|
||||||
baseDir: context.selectedDirectory.path,
|
baseDir: context.selectedDirectory.path,
|
||||||
})
|
})
|
||||||
@ -108,16 +108,13 @@ export const FileMachineProvider = ({
|
|||||||
createdPath = path
|
createdPath = path
|
||||||
await window.electron.mkdir(createdPath)
|
await window.electron.mkdir(createdPath)
|
||||||
} else {
|
} else {
|
||||||
const { name, path } = await getNextFileName({
|
const { name, path } = getNextFileName({
|
||||||
entryName: createdName,
|
entryName: createdName,
|
||||||
baseDir: context.selectedDirectory.path,
|
baseDir: context.selectedDirectory.path,
|
||||||
})
|
})
|
||||||
createdName = name
|
createdName = name
|
||||||
createdPath = path
|
createdPath = path
|
||||||
await window.electron.mkdir(createdPath)
|
await window.electron.writeFile(createdPath, event.data.content ?? '')
|
||||||
if (event.data.content) {
|
|
||||||
await window.electron.writeFile(createdPath, event.data.content)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -130,7 +127,7 @@ export const FileMachineProvider = ({
|
|||||||
let createdPath: string
|
let createdPath: string
|
||||||
|
|
||||||
if (event.data.makeDir) {
|
if (event.data.makeDir) {
|
||||||
let { name, path } = await getNextDirName({
|
let { name, path } = getNextDirName({
|
||||||
entryName: createdName,
|
entryName: createdName,
|
||||||
baseDir: context.selectedDirectory.path,
|
baseDir: context.selectedDirectory.path,
|
||||||
})
|
})
|
||||||
@ -138,16 +135,13 @@ export const FileMachineProvider = ({
|
|||||||
createdPath = path
|
createdPath = path
|
||||||
await window.electron.mkdir(createdPath)
|
await window.electron.mkdir(createdPath)
|
||||||
} else {
|
} else {
|
||||||
const { name, path } = await getNextFileName({
|
const { name, path } = getNextFileName({
|
||||||
entryName: createdName,
|
entryName: createdName,
|
||||||
baseDir: context.selectedDirectory.path,
|
baseDir: context.selectedDirectory.path,
|
||||||
})
|
})
|
||||||
createdName = name
|
createdName = name
|
||||||
createdPath = path
|
createdPath = path
|
||||||
await window.electron.mkdir(createdPath)
|
await window.electron.writeFile(createdPath, event.data.content ?? '')
|
||||||
if (event.data.content) {
|
|
||||||
await window.electron.writeFile(createdPath, '')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -180,13 +174,13 @@ export const FileMachineProvider = ({
|
|||||||
const currentFilePath = window.electron.path.join(file.path, file.name)
|
const currentFilePath = window.electron.path.join(file.path, file.name)
|
||||||
if (oldPath === currentFilePath && project?.path) {
|
if (oldPath === currentFilePath && project?.path) {
|
||||||
// If we just renamed the current file, navigate to the new path
|
// If we just renamed the current file, navigate to the new path
|
||||||
navigate(PATHS.FILE + '/' + encodeURIComponent(newPath))
|
navigate(`..${PATHS.FILE}/${encodeURIComponent(newPath)}`)
|
||||||
} else if (file?.path.includes(oldPath)) {
|
} else if (file?.path.includes(oldPath)) {
|
||||||
// If we just renamed a directory that the current file is in, navigate to the new path
|
// If we just renamed a directory that the current file is in, navigate to the new path
|
||||||
navigate(
|
navigate(
|
||||||
PATHS.FILE +
|
`..${PATHS.FILE}/${encodeURIComponent(
|
||||||
'/' +
|
file.path.replace(oldPath, newDirPath)
|
||||||
encodeURIComponent(file.path.replace(oldPath, newDirPath))
|
)}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -221,7 +215,7 @@ export const FileMachineProvider = ({
|
|||||||
file?.path.includes(event.data.path)) &&
|
file?.path.includes(event.data.path)) &&
|
||||||
project?.path
|
project?.path
|
||||||
) {
|
) {
|
||||||
navigate(PATHS.FILE + '/' + encodeURIComponent(project.path))
|
navigate(`../${PATHS.FILE}/${encodeURIComponent(project.path)}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return `Successfully deleted ${isDir ? 'folder' : 'file'} "${
|
return `Successfully deleted ${isDir ? 'folder' : 'file'} "${
|
||||||
|
@ -107,7 +107,9 @@ export function LowerRightControls({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Link>
|
</Link>
|
||||||
<NetworkMachineIndicator className={linkOverrideClassName} />
|
<NetworkMachineIndicator className={linkOverrideClassName} />
|
||||||
|
{!location.pathname.startsWith(PATHS.HOME) && (
|
||||||
<NetworkHealthIndicator />
|
<NetworkHealthIndicator />
|
||||||
|
)}
|
||||||
<HelpMenu />
|
<HelpMenu />
|
||||||
</menu>
|
</menu>
|
||||||
</section>
|
</section>
|
||||||
|
@ -5,7 +5,7 @@ import { isDesktop } from 'lib/isDesktop'
|
|||||||
import { PATHS } from 'lib/paths'
|
import { PATHS } from 'lib/paths'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { TextToCad_type } from '@kittycad/lib/dist/types/src/models'
|
import { TextToCad_type } from '@kittycad/lib/dist/types/src/models'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
Box3,
|
Box3,
|
||||||
Color,
|
Color,
|
||||||
@ -121,10 +121,40 @@ export function ToastTextToCadSuccess({
|
|||||||
}) {
|
}) {
|
||||||
const wrapperRef = useRef<HTMLDivElement | null>(null)
|
const wrapperRef = useRef<HTMLDivElement | null>(null)
|
||||||
const canvasRef = useRef<HTMLCanvasElement | null>(null)
|
const canvasRef = useRef<HTMLCanvasElement | null>(null)
|
||||||
|
const animationRequestRef = useRef<number>()
|
||||||
const [hasCopied, setHasCopied] = useState(false)
|
const [hasCopied, setHasCopied] = useState(false)
|
||||||
const [showCopiedUi, setShowCopiedUi] = useState(false)
|
const [showCopiedUi, setShowCopiedUi] = useState(false)
|
||||||
const modelId = data.id
|
const modelId = data.id
|
||||||
|
|
||||||
|
const animate = useCallback(
|
||||||
|
({
|
||||||
|
renderer,
|
||||||
|
scene,
|
||||||
|
camera,
|
||||||
|
controls,
|
||||||
|
isFirstRender = false,
|
||||||
|
}: {
|
||||||
|
renderer: WebGLRenderer
|
||||||
|
scene: Scene
|
||||||
|
camera: OrthographicCamera
|
||||||
|
controls: OrbitControls
|
||||||
|
isFirstRender?: boolean
|
||||||
|
}) => {
|
||||||
|
if (
|
||||||
|
!wrapperRef.current ||
|
||||||
|
!(isFirstRender || animationRequestRef.current)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
animationRequestRef.current = requestAnimationFrame(() =>
|
||||||
|
animate({ renderer, scene, camera, controls })
|
||||||
|
)
|
||||||
|
// required if controls.enableDamping or controls.autoRotate are set to true
|
||||||
|
controls.update()
|
||||||
|
renderer.render(scene, camera)
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!canvasRef.current) return
|
if (!canvasRef.current) return
|
||||||
|
|
||||||
@ -132,7 +162,6 @@ export function ToastTextToCadSuccess({
|
|||||||
const renderer = new WebGLRenderer({ canvas, antialias: true, alpha: true })
|
const renderer = new WebGLRenderer({ canvas, antialias: true, alpha: true })
|
||||||
renderer.setSize(CANVAS_SIZE, CANVAS_SIZE)
|
renderer.setSize(CANVAS_SIZE, CANVAS_SIZE)
|
||||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
|
||||||
renderer.setAnimationLoop(animate)
|
|
||||||
|
|
||||||
const scene = new Scene()
|
const scene = new Scene()
|
||||||
const ambientLight = new DirectionalLight(new Color('white'), 8.0)
|
const ambientLight = new DirectionalLight(new Color('white'), 8.0)
|
||||||
@ -155,13 +184,6 @@ export function ToastTextToCadSuccess({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
function animate() {
|
|
||||||
requestAnimationFrame(animate)
|
|
||||||
// required if controls.enableDamping or controls.autoRotate are set to true
|
|
||||||
controls.update()
|
|
||||||
renderer.render(scene, camera)
|
|
||||||
}
|
|
||||||
|
|
||||||
loader.parse(
|
loader.parse(
|
||||||
buffer,
|
buffer,
|
||||||
'',
|
'',
|
||||||
@ -212,6 +234,8 @@ export function ToastTextToCadSuccess({
|
|||||||
|
|
||||||
camera.updateProjectionMatrix()
|
camera.updateProjectionMatrix()
|
||||||
controls.update()
|
controls.update()
|
||||||
|
// render the scene once...
|
||||||
|
renderer.render(scene, camera)
|
||||||
},
|
},
|
||||||
// called when loading has errors
|
// called when loading has errors
|
||||||
function (error) {
|
function (error) {
|
||||||
@ -221,8 +245,26 @@ export function ToastTextToCadSuccess({
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ...and set a mouseover listener on the canvas to enable the orbit controls
|
||||||
|
canvasRef.current.addEventListener('mouseover', () => {
|
||||||
|
renderer.setAnimationLoop(() =>
|
||||||
|
animate({ renderer, scene, camera, controls, isFirstRender: true })
|
||||||
|
)
|
||||||
|
})
|
||||||
|
canvasRef.current.addEventListener('mouseout', () => {
|
||||||
|
renderer.setAnimationLoop(null)
|
||||||
|
if (animationRequestRef.current) {
|
||||||
|
cancelAnimationFrame(animationRequestRef.current)
|
||||||
|
animationRequestRef.current = undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
renderer.dispose()
|
renderer.dispose()
|
||||||
|
if (animationRequestRef.current) {
|
||||||
|
cancelAnimationFrame(animationRequestRef.current)
|
||||||
|
animationRequestRef.current = undefined
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@ -251,6 +293,7 @@ export function ToastTextToCadSuccess({
|
|||||||
iconStart={{
|
iconStart={{
|
||||||
icon: 'close',
|
icon: 'close',
|
||||||
}}
|
}}
|
||||||
|
data-negative-button={hasCopied ? 'close' : 'reject'}
|
||||||
name={hasCopied ? 'Close' : 'Reject'}
|
name={hasCopied ? 'Close' : 'Reject'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!hasCopied) {
|
if (!hasCopied) {
|
||||||
|
@ -112,7 +112,7 @@ const Home = () => {
|
|||||||
).trim()
|
).trim()
|
||||||
|
|
||||||
if (doesProjectNameNeedInterpolated(name)) {
|
if (doesProjectNameNeedInterpolated(name)) {
|
||||||
const nextIndex = await getNextProjectIndex(name, projects)
|
const nextIndex = getNextProjectIndex(name, projects)
|
||||||
name = interpolateProjectNameWithIndex(name, nextIndex)
|
name = interpolateProjectNameWithIndex(name, nextIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
7
src/wasm-lib/Cargo.lock
generated
7
src/wasm-lib/Cargo.lock
generated
@ -1428,6 +1428,7 @@ dependencies = [
|
|||||||
"parse-display",
|
"parse-display",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"pyo3",
|
"pyo3",
|
||||||
|
"recursion",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"ropey",
|
"ropey",
|
||||||
"schemars",
|
"schemars",
|
||||||
@ -2148,6 +2149,12 @@ dependencies = [
|
|||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "recursion"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9f705426858ccd7bbfe19798239d6b6bfd9bf96bde0624a84b92694046e98871"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.2.16"
|
version = "0.2.16"
|
||||||
|
@ -31,6 +31,7 @@ lazy_static = "1.5.0"
|
|||||||
mime_guess = "2.0.5"
|
mime_guess = "2.0.5"
|
||||||
parse-display = "0.9.1"
|
parse-display = "0.9.1"
|
||||||
pyo3 = { version = "0.22.2", optional = true }
|
pyo3 = { version = "0.22.2", optional = true }
|
||||||
|
recursion = "0.5.2"
|
||||||
reqwest = { version = "0.11.26", default-features = false, features = ["stream", "rustls-tls"] }
|
reqwest = { version = "0.11.26", default-features = false, features = ["stream", "rustls-tls"] }
|
||||||
ropey = "1.6.1"
|
ropey = "1.6.1"
|
||||||
schemars = { version = "0.8.17", features = ["impl_json_schema", "url", "uuid1"] }
|
schemars = { version = "0.8.17", features = ["impl_json_schema", "url", "uuid1"] }
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
pub mod modify;
|
pub mod modify;
|
||||||
|
mod recursion;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
|
167
src/wasm-lib/kcl/src/ast/recursion.rs
Normal file
167
src/wasm-lib/kcl/src/ast/recursion.rs
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
use recursion::{Collapsible, MappableFrame, PartiallyApplied};
|
||||||
|
|
||||||
|
use super::types::{
|
||||||
|
BinaryExpression, BinaryOperator, BinaryPart, Digest, Expr, FnArgType, Identifier, KclNone, Literal, Parameter,
|
||||||
|
PipeSubstitution, TagDeclarator,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub enum ExprFrame<A> {
|
||||||
|
Literal(Box<Literal>),
|
||||||
|
Identifier(Box<Identifier>),
|
||||||
|
TagDeclarator(Box<TagDeclarator>),
|
||||||
|
BinaryExpression(Box<BinaryExpressionFrame<A>>),
|
||||||
|
FunctionExpression(Box<FunctionExpressionFrame<A>>),
|
||||||
|
CallExpression(Box<CallExpressionFrame<A>>),
|
||||||
|
PipeExpression(Box<PipeExpressionFrame<A>>),
|
||||||
|
PipeSubstitution(Box<PipeSubstitution>),
|
||||||
|
ArrayExpression(Box<ArrayExpressionFrame<A>>),
|
||||||
|
ObjectExpression(Box<ObjectExpressionFrame<A>>),
|
||||||
|
MemberExpression(Box<MemberExpressionFrame<A>>),
|
||||||
|
UnaryExpression(Box<UnaryExpressionFrame<A>>),
|
||||||
|
None(KclNone),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MappableFrame for ExprFrame<PartiallyApplied> {
|
||||||
|
type Frame<X> = ExprFrame<X>;
|
||||||
|
|
||||||
|
fn map_frame<A, B>(input: Self::Frame<A>, mut f: impl FnMut(A) -> B) -> Self::Frame<B> {
|
||||||
|
match input {
|
||||||
|
ExprFrame::Literal(x) => ExprFrame::Literal(x),
|
||||||
|
ExprFrame::Identifier(x) => ExprFrame::Identifier(x),
|
||||||
|
ExprFrame::TagDeclarator(x) => ExprFrame::TagDeclarator(x),
|
||||||
|
ExprFrame::BinaryExpression(x) => ExprFrame::BinaryExpression(MappableFrame::map_frame(x, &mut f)),
|
||||||
|
ExprFrame::FunctionExpression(x) => ExprFrame::FunctionExpression(MappableFrame::map_frame(x, &mut f)),
|
||||||
|
ExprFrame::CallExpression(x) => ExprFrame::CallExpression(MappableFrame::map_frame(x, &mut f)),
|
||||||
|
ExprFrame::PipeExpression(x) => ExprFrame::PipeExpression(MappableFrame::map_frame(x, &mut f)),
|
||||||
|
ExprFrame::PipeSubstitution(x) => ExprFrame::PipeSubstitution(x),
|
||||||
|
ExprFrame::ArrayExpression(x) => ExprFrame::ArrayExpression(MappableFrame::map_frame(x, &mut f)),
|
||||||
|
ExprFrame::ObjectExpression(x) => ExprFrame::ObjectExpression(MappableFrame::map_frame(x, &mut f)),
|
||||||
|
ExprFrame::MemberExpression(x) => ExprFrame::MemberExpression(MappableFrame::map_frame(x, &mut f)),
|
||||||
|
ExprFrame::UnaryExpression(x) => ExprFrame::UnaryExpression(MappableFrame::map_frame(x, &mut f)),
|
||||||
|
ExprFrame::None(x) => ExprFrame::None(x),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Collapsible for &'a Expr {
|
||||||
|
type FrameToken = ExprFrame<PartiallyApplied>;
|
||||||
|
|
||||||
|
fn into_frame(self) -> <Self::FrameToken as MappableFrame>::Frame<Self> {
|
||||||
|
match self {
|
||||||
|
Expr::Literal(x) => ExprFrame::Literal(x.clone()),
|
||||||
|
Expr::Identifier(x) => ExprFrame::Identifier(x.clone()),
|
||||||
|
Expr::TagDeclarator(x) => ExprFrame::TagDeclarator(x.clone()),
|
||||||
|
Expr::BinaryExpression(x) => ExprFrame::BinaryExpression(Box::new(x.into_frame())),
|
||||||
|
Expr::FunctionExpression(x) => ExprFrame::FunctionExpression(Box::new(x.into_frame())),
|
||||||
|
Expr::CallExpression(x) => ExprFrame::CallExpression(Box::new(x.into_frame())),
|
||||||
|
Expr::PipeExpression(x) => ExprFrame::PipeExpression(Box::new(x.into_frame())),
|
||||||
|
Expr::PipeSubstitution(x) => ExprFrame::PipeSubstitution(x.clone()),
|
||||||
|
Expr::ArrayExpression(x) => ExprFrame::ArrayExpression(Box::new(x.into_frame())),
|
||||||
|
Expr::ObjectExpression(x) => ExprFrame::ObjectExpression(Box::new(x.into_frame())),
|
||||||
|
Expr::MemberExpression(x) => ExprFrame::MemberExpression(Box::new(x.into_frame())),
|
||||||
|
Expr::UnaryExpression(x) => ExprFrame::UnaryExpression(Box::new(x.into_frame())),
|
||||||
|
Expr::None(x) => ExprFrame::None(x.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BinaryExpressionFrame<A> {
|
||||||
|
pub start: usize,
|
||||||
|
pub end: usize,
|
||||||
|
pub operator: BinaryOperator,
|
||||||
|
pub left: BinaryPartFrame<A>,
|
||||||
|
pub right: BinaryPartFrame<A>,
|
||||||
|
|
||||||
|
pub digest: Option<Digest>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MappableFrame for BinaryExpressionFrame<PartiallyApplied> {
|
||||||
|
type Frame<X> = BinaryExpressionFrame<X>;
|
||||||
|
|
||||||
|
fn map_frame<A, B>(input: Self::Frame<A>, mut f: impl FnMut(A) -> B) -> Self::Frame<B> {
|
||||||
|
BinaryExpressionFrame::<B> {
|
||||||
|
start: input.start,
|
||||||
|
end: input.end,
|
||||||
|
operator: input.operator,
|
||||||
|
left: <BinaryPartFrame<PartiallyApplied> as MappableFrame>::map_frame(input.left, &mut f),
|
||||||
|
right: <BinaryPartFrame<PartiallyApplied> as MappableFrame>::map_frame(input.right, &mut f),
|
||||||
|
digest: input.digest,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Collapsible for &'a BinaryExpression {
|
||||||
|
type FrameToken = BinaryExpressionFrame<PartiallyApplied>;
|
||||||
|
|
||||||
|
fn into_frame(self) -> <Self::FrameToken as MappableFrame>::Frame<Self> {
|
||||||
|
BinaryExpressionFrame::<BinaryExpression> {
|
||||||
|
start: self.start,
|
||||||
|
end: self.end,
|
||||||
|
operator: self.operator.clone(),
|
||||||
|
left: self.left.into_frame(),
|
||||||
|
right: self.right.into_frame(),
|
||||||
|
digest: self.digest,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum BinaryPartFrame<A> {
|
||||||
|
Literal(Box<Literal>),
|
||||||
|
Identifier(Box<Identifier>),
|
||||||
|
BinaryExpression(Box<BinaryExpressionFrame<A>>),
|
||||||
|
CallExpression(Box<CallExpressionFrame<A>>),
|
||||||
|
UnaryExpression(Box<UnaryExpressionFrame<A>>),
|
||||||
|
MemberExpression(Box<MemberExpressionFrame<A>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MappableFrame for BinaryPartFrame<PartiallyApplied> {
|
||||||
|
type Frame<X> = BinaryPartFrame<X>;
|
||||||
|
|
||||||
|
fn map_frame<A, B>(input: Self::Frame<A>, mut f: impl FnMut(A) -> B) -> Self::Frame<B> {
|
||||||
|
match input {
|
||||||
|
BinaryPartFrame::Literal(x) => BinaryPartFrame::Literal(x),
|
||||||
|
BinaryPartFrame::Identifier(x) => BinaryPartFrame::Identifier(x),
|
||||||
|
BinaryPartFrame::BinaryExpression(x) => BinaryPartFrame::BinaryExpression(MappableFrame::map_frame(x, f)),
|
||||||
|
BinaryPartFrame::CallExpression(x) => BinaryPartFrame::CallExpression(MappableFrame::map_frame(x, f)),
|
||||||
|
BinaryPartFrame::UnaryExpression(x) => BinaryPartFrame::UnaryExpression(MappableFrame::map_frame(x, f)),
|
||||||
|
BinaryPartFrame::MemberExpression(x) => BinaryPartFrame::MemberExpression(MappableFrame::map_frame(x, f)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Collapsible for &'a BinaryPart {
|
||||||
|
type FrameToken = BinaryPartFrame<PartiallyApplied>;
|
||||||
|
|
||||||
|
fn into_frame(self) -> <Self::FrameToken as MappableFrame>::Frame<Self> {
|
||||||
|
match self {
|
||||||
|
BinaryPart::Literal(x) => BinaryPartFrame::Literal(x.clone()),
|
||||||
|
BinaryPart::Identifier(x) => BinaryPartFrame::Identifier(x.clone()),
|
||||||
|
BinaryPart::BinaryExpression(x) => BinaryPartFrame::BinaryExpression(x.into_frame()),
|
||||||
|
BinaryPart::CallExpression(x) => BinaryPartFrame::CallExpression(x.into_frame()),
|
||||||
|
BinaryPart::UnaryExpression(x) => BinaryPartFrame::UnaryExpression(x.into_frame()),
|
||||||
|
BinaryPart::MemberExpression(x) => BinaryPartFrame::MemberExpression(x.into_frame()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FunctionExpressionFrame<A> {
|
||||||
|
pub start: usize,
|
||||||
|
pub end: usize,
|
||||||
|
pub params: Vec<Parameter>,
|
||||||
|
pub body: ProgramFrame<A>,
|
||||||
|
pub return_type: Option<FnArgType>,
|
||||||
|
|
||||||
|
pub digest: Option<Digest>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CallExpressionFrame<A> {
|
||||||
|
pub start: usize,
|
||||||
|
pub end: usize,
|
||||||
|
pub callee: Identifier,
|
||||||
|
pub arguments: Vec<ExprFrame<A>>,
|
||||||
|
pub optional: bool,
|
||||||
|
|
||||||
|
pub digest: Option<Digest>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// More...
|
Reference in New Issue
Block a user