Compare commits
47 Commits
Author | SHA1 | Date | |
---|---|---|---|
c184a7d4d8 | |||
c38e52fbb7 | |||
ea0a3ac3ba | |||
385589ddf9 | |||
22df47fa96 | |||
a68748abcf | |||
1b8688f274 | |||
397839da84 | |||
ac120838e5 | |||
e6a2ac9c4a | |||
6e7e6e96cf | |||
73e155d79b | |||
a782f26ec2 | |||
01076c3aed | |||
fe512611ac | |||
cba953c245 | |||
54ca6ea0b2 | |||
6a01608c3a | |||
530f15e04a | |||
725e59d987 | |||
54313c9b03 | |||
890d96496c | |||
35999366a7 | |||
2affc7271d | |||
d30fbf8b4b | |||
3f7e776464 | |||
79cff57f43 | |||
1cd2cd82b2 | |||
60e187bd3e | |||
c64175425b | |||
36464e6984 | |||
2f0002e53c | |||
482833c88f | |||
d9d0a72306 | |||
65cd9fab64 | |||
5e41e382ce | |||
1e3cb00092 | |||
d1a2bd01ca | |||
aca13d087b | |||
fcdde3e482 | |||
a1df3d0ffc | |||
1852e6167b | |||
29bf77bb82 | |||
e81b614523 | |||
5a5fe3bb95 | |||
0710f6e5f2 | |||
c9d5633647 |
37
.github/ISSUE_TEMPLATE/cryptic_error.yml
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
name: Cryptic KCL Error
|
||||
description: File a bug report for source code that produces a confusing error
|
||||
title: "[CRYPTIC]: "
|
||||
labels: ["cryptic-error"]
|
||||
assignees: []
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "Thank you for taking the time to report a confusing error. Please provide as much information as possible to help us resolve it."
|
||||
|
||||
- type: textarea
|
||||
id: kcl
|
||||
attributes:
|
||||
label: Paste minimal KCL source that produces a cryptic error
|
||||
description: Minimal KCL reproducer that produces a cryptic error
|
||||
placeholder: "const ..."
|
||||
render: javascript
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: Description of what you expected to happen (if you know).
|
||||
placeholder: "I expected that..."
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context about the problem here.
|
||||
placeholder: "Anything else you want to add..."
|
||||
validations:
|
||||
required: false
|
38
README.md
@ -124,20 +124,40 @@ Before you submit a contribution PR to this repo, please ensure that:
|
||||
|
||||
## Release a new version
|
||||
|
||||
1. Bump the versions by running `./make-realease.sh` while on a fresh pull of main
|
||||
#### 1. Bump the versions by running `./make-release.sh` and create a Cut Release PR
|
||||
|
||||
That will create the branch with the updated json files for you.
|
||||
run `./make-release.sh` for a patch update
|
||||
run `./make-release.sh "minor"` for minor
|
||||
run `./make-release.sh "major"` for major
|
||||
That will create the branch with the updated json files for you:
|
||||
- run `./make-release.sh` or `./make-release.sh patch` for a patch update;
|
||||
- run `./make-release.sh minor` for minor; or
|
||||
- run `./make-release.sh major` for major.
|
||||
|
||||
After it runs you should just need to push the push the branch and open a PR (it will suggest a changelog for you too, delete any that are not user facing)
|
||||
After it runs you should just need the push the branch and open a PR.
|
||||
|
||||
The PR may serve as a place to discuss the human-readable changelog and extra QA.
|
||||
**Important:** It needs to be prefixed with `Cut release v` to build in release mode and a few other things to test in the best context possible, the intent would be for instance to have `Cut release v1.2.3` for the `v1.2.3` release candidate.
|
||||
|
||||
2. Merge the PR
|
||||
The PR may then serve as a place to discuss the human-readable changelog and extra QA. The `make-release.sh` tool suggests a changelog for you too to be used as PR description, just make sure to delete lines that are not user facing.
|
||||
|
||||
#### 2. Smoke test artifacts from the Cut Release PR
|
||||
|
||||
The release builds can be find under the `artifact` zip, at the very bottom of the `ci` action page for each commit on this branch.
|
||||
|
||||
We don't have a strict process, but click around and check for anything obvious, posting results as comments in the Cut Release PR.
|
||||
|
||||
The other `ci` output in Cut Release PRs is `updater-test`, because we don't have a way to test this fully automated, we have a semi-automated process. Download updater-test zip file, install the app, run it, expect an updater prompt to a dummy v0.99.99, install it and check that the app comes back at that version (on both macOS and Windows).
|
||||
|
||||
#### 3. Merge the Cut Release PR
|
||||
|
||||
This will kick the `create-release` action, that creates a _Draft_ release out of this Cut Release PR merge after less than a minute, with the new version as title and Cut Release PR as description.
|
||||
|
||||
|
||||
#### 4. Publish the release
|
||||
|
||||
Head over to https://github.com/KittyCAD/modeling-app/releases, the draft release corresponding to the merged Cut Release PR should show up at the top as _Draft_. Click on it, verify the content, and hit _Publish_.
|
||||
|
||||
#### 5. Profit
|
||||
|
||||
A new Action kicks in at https://github.com/KittyCAD/modeling-app/actions, which can be found under `release` event filter.
|
||||
|
||||
3. Profit (A new Action kicks in at https://github.com/KittyCAD/modeling-app/actions if the PR was correctly named)
|
||||
|
||||
## Fuzzing the parser
|
||||
|
||||
|
Before Width: | Height: | Size: 249 KiB After Width: | Height: | Size: 249 KiB |
Before Width: | Height: | Size: 249 KiB After Width: | Height: | Size: 249 KiB |
Before Width: | Height: | Size: 249 KiB After Width: | Height: | Size: 249 KiB |
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 171 KiB |
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 171 KiB |
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 171 KiB |
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 171 KiB |
Before Width: | Height: | Size: 249 KiB After Width: | Height: | Size: 249 KiB |
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 171 KiB |
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 171 KiB |
@ -175,7 +175,7 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
|
||||
}
|
||||
|
||||
// deselect line tool
|
||||
await page.getByRole('button', { name: 'Line' }).click()
|
||||
await page.getByRole('button', { name: 'Line', exact: true }).click()
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
const line1 = await u.getSegmentBodyCoords(`[data-overlay-index="${0}"]`, 0)
|
||||
@ -203,7 +203,7 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
|
||||
await expect(page.locator('.cm-cursor')).toHaveCount(2)
|
||||
}
|
||||
|
||||
await page.getByRole('button', { name: 'Constraints' }).click()
|
||||
await page.getByRole('button', { name: 'Length: open menu' }).click()
|
||||
await page.getByRole('button', { name: 'Equal Length' }).click()
|
||||
|
||||
// Open the code pane.
|
||||
@ -452,7 +452,7 @@ test.describe('Testing Camera Movement', () => {
|
||||
// await expect(u.codeLocator).toHaveText(code)
|
||||
|
||||
// click the line button
|
||||
await page.getByRole('button', { name: 'Line' }).click()
|
||||
await page.getByRole('button', { name: 'Line', exact: true }).click()
|
||||
|
||||
const hoverOverNothing = async () => {
|
||||
// await u.canvasLocator.hover({position: {x: 700, y: 325}})
|
||||
@ -1462,7 +1462,9 @@ test.describe('Can create sketches on all planes and their back sides', () => {
|
||||
await page.mouse.click(clickCoords.x, clickCoords.y)
|
||||
await page.waitForTimeout(300) // wait for animation
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Line' })).toBeVisible()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Line', exact: true })
|
||||
).toBeVisible()
|
||||
|
||||
// draw a line
|
||||
const startXPx = 600
|
||||
@ -1472,7 +1474,7 @@ test.describe('Can create sketches on all planes and their back sides', () => {
|
||||
|
||||
await expect(page.locator('.cm-content')).toHaveText(code)
|
||||
|
||||
await page.getByRole('button', { name: 'Line' }).click()
|
||||
await page.getByRole('button', { name: 'Line', exact: true }).click()
|
||||
await u.openAndClearDebugPanel()
|
||||
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
@ -1511,6 +1513,8 @@ test.describe('Can create sketches on all planes and their back sides', () => {
|
||||
})
|
||||
|
||||
test.describe('Copilot ghost text', () => {
|
||||
test.skip(true, 'Needs to get covered again')
|
||||
|
||||
test('completes code in empty file', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
// const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||
@ -1549,7 +1553,9 @@ test.describe('Copilot ghost text', () => {
|
||||
await expect(page.locator('.cm-ghostText')).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('copilot disabled in sketch mode no select plane', async ({ page }) => {
|
||||
test.skip('copilot disabled in sketch mode no select plane', async ({
|
||||
page,
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
// const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
@ -2096,7 +2102,7 @@ test.describe('Testing settings', () => {
|
||||
.hover()
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Roll back theme ; Has tooltip: Roll back to match default',
|
||||
name: 'Roll back theme',
|
||||
})
|
||||
.click()
|
||||
await expect(page.locator('select[name="app-theme"]')).toHaveValue('system')
|
||||
@ -2148,7 +2154,7 @@ test.describe('Testing settings', () => {
|
||||
.hover()
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Roll back theme ; Has tooltip: Roll back to match default',
|
||||
name: 'Roll back theme',
|
||||
})
|
||||
.click()
|
||||
await expect(page.locator('select[name="app-theme"]')).toHaveValue('system')
|
||||
@ -2562,7 +2568,7 @@ test.describe('Testing selections', () => {
|
||||
|> line([-${commonPoints.num2}, 0], %)`)
|
||||
|
||||
// deselect line tool
|
||||
await page.getByRole('button', { name: 'Line' }).click()
|
||||
await page.getByRole('button', { name: 'Line', exact: true }).click()
|
||||
|
||||
await u.closeDebugPanel()
|
||||
const selectionSequence = async () => {
|
||||
@ -2587,8 +2593,10 @@ test.describe('Testing selections', () => {
|
||||
// click a segment hold shift and click an axis, see that a relevant constraint is enabled
|
||||
await topHorzSegmentClick()
|
||||
await page.keyboard.down('Shift')
|
||||
const constrainButton = page.getByRole('button', { name: 'Constraints' })
|
||||
const absYButton = page.getByRole('button', { name: 'ABS Y' })
|
||||
const constrainButton = page.getByRole('button', {
|
||||
name: 'Length: open menu',
|
||||
})
|
||||
const absYButton = page.getByRole('button', { name: 'Absolute Y' })
|
||||
await constrainButton.click()
|
||||
await expect(absYButton).toBeDisabled()
|
||||
await page.waitForTimeout(100)
|
||||
@ -3099,6 +3107,49 @@ const sketch002 = startSketchOn(extrude001, $seg01)
|
||||
).not.toBeDisabled()
|
||||
})
|
||||
|
||||
test('Fillet button states test', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([-5, -5], %)
|
||||
|> line([0, 10], %)
|
||||
|> line([10, 0], %)
|
||||
|> line([0, -10], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)`
|
||||
)
|
||||
})
|
||||
|
||||
await page.setViewportSize({ width: 1000, height: 500 })
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.closeDebugPanel()
|
||||
|
||||
const selectSegment = () => page.getByText(`line([10, 0], %)`).click()
|
||||
const selectClose = () => page.getByText(`close(%)`).click()
|
||||
const clickEmpty = () => page.mouse.click(950, 100)
|
||||
|
||||
// expect fillet button without any bodies in the scene
|
||||
await selectSegment()
|
||||
await expect(page.getByRole('button', { name: 'Fillet' })).toBeDisabled()
|
||||
await clickEmpty()
|
||||
await expect(page.getByRole('button', { name: 'Fillet' })).toBeDisabled()
|
||||
|
||||
// test fillet button with the body in the scene
|
||||
const codeToAdd = `${await u.codeLocator.allInnerTexts()}
|
||||
const extrude001 = extrude(10, sketch001)`
|
||||
await u.codeLocator.fill(codeToAdd)
|
||||
await selectSegment()
|
||||
await expect(page.getByRole('button', { name: 'Fillet' })).toBeEnabled()
|
||||
await selectClose()
|
||||
await expect(page.getByRole('button', { name: 'Fillet' })).toBeDisabled()
|
||||
await clickEmpty()
|
||||
await expect(page.getByRole('button', { name: 'Fillet' })).toBeEnabled()
|
||||
})
|
||||
|
||||
const removeAfterFirstParenthesis = (inputString: string) => {
|
||||
const index = inputString.indexOf('(')
|
||||
if (index !== -1) {
|
||||
@ -3369,21 +3420,6 @@ const extrude001 = extrude(50, sketch001)
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Edit Sketch' })
|
||||
).not.toBeVisible()
|
||||
|
||||
// selecting an editable sketch but clicking "start sketch" should start a new sketch and not edit the existing one
|
||||
await page.getByText(selectionsSnippets.extrudeAndEditAllowed).click()
|
||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||
await page.waitForTimeout(200)
|
||||
await page.getByTestId('KCL Code').click()
|
||||
await page.waitForTimeout(200)
|
||||
await page.mouse.click(734, 134)
|
||||
await page.waitForTimeout(100)
|
||||
await page.getByTestId('KCL Code').click()
|
||||
// expect main content to contain `sketch005` i.e. started a new sketch
|
||||
await page.waitForTimeout(300)
|
||||
await expect(page.locator('.cm-content')).toHaveText(
|
||||
/sketch001 = startSketchOn\('XZ'\)/
|
||||
)
|
||||
})
|
||||
|
||||
test('Deselecting line tool should mean nothing happens on click', async ({
|
||||
@ -3420,7 +3456,7 @@ const extrude001 = extrude(50, sketch001)
|
||||
let previousCodeContent = await page.locator('.cm-content').innerText()
|
||||
|
||||
// deselect the line tool by clicking it
|
||||
await page.getByRole('button', { name: 'Line' }).click()
|
||||
await page.getByRole('button', { name: 'Line', exact: true }).click()
|
||||
|
||||
await page.mouse.click(700, 200)
|
||||
await page.waitForTimeout(100)
|
||||
@ -3433,7 +3469,7 @@ const extrude001 = extrude(50, sketch001)
|
||||
await expect(page.locator('.cm-content')).toHaveText(previousCodeContent)
|
||||
|
||||
// select line tool again
|
||||
await page.getByRole('button', { name: 'Line' }).click()
|
||||
await page.getByRole('button', { name: 'Line', exact: true }).click()
|
||||
|
||||
await u.closeDebugPanel()
|
||||
|
||||
@ -3500,11 +3536,62 @@ test.describe('Command bar tests', () => {
|
||||
`const extrude001 = extrude(${KCL_DEFAULT_LENGTH}, sketch001)`
|
||||
)
|
||||
})
|
||||
test('Command bar works and can change a setting', async ({ page }) => {
|
||||
|
||||
test('Fillet from command bar', async ({ page }) => {
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`const sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-5, -5], %)
|
||||
|> line([0, 10], %)
|
||||
|> line([10, 0], %)
|
||||
|> line([0, -10], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
const extrude001 = extrude(-10, sketch001)`
|
||||
)
|
||||
})
|
||||
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1000, height: 500 })
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.closeDebugPanel()
|
||||
|
||||
const selectSegment = () => page.getByText(`line([0, -10], %)`).click()
|
||||
|
||||
await selectSegment()
|
||||
await page.waitForTimeout(100)
|
||||
await page.getByRole('button', { name: 'Fillet' }).click()
|
||||
await page.waitForTimeout(100)
|
||||
await page.keyboard.press('Enter')
|
||||
await page.waitForTimeout(100)
|
||||
await page.keyboard.press('Enter')
|
||||
await page.waitForTimeout(100)
|
||||
await expect(page.locator('.cm-activeLine')).toContainText(
|
||||
`fillet({ radius: ${KCL_DEFAULT_LENGTH}, tags: [seg01] }, %)`
|
||||
)
|
||||
})
|
||||
|
||||
test('Command bar can change a setting, and switch back and forth between arguments', async ({
|
||||
page,
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
const commandBarButton = page.getByRole('button', { name: 'Commands' })
|
||||
const cmdSearchBar = page.getByPlaceholder('Search commands')
|
||||
const themeOption = page.getByRole('option', {
|
||||
name: 'theme',
|
||||
exact: false,
|
||||
})
|
||||
const commandLevelArgButton = page.getByRole('button', { name: 'level' })
|
||||
const commandThemeArgButton = page.getByRole('button', { name: 'value' })
|
||||
// This selector changes after we set the setting
|
||||
let commandOptionInput = page.getByPlaceholder('Select an option')
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).not.toBeDisabled()
|
||||
@ -3515,23 +3602,17 @@ test.describe('Command bar tests', () => {
|
||||
.or(page.getByRole('button', { name: '⌘K' }))
|
||||
.click()
|
||||
|
||||
let cmdSearchBar = page.getByPlaceholder('Search commands')
|
||||
await expect(cmdSearchBar).toBeVisible()
|
||||
await page.keyboard.press('Escape')
|
||||
cmdSearchBar = page.getByPlaceholder('Search commands')
|
||||
await expect(cmdSearchBar).not.toBeVisible()
|
||||
|
||||
// Now try the same, but with the keyboard shortcut, check focus
|
||||
await page.keyboard.press('Meta+K')
|
||||
cmdSearchBar = page.getByPlaceholder('Search commands')
|
||||
await expect(cmdSearchBar).toBeVisible()
|
||||
await expect(cmdSearchBar).toBeFocused()
|
||||
|
||||
// Try typing in the command bar
|
||||
await page.keyboard.type('theme')
|
||||
const themeOption = page.getByRole('option', {
|
||||
name: 'Settings · app · theme',
|
||||
})
|
||||
await cmdSearchBar.fill('theme')
|
||||
await expect(themeOption).toBeVisible()
|
||||
await themeOption.click()
|
||||
const themeInput = page.getByPlaceholder('Select an option')
|
||||
@ -3553,6 +3634,24 @@ test.describe('Command bar tests', () => {
|
||||
).toBeVisible()
|
||||
// Check that the theme changed
|
||||
await expect(page.locator('body')).not.toHaveClass(`body-bg dark`)
|
||||
|
||||
commandOptionInput = page.getByPlaceholder('system')
|
||||
|
||||
// Test case for https://github.com/KittyCAD/modeling-app/issues/2882
|
||||
await commandBarButton.click()
|
||||
await cmdSearchBar.focus()
|
||||
await cmdSearchBar.fill('theme')
|
||||
await themeOption.click()
|
||||
await expect(commandThemeArgButton).toBeDisabled()
|
||||
await commandOptionInput.focus()
|
||||
await commandOptionInput.fill('lig')
|
||||
await commandLevelArgButton.click()
|
||||
await expect(commandLevelArgButton).toBeDisabled()
|
||||
|
||||
// Test case for https://github.com/KittyCAD/modeling-app/issues/2881
|
||||
await commandThemeArgButton.click()
|
||||
await expect(commandThemeArgButton).toBeDisabled()
|
||||
await expect(commandLevelArgButton).toHaveText('level: project')
|
||||
})
|
||||
|
||||
test('Command bar keybinding works from code editor and can change a setting', async ({
|
||||
@ -3577,7 +3676,7 @@ test.describe('Command bar tests', () => {
|
||||
await expect(cmdSearchBar).toBeFocused()
|
||||
|
||||
// Try typing in the command bar
|
||||
await page.keyboard.type('theme')
|
||||
await cmdSearchBar.fill('theme')
|
||||
const themeOption = page.getByRole('option', {
|
||||
name: 'Settings · app · theme',
|
||||
})
|
||||
@ -3648,7 +3747,9 @@ test.describe('Command bar tests', () => {
|
||||
await page.mouse.click(700, 200)
|
||||
|
||||
// Assert that we're on the distance step
|
||||
await expect(page.getByRole('button', { name: 'distance' })).toBeDisabled()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'distance', exact: false })
|
||||
).toBeDisabled()
|
||||
|
||||
// Assert that the an alternative variable name is chosen,
|
||||
// since the default variable name is already in use (distance)
|
||||
@ -3663,11 +3764,12 @@ test.describe('Command bar tests', () => {
|
||||
|
||||
// Review step and argument hotkeys
|
||||
await expect(submitButton).toBeEnabled()
|
||||
await page.keyboard.press('Backspace')
|
||||
await expect(submitButton).toBeFocused()
|
||||
await submitButton.press('Backspace')
|
||||
|
||||
// Assert we're back on the distance step
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Distance 5', exact: false })
|
||||
page.getByRole('button', { name: 'distance', exact: false })
|
||||
).toBeDisabled()
|
||||
|
||||
await continueButton.click()
|
||||
@ -3691,6 +3793,58 @@ const extrude001 = extrude(distance001, sketch001)`.replace(
|
||||
) // remove newlines
|
||||
)
|
||||
})
|
||||
|
||||
test('Can switch between sketch tools via command bar', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
const sketchButton = page.getByRole('button', { name: 'Start Sketch' })
|
||||
const cmdBarButton = page.getByRole('button', { name: 'Commands' })
|
||||
const rectangleToolCommand = page.getByRole('option', {
|
||||
name: 'rectangle',
|
||||
})
|
||||
const rectangleToolButton = page.getByRole('button', {
|
||||
name: 'Corner rectangle',
|
||||
exact: true,
|
||||
})
|
||||
const lineToolCommand = page.getByRole('option', {
|
||||
name: 'Line',
|
||||
})
|
||||
const lineToolButton = page.getByRole('button', {
|
||||
name: 'Line',
|
||||
exact: true,
|
||||
})
|
||||
const arcToolCommand = page.getByRole('option', { name: 'Tangential Arc' })
|
||||
const arcToolButton = page.getByRole('button', {
|
||||
name: 'Tangential Arc',
|
||||
exact: true,
|
||||
})
|
||||
|
||||
// Start a sketch
|
||||
await sketchButton.click()
|
||||
await page.mouse.click(700, 200)
|
||||
|
||||
// Switch between sketch tools via the command bar
|
||||
await expect(lineToolButton).toHaveAttribute('aria-pressed', 'true')
|
||||
await cmdBarButton.click()
|
||||
await rectangleToolCommand.click()
|
||||
await expect(rectangleToolButton).toHaveAttribute('aria-pressed', 'true')
|
||||
await cmdBarButton.click()
|
||||
await lineToolCommand.click()
|
||||
await expect(lineToolButton).toHaveAttribute('aria-pressed', 'true')
|
||||
|
||||
// Click in the scene a couple times to draw a line
|
||||
// so tangential arc is valid
|
||||
await page.mouse.click(700, 200)
|
||||
await page.mouse.move(700, 300, { steps: 5 })
|
||||
await page.mouse.click(700, 300)
|
||||
|
||||
// switch to tangential arc via command bar
|
||||
await cmdBarButton.click()
|
||||
await arcToolCommand.click()
|
||||
await expect(arcToolButton).toHaveAttribute('aria-pressed', 'true')
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Regression tests', () => {
|
||||
@ -3712,10 +3866,7 @@ test.describe('Regression tests', () => {
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
// expand variables section
|
||||
const variablesTabButton = page.getByRole('tab', {
|
||||
name: 'Variables',
|
||||
exact: false,
|
||||
})
|
||||
const variablesTabButton = page.getByTestId('variables-pane-button')
|
||||
await variablesTabButton.click()
|
||||
|
||||
// can find sketch001 in the variables summary (pretty-json-container, makes sure we're not looking in the code editor)
|
||||
@ -3740,10 +3891,7 @@ test.describe('Regression tests', () => {
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
const variablesTabButton = page.getByRole('tab', {
|
||||
name: 'Variables',
|
||||
exact: false,
|
||||
})
|
||||
const variablesTabButton = page.getByTestId('variables-pane-button')
|
||||
await variablesTabButton.click()
|
||||
// expect to see "myVar:5"
|
||||
await expect(
|
||||
@ -4004,7 +4152,7 @@ test.describe('Sketch tests', () => {
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await page.getByRole('button', { name: 'Line' }).click()
|
||||
await page.getByRole('button', { name: 'Line', exact: true }).click()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await page.mouse.click(700, 200)
|
||||
@ -4031,9 +4179,7 @@ test.describe('Sketch tests', () => {
|
||||
page.getByRole('button', { name: 'Exit Sketch' })
|
||||
).toBeVisible()
|
||||
|
||||
await expect(
|
||||
page.getByText('click plane or face to sketch on')
|
||||
).toBeVisible()
|
||||
await expect(page.getByText('select a plane or face')).toBeVisible()
|
||||
|
||||
await page.keyboard.press('Escape')
|
||||
await expect(
|
||||
@ -4584,7 +4730,7 @@ test.describe('Sketch tests', () => {
|
||||
await expect(page.locator('.cm-content')).toHaveText(code)
|
||||
// Assert the tool was unequipped
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Line' })
|
||||
page.getByRole('button', { name: 'Line', exact: true })
|
||||
).not.toHaveAttribute('aria-pressed', 'true')
|
||||
|
||||
// exit sketch
|
||||
@ -4642,10 +4788,10 @@ test.describe('Sketch tests', () => {
|
||||
// click extrude
|
||||
await page.getByRole('button', { name: 'Extrude' }).click()
|
||||
|
||||
// sketch selection should already have been made. "Selection 1 face" only show up when the selection has been made already
|
||||
// sketch selection should already have been made. "Selection: 1 face" only show up when the selection has been made already
|
||||
// otherwise the cmdbar would be waiting for a selection.
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Selection 1 face' })
|
||||
page.getByRole('button', { name: 'selection : 1 face', exact: false })
|
||||
).toBeVisible()
|
||||
})
|
||||
test("Existing sketch with bad code delete user's code", async ({ page }) => {
|
||||
@ -4746,8 +4892,7 @@ test.describe('Testing constraints', () => {
|
||||
await page.mouse.click(834, 244)
|
||||
await page.keyboard.up('Shift')
|
||||
|
||||
await page.getByRole('button', { name: 'Constraints', exact: true }).click()
|
||||
await page.getByRole('button', { name: 'length', exact: true }).click()
|
||||
await page.getByRole('button', { name: 'Length', exact: true }).click()
|
||||
await page.getByText('Add constraining value').click()
|
||||
|
||||
await expect(page.locator('.cm-content')).toHaveText(
|
||||
@ -4801,12 +4946,10 @@ const part002 = startSketchOn('XZ')
|
||||
await page.waitForTimeout(100) // this wait is needed for webkit - not sure why
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Constraints',
|
||||
name: 'Length: open menu',
|
||||
})
|
||||
.click()
|
||||
await page
|
||||
.getByRole('button', { name: 'remove constraints', exact: true })
|
||||
.click()
|
||||
await page.getByRole('button', { name: 'remove constraints' }).click()
|
||||
|
||||
await page.getByText('line([39.13, 68.63], %)').click()
|
||||
const activeLinesContent = await page.locator('.cm-activeLine').all()
|
||||
@ -4867,11 +5010,11 @@ const part002 = startSketchOn('XZ')
|
||||
await page.keyboard.up('Shift')
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Constraints',
|
||||
name: 'Length: open menu',
|
||||
})
|
||||
.click()
|
||||
await page
|
||||
.getByRole('button', { name: 'perpendicular distance', exact: true })
|
||||
.getByRole('button', { name: 'Perpendicular Distance' })
|
||||
.click()
|
||||
|
||||
const createNewVariableCheckbox = page.getByTestId(
|
||||
@ -4966,12 +5109,10 @@ const part002 = startSketchOn('XZ')
|
||||
await page.keyboard.up('Shift')
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Constraints',
|
||||
name: 'Length: open menu',
|
||||
})
|
||||
.click()
|
||||
await page
|
||||
.getByRole('button', { name: constraint, exact: true })
|
||||
.click()
|
||||
await page.getByRole('button', { name: constraint }).click()
|
||||
|
||||
const createNewVariableCheckbox = page.getByTestId(
|
||||
'create-new-variable-checkbox'
|
||||
@ -5012,25 +5153,25 @@ const part002 = startSketchOn('XZ')
|
||||
{
|
||||
testName: 'Add variable',
|
||||
addVariable: true,
|
||||
constraint: 'ABS X',
|
||||
constraint: 'Absolute X',
|
||||
value: 'xDis001, 61.34',
|
||||
},
|
||||
{
|
||||
testName: 'No variable',
|
||||
addVariable: false,
|
||||
constraint: 'ABS X',
|
||||
constraint: 'Absolute X',
|
||||
value: '154.9, 61.34',
|
||||
},
|
||||
{
|
||||
testName: 'Add variable',
|
||||
addVariable: true,
|
||||
constraint: 'ABS Y',
|
||||
constraint: 'Absolute Y',
|
||||
value: '154.9, yDis001',
|
||||
},
|
||||
{
|
||||
testName: 'No variable',
|
||||
addVariable: false,
|
||||
constraint: 'ABS Y',
|
||||
constraint: 'Absolute Y',
|
||||
value: '154.9, 61.34',
|
||||
},
|
||||
] as const
|
||||
@ -5066,7 +5207,7 @@ const part002 = startSketchOn('XZ')
|
||||
u.getSegmentBodyCoords(`[data-overlay-index="${2}"]`),
|
||||
])
|
||||
|
||||
if (constraint === 'ABS X') {
|
||||
if (constraint === 'Absolute X') {
|
||||
await page.mouse.click(600, 130)
|
||||
} else {
|
||||
await page.mouse.click(900, 250)
|
||||
@ -5077,7 +5218,7 @@ const part002 = startSketchOn('XZ')
|
||||
await page.keyboard.up('Shift')
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Constraints',
|
||||
name: 'Length: open menu',
|
||||
})
|
||||
.click()
|
||||
await page
|
||||
@ -5185,10 +5326,10 @@ const part002 = startSketchOn('XZ')
|
||||
await page.keyboard.up('Shift')
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Constraints',
|
||||
name: 'Length: open menu',
|
||||
})
|
||||
.click()
|
||||
await page.getByTestId('angle').click()
|
||||
await page.getByTestId('dropdown-constraint-angle').click()
|
||||
|
||||
const createNewVariableCheckbox = page.getByTestId(
|
||||
'create-new-variable-checkbox'
|
||||
@ -5286,10 +5427,10 @@ const part002 = startSketchOn('XZ')
|
||||
await page.mouse.click(line3.x, line3.y)
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Constraints',
|
||||
name: 'Length: open menu',
|
||||
})
|
||||
.click()
|
||||
await page.getByTestId(constraint).click()
|
||||
await page.getByTestId('dropdown-constraint-' + constraint).click()
|
||||
|
||||
if (!addVariable) {
|
||||
await page.getByTestId('create-new-variable-checkbox').click()
|
||||
@ -5377,7 +5518,7 @@ const part002 = startSketchOn('XZ')
|
||||
await expect(activeLinesContent).toHaveLength(codeAfter.length)
|
||||
|
||||
const constraintMenuButton = page.getByRole('button', {
|
||||
name: 'Constraints',
|
||||
name: 'Length: open menu',
|
||||
})
|
||||
const constraintButton = page
|
||||
.getByRole('button', {
|
||||
@ -5460,7 +5601,7 @@ const part002 = startSketchOn('XZ')
|
||||
await page.mouse.click(line3.x - 3, line3.y + 20)
|
||||
await page.keyboard.up('Shift')
|
||||
const constraintMenuButton = page.getByRole('button', {
|
||||
name: 'Constraints',
|
||||
name: 'Length: open menu',
|
||||
})
|
||||
const constraintButton = page.getByRole('button', {
|
||||
name: constraintName,
|
||||
@ -5537,7 +5678,7 @@ const part002 = startSketchOn('XZ')
|
||||
await page.mouse.click(axisClick.x, axisClick.y)
|
||||
await page.keyboard.up('Shift')
|
||||
const constraintMenuButton = page.getByRole('button', {
|
||||
name: 'Constraints',
|
||||
name: 'Length: open menu',
|
||||
})
|
||||
const constraintButton = page.getByRole('button', {
|
||||
name: constraintName,
|
||||
@ -5596,10 +5737,10 @@ const part002 = startSketchOn('XZ')
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Constraints',
|
||||
name: 'Length: open menu',
|
||||
})
|
||||
.click()
|
||||
await page.getByRole('button', { name: 'horizontal', exact: true }).click()
|
||||
await page.getByRole('button', { name: 'Horizontal', exact: true }).click()
|
||||
|
||||
let activeLinesContent = await page.locator('.cm-activeLine').all()
|
||||
await expect(activeLinesContent[0]).toHaveText(`|> xLine(3.13, %)`)
|
||||
@ -5620,13 +5761,13 @@ const part002 = startSketchOn('XZ')
|
||||
await page.waitForTimeout(300)
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Constraints',
|
||||
name: 'Length: open menu',
|
||||
})
|
||||
.click()
|
||||
// await expect(page.getByRole('button', { name: 'length', exact: true })).toBeVisible()
|
||||
await page.waitForTimeout(200)
|
||||
// await page.getByRole('button', { name: 'length', exact: true }).click()
|
||||
await page.locator('[data-testid="length"]').click()
|
||||
await page.getByTestId('dropdown-constraint-length').click()
|
||||
|
||||
await page.getByLabel('length Value').fill('10')
|
||||
await page.getByRole('button', { name: 'Add constraining value' }).click()
|
||||
@ -6918,6 +7059,8 @@ test.describe('Test network and connection issues', () => {
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
const networkToggle = page.getByTestId('network-toggle')
|
||||
|
||||
// This is how we wait until the stream is online
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
@ -6931,7 +7074,7 @@ test.describe('Test network and connection issues', () => {
|
||||
await expect(networkPopover).not.toBeVisible()
|
||||
|
||||
// (First check) Expect the network to be up
|
||||
await expect(page.getByText('Network Health (Connected)')).toBeVisible()
|
||||
await expect(networkToggle).toContainText('Connected')
|
||||
|
||||
// Click the network widget
|
||||
await networkWidget.click()
|
||||
@ -6953,7 +7096,7 @@ test.describe('Test network and connection issues', () => {
|
||||
})
|
||||
|
||||
// Expect the network to be down
|
||||
await expect(page.getByText('Network Health (Offline)')).toBeVisible()
|
||||
await expect(networkToggle).toContainText('Offline')
|
||||
|
||||
// Click the network widget
|
||||
await networkWidget.click()
|
||||
@ -6979,7 +7122,7 @@ test.describe('Test network and connection issues', () => {
|
||||
).not.toBeDisabled({ timeout: 15000 })
|
||||
|
||||
// (Second check) expect the network to be up
|
||||
await expect(page.getByText('Network Health (Connected)')).toBeVisible()
|
||||
await expect(networkToggle).toContainText('Connected')
|
||||
})
|
||||
|
||||
test('Engine disconnect & reconnect in sketch mode', async ({
|
||||
@ -6991,6 +7134,8 @@ test.describe('Test network and connection issues', () => {
|
||||
browserName === 'webkit',
|
||||
'Skip on Safari until `window.tearDown` is working there'
|
||||
)
|
||||
const networkToggle = page.getByTestId('network-toggle')
|
||||
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||
@ -7033,7 +7178,7 @@ test.describe('Test network and connection issues', () => {
|
||||
|> line([${commonPoints.num1}, 0], %)`)
|
||||
|
||||
// Expect the network to be up
|
||||
await expect(page.getByText('Network Health (Connected)')).toBeVisible()
|
||||
await expect(networkToggle).toContainText('Connected')
|
||||
|
||||
// simulate network down
|
||||
await u.emulateNetworkConditions({
|
||||
@ -7045,7 +7190,7 @@ test.describe('Test network and connection issues', () => {
|
||||
})
|
||||
|
||||
// Expect the network to be down
|
||||
await expect(page.getByText('Network Health (Offline)')).toBeVisible()
|
||||
await expect(networkToggle).toContainText('Offline')
|
||||
|
||||
// Ensure we are not in sketch mode
|
||||
await expect(
|
||||
@ -7070,7 +7215,8 @@ test.describe('Test network and connection issues', () => {
|
||||
).not.toBeDisabled({ timeout: 15000 })
|
||||
|
||||
// Expect the network to be up
|
||||
await expect(page.getByText('Network Health (Connected)')).toBeVisible()
|
||||
await expect(networkToggle).toContainText('Connected')
|
||||
await expect(page.getByTestId('loading-stream')).not.toBeAttached()
|
||||
|
||||
// Click off the code pane.
|
||||
await page.mouse.click(100, 100)
|
||||
@ -7086,7 +7232,7 @@ test.describe('Test network and connection issues', () => {
|
||||
await page.waitForTimeout(150)
|
||||
|
||||
// Click the line tool
|
||||
await page.getByRole('button', { name: 'Line' }).click()
|
||||
await page.getByRole('button', { name: 'Line', exact: true }).click()
|
||||
|
||||
await page.waitForTimeout(150)
|
||||
|
||||
@ -7113,7 +7259,7 @@ test.describe('Test network and connection issues', () => {
|
||||
page.getByRole('button', { name: 'Exit Sketch' })
|
||||
).toBeVisible()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Line' })
|
||||
page.getByRole('button', { name: 'Line', exact: true })
|
||||
).not.toHaveAttribute('aria-pressed', 'true')
|
||||
|
||||
// Exit sketch
|
||||
@ -7518,7 +7664,7 @@ test('Keyboard shortcuts can be viewed through the help menu', async ({
|
||||
.waitFor({ state: 'visible' })
|
||||
|
||||
// Open the help menu
|
||||
await page.getByRole('button', { name: 'Help', exact: false }).click()
|
||||
await page.getByRole('button', { name: 'Help and resources' }).click()
|
||||
|
||||
// Open the keyboard shortcuts
|
||||
await page.getByRole('button', { name: 'Keyboard Shortcuts' }).click()
|
||||
@ -7542,8 +7688,11 @@ test('First escape in tool pops you out of tool, second exits sketch mode', asyn
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.closeDebugPanel()
|
||||
|
||||
const lineButton = page.getByRole('button', { name: 'Line' })
|
||||
const arcButton = page.getByRole('button', { name: 'Tangential Arc' })
|
||||
const lineButton = page.getByRole('button', { name: 'Line', exact: true })
|
||||
const arcButton = page.getByRole('button', {
|
||||
name: 'Tangential Arc',
|
||||
exact: true,
|
||||
})
|
||||
|
||||
// Test these hotkeys perform actions when
|
||||
// focus is on the canvas
|
||||
@ -7555,6 +7704,7 @@ test('First escape in tool pops you out of tool, second exits sketch mode', asyn
|
||||
await page.mouse.move(800, 300)
|
||||
await page.mouse.click(800, 300)
|
||||
await page.waitForTimeout(1000)
|
||||
await expect(lineButton).toBeVisible()
|
||||
await expect(lineButton).toHaveAttribute('aria-pressed', 'true')
|
||||
|
||||
// Draw a line
|
||||
@ -7624,9 +7774,12 @@ test('Basic default modeling and sketch hotkeys work', async ({ page }) => {
|
||||
await u.closeDebugPanel()
|
||||
|
||||
const codePane = page.getByRole('textbox').locator('div')
|
||||
const codePaneButton = page.getByRole('tab', { name: 'KCL Code' })
|
||||
const lineButton = page.getByRole('button', { name: 'Line' })
|
||||
const arcButton = page.getByRole('button', { name: 'Tangential Arc' })
|
||||
const codePaneButton = page.getByTestId('code-pane-button')
|
||||
const lineButton = page.getByRole('button', { name: 'Line', exact: true })
|
||||
const arcButton = page.getByRole('button', {
|
||||
name: 'Tangential Arc',
|
||||
exact: true,
|
||||
})
|
||||
const extrudeButton = page.getByRole('button', { name: 'Extrude' })
|
||||
|
||||
// Test that the hotkeys do nothing when
|
||||
@ -7647,7 +7800,7 @@ test('Basic default modeling and sketch hotkeys work', async ({ page }) => {
|
||||
await page.mouse.click(600, 250)
|
||||
|
||||
// work-around: to stop "keyboard.press('s')" from typing in the editor even when it should be blurred
|
||||
await page.getByRole('button', { name: 'Commands ⌘K' }).click()
|
||||
await page.getByRole('button', { name: 'Commands' }).click()
|
||||
await page.waitForTimeout(100)
|
||||
await page.keyboard.press('Escape')
|
||||
await page.waitForTimeout(100)
|
||||
|
@ -431,7 +431,9 @@ test('Draft segments should look right', async ({ page, context }) => {
|
||||
|> line([7.25, 0], %)`
|
||||
await expect(page.locator('.cm-content')).toHaveText(code)
|
||||
|
||||
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
||||
await page
|
||||
.getByRole('button', { name: 'Tangential Arc', exact: true })
|
||||
.click()
|
||||
|
||||
await page.mouse.move(startXPx + PUR * 30, 500 - PUR * 20, { steps: 10 })
|
||||
|
||||
@ -475,8 +477,10 @@ test('Draft rectangles should look right', async ({ page, context }) => {
|
||||
const startXPx = 600
|
||||
|
||||
// Equip the rectangle tool
|
||||
await page.getByRole('button', { name: 'Line' }).click()
|
||||
await page.getByRole('button', { name: 'Rectangle' }).click()
|
||||
await page.getByRole('button', { name: 'Line', exact: true }).click()
|
||||
await page
|
||||
.getByRole('button', { name: 'Corner rectangle', exact: true })
|
||||
.click()
|
||||
|
||||
// Draw the rectangle
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 30)
|
||||
@ -535,7 +539,9 @@ test.describe('Client side scene scale should match engine scale', () => {
|
||||
|> line([7.25, 0], %)`
|
||||
await expect(u.codeLocator).toHaveText(code)
|
||||
|
||||
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
||||
await page
|
||||
.getByRole('button', { name: 'Tangential Arc', exact: true })
|
||||
.click()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
|
||||
@ -545,7 +551,9 @@ test.describe('Client side scene scale should match engine scale', () => {
|
||||
await expect(u.codeLocator).toHaveText(code)
|
||||
|
||||
// click tangential arc tool again to unequip it
|
||||
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
||||
await page
|
||||
.getByRole('button', { name: 'Tangential Arc', exact: true })
|
||||
.click()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
// screen shot should show the sketch
|
||||
@ -634,7 +642,9 @@ test.describe('Client side scene scale should match engine scale', () => {
|
||||
|> line([184.3, 0], %)`
|
||||
await expect(u.codeLocator).toHaveText(code)
|
||||
|
||||
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
||||
await page
|
||||
.getByRole('button', { name: 'Tangential Arc', exact: true })
|
||||
.click()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
|
||||
@ -643,7 +653,9 @@ test.describe('Client side scene scale should match engine scale', () => {
|
||||
|> tangentialArcTo([551.2, -62.01], %)`
|
||||
await expect(u.codeLocator).toHaveText(code)
|
||||
|
||||
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
||||
await page
|
||||
.getByRole('button', { name: 'Tangential Arc', exact: true })
|
||||
.click()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
// screen shot should show the sketch
|
||||
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 72 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 63 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 31 KiB |
@ -16,14 +16,14 @@ export const TEST_COLORS = {
|
||||
} as const
|
||||
|
||||
async function waitForPageLoad(page: Page) {
|
||||
// wait for 'Loading stream...' spinner
|
||||
await page.getByTestId('loading-stream').waitFor()
|
||||
// wait for all spinners to be gone
|
||||
await page
|
||||
.getByTestId('loading')
|
||||
.waitFor({ state: 'detached', timeout: 20_000 })
|
||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
await page.getByTestId('start-sketch').waitFor()
|
||||
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeEnabled({
|
||||
timeout: 20_000,
|
||||
})
|
||||
}
|
||||
|
||||
async function removeCurrentCode(page: Page) {
|
||||
@ -58,44 +58,45 @@ async function waitForDefaultPlanesToBeVisible(page: Page) {
|
||||
}
|
||||
|
||||
async function openKclCodePanel(page: Page) {
|
||||
const paneLocator = page.getByRole('tab', { name: 'KCL Code', exact: false })
|
||||
const isOpen = (await paneLocator?.getAttribute('aria-selected')) === 'true'
|
||||
const paneLocator = page.getByTestId('code-pane-button')
|
||||
const ariaSelected = await paneLocator?.getAttribute('aria-pressed')
|
||||
const isOpen = ariaSelected === 'true'
|
||||
|
||||
if (!isOpen) {
|
||||
await paneLocator.click()
|
||||
await paneLocator.and(page.locator('[aria-selected="true"]')).waitFor()
|
||||
await expect(paneLocator).toHaveAttribute('aria-pressed', 'true')
|
||||
}
|
||||
}
|
||||
|
||||
async function closeKclCodePanel(page: Page) {
|
||||
const paneLocator = page.getByRole('tab', { name: 'KCL Code', exact: false })
|
||||
const isOpen = (await paneLocator?.getAttribute('aria-selected')) === 'true'
|
||||
const paneLocator = page.getByTestId('code-pane-button')
|
||||
const ariaSelected = await paneLocator?.getAttribute('aria-pressed')
|
||||
const isOpen = ariaSelected === 'true'
|
||||
|
||||
if (isOpen) {
|
||||
await paneLocator.click()
|
||||
await paneLocator
|
||||
.and(page.locator(':not([aria-selected="true"])'))
|
||||
.waitFor()
|
||||
await expect(paneLocator).not.toHaveAttribute('aria-pressed', 'true')
|
||||
}
|
||||
}
|
||||
|
||||
async function openDebugPanel(page: Page) {
|
||||
const debugLocator = page.getByRole('tab', { name: 'Debug', exact: false })
|
||||
const isOpen = (await debugLocator?.getAttribute('aria-selected')) === 'true'
|
||||
const debugLocator = page.getByTestId('debug-pane-button')
|
||||
await expect(debugLocator).toBeVisible()
|
||||
const isOpen = (await debugLocator?.getAttribute('aria-pressed')) === 'true'
|
||||
|
||||
if (!isOpen) {
|
||||
await debugLocator.click()
|
||||
await debugLocator.and(page.locator('[aria-selected="true"]')).waitFor()
|
||||
await expect(debugLocator).toHaveAttribute('aria-pressed', 'true')
|
||||
}
|
||||
}
|
||||
|
||||
async function closeDebugPanel(page: Page) {
|
||||
const debugLocator = page.getByRole('tab', { name: 'Debug', exact: false })
|
||||
const isOpen = (await debugLocator?.getAttribute('aria-selected')) === 'true'
|
||||
const debugLocator = page.getByTestId('debug-pane-button')
|
||||
await expect(debugLocator).toBeVisible()
|
||||
const isOpen = (await debugLocator?.getAttribute('aria-pressed')) === 'true'
|
||||
if (isOpen) {
|
||||
await debugLocator.click()
|
||||
await debugLocator
|
||||
.and(page.locator(':not([aria-selected="true"])'))
|
||||
.waitFor()
|
||||
await expect(debugLocator).not.toHaveAttribute('aria-pressed', 'true')
|
||||
}
|
||||
}
|
||||
|
||||
@ -471,8 +472,11 @@ export const doExport = async (
|
||||
page: Page
|
||||
): Promise<Paths> => {
|
||||
await page.getByRole('button', { name: APP_NAME }).click()
|
||||
await expect(page.getByRole('button', { name: 'Export Part' })).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Export Part' }).click()
|
||||
const exportMenuButton = page.getByRole('button', {
|
||||
name: 'Export current part',
|
||||
})
|
||||
await expect(exportMenuButton).toBeVisible()
|
||||
await exportMenuButton.click()
|
||||
await expect(page.getByTestId('command-bar')).toBeVisible()
|
||||
|
||||
// Go through export via command bar
|
||||
|
@ -77,7 +77,7 @@ describe('ZMA authorized user flows', () => {
|
||||
const menuButton = await $('[data-testid="user-sidebar-toggle"]')
|
||||
await click(menuButton)
|
||||
|
||||
const settingsButton = await $('[data-testid="settings-button"]')
|
||||
const settingsButton = await $('[data-testid="user-settings"]')
|
||||
await click(settingsButton)
|
||||
|
||||
const projectDirInput = await $('[data-testid="project-directory-input"]')
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "untitled-app",
|
||||
"version": "0.24.1",
|
||||
"version": "0.24.4",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.17.0",
|
||||
|
@ -17,6 +17,7 @@ import type {
|
||||
PluginSpec,
|
||||
ViewPlugin,
|
||||
} from '@codemirror/view'
|
||||
import { setDiagnosticsEffect } from '@codemirror/lint'
|
||||
import { EditorView, Tooltip } from '@codemirror/view'
|
||||
|
||||
import type { PublishDiagnosticsParams } from 'vscode-languageserver-protocol'
|
||||
@ -30,7 +31,7 @@ import { URI } from 'vscode-uri'
|
||||
import { LanguageServerClient } from '../client'
|
||||
import { CompletionItemKindMap } from './autocomplete'
|
||||
import { addToken, SemanticToken } from './semantic-tokens'
|
||||
import { deferExecution, posToOffset, formatMarkdownContents } from './util'
|
||||
import { posToOffset, formatMarkdownContents } from './util'
|
||||
import lspAutocompleteExt from './autocomplete'
|
||||
import lspHoverExt from './hover'
|
||||
import lspFormatExt from './format'
|
||||
@ -214,6 +215,21 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
}
|
||||
|
||||
if (!this.client.ready) return
|
||||
|
||||
// TODO(paultag): This is the *wrong* place for this to live.
|
||||
//
|
||||
// We need to clear diagnostics before updating the code, because
|
||||
// if the code shrinks, the errors/diagnostics can go out of range
|
||||
// of the source code, which cause an exception, breaking all the
|
||||
// things.
|
||||
//
|
||||
// We need some sort of clear diagnostics boolean on the editor
|
||||
// and we can drop this.
|
||||
this.view.dispatch({
|
||||
effects: [setDiagnosticsEffect.of([])],
|
||||
annotations: [],
|
||||
})
|
||||
|
||||
try {
|
||||
// Update the state (not the editor) with the new code.
|
||||
this.client.textDocumentDidChange({
|
||||
|
108
src-tauri/Cargo.lock
generated
@ -332,7 +332,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -367,7 +367,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -407,7 +407,7 @@ checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -550,7 +550,7 @@ dependencies = [
|
||||
"proc-macro-crate 3.1.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
"syn_derive",
|
||||
]
|
||||
|
||||
@ -823,7 +823,7 @@ dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1073,7 +1073,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1083,7 +1083,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1107,7 +1107,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim 0.10.0",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1118,7 +1118,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1179,7 +1179,7 @@ checksum = "4078275de501a61ceb9e759d37bdd3d7210e654dbc167ac1a3678ef4435ed57b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@ -1216,7 +1216,7 @@ dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_tokenstream",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1227,7 +1227,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1288,7 +1288,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1320,7 +1320,7 @@ checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1427,7 +1427,7 @@ checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1588,7 +1588,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1704,7 +1704,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1980,7 +1980,7 @@ dependencies = [
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2008,7 +2008,7 @@ dependencies = [
|
||||
"inflections",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2083,7 +2083,7 @@ dependencies = [
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3377,7 +3377,7 @@ dependencies = [
|
||||
"regex",
|
||||
"regex-syntax 0.8.3",
|
||||
"structmeta",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3496,7 +3496,7 @@ dependencies = [
|
||||
"phf_shared 0.11.2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3564,7 +3564,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4438,7 +4438,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_derive_internals",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4558,7 +4558,7 @@ checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4569,7 +4569,7 @@ checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4602,7 +4602,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4623,7 +4623,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4665,7 +4665,7 @@ dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4933,7 +4933,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"structmeta-derive",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4944,7 +4944,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4966,7 +4966,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4999,9 +4999,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.70"
|
||||
version = "2.0.71"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f0209b68b3613b093e0ec905354eccaedcfe83b8cb37cbdeae64026c3064c16"
|
||||
checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -5017,7 +5017,7 @@ dependencies = [
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -5034,7 +5034,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -5251,7 +5251,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
"tauri-utils",
|
||||
"thiserror",
|
||||
"time",
|
||||
@ -5269,7 +5269,7 @@ dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
"tauri-codegen",
|
||||
"tauri-utils",
|
||||
]
|
||||
@ -5627,22 +5627,22 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.61"
|
||||
version = "1.0.62"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
|
||||
checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.61"
|
||||
version = "1.0.62"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
|
||||
checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -5740,7 +5740,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -5940,7 +5940,7 @@ checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -5969,7 +5969,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -6099,7 +6099,7 @@ checksum = "c88cc88fd23b5a04528f3a8436024f20010a16ec18eb23c164b1242f65860130"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
@ -6316,7 +6316,7 @@ dependencies = [
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -6415,7 +6415,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@ -6449,7 +6449,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@ -6590,7 +6590,7 @@ checksum = "ac1345798ecd8122468840bcdf1b95e5dc6d2206c5e4b0eafa078d061f59c9bc"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -6696,7 +6696,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -6707,7 +6707,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -7159,7 +7159,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.70",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -80,5 +80,5 @@
|
||||
}
|
||||
},
|
||||
"productName": "Zoo Modeling App",
|
||||
"version": "0.24.1"
|
||||
"version": "0.24.4"
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ export function App() {
|
||||
}, [projectName, projectPath])
|
||||
|
||||
useHotKeyListener()
|
||||
const { context } = useModelingContext()
|
||||
const { context, state } = useModelingContext()
|
||||
|
||||
const { auth, settings } = useSettingsAuthContext()
|
||||
const token = auth?.context?.token
|
||||
@ -57,7 +57,6 @@ export function App() {
|
||||
const {
|
||||
app: { onboardingStatus },
|
||||
} = settings.context
|
||||
const { state } = useModelingContext()
|
||||
|
||||
useHotkeys('backspace', (e) => {
|
||||
e.preventDefault()
|
||||
|
627
src/Toolbar.tsx
@ -1,4 +1,4 @@
|
||||
import { WheelEvent, useRef, useMemo } from 'react'
|
||||
import { useRef, useMemo, memo } from 'react'
|
||||
import { isCursorInSketchCommandRange } from 'lang/util'
|
||||
import { engineCommandManager, kclManager } from 'lib/singletons'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
@ -12,10 +12,14 @@ import { ActionButtonDropdown } from 'components/ActionButtonDropdown'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import { useAppState } from 'AppState'
|
||||
import { CustomIcon } from 'components/CustomIcon'
|
||||
import {
|
||||
canRectangleTool,
|
||||
isEditingExistingSketch,
|
||||
} from 'machines/modelingMachine'
|
||||
toolbarConfig,
|
||||
ToolbarItem,
|
||||
ToolbarItemCallbackProps,
|
||||
ToolbarItemResolved,
|
||||
ToolbarModeName,
|
||||
} from 'lib/toolbar'
|
||||
|
||||
export function Toolbar({
|
||||
className = '',
|
||||
@ -24,12 +28,14 @@ export function Toolbar({
|
||||
const { state, send, context } = useModelingContext()
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const iconClassName =
|
||||
'group-disabled:text-chalkboard-50 group-enabled:group-hover:!text-primary dark:group-enabled:group-hover:!text-inherit group-pressed:!text-chalkboard-10 group-ui-open:!text-chalkboard-10 dark:group-ui-open:!text-chalkboard-10'
|
||||
const bgClassName =
|
||||
'group-disabled:!bg-transparent group-enabled:group-hover:bg-primary/10 dark:group-enabled:group-hover:bg-primary group-pressed:bg-primary group-ui-open:bg-primary'
|
||||
const buttonClassName =
|
||||
'bg-chalkboard-10 dark:bg-chalkboard-100 enabled:hover:bg-chalkboard-10 dark:enabled:hover:bg-chalkboard-100 pressed:!border-primary ui-open:!border-primary'
|
||||
const pathId = useMemo(() => {
|
||||
'group-disabled:text-chalkboard-50 !text-inherit dark:group-enabled:group-hover:!text-inherit'
|
||||
const bgClassName = '!bg-transparent'
|
||||
const buttonBgClassName =
|
||||
'bg-chalkboard-transparent dark:bg-transparent disabled:bg-transparent dark:disabled:bg-transparent enabled:hover:bg-chalkboard-10 dark:enabled:hover:bg-chalkboard-100 pressed:!bg-primary pressed:enabled:hover:!text-chalkboard-10'
|
||||
const buttonBorderClassName =
|
||||
'!border-transparent hover:!border-chalkboard-20 dark:enabled:hover:!border-primary pressed:!border-primary ui-open:!border-primary'
|
||||
|
||||
const sketchPathId = useMemo(() => {
|
||||
if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast)) {
|
||||
return false
|
||||
}
|
||||
@ -50,361 +56,292 @@ export function Toolbar({
|
||||
isExecuting ||
|
||||
!isStreamReady
|
||||
|
||||
const disableLineButton =
|
||||
state.matches('Sketch.Rectangle tool.Awaiting second corner') ||
|
||||
disableAllButtons
|
||||
useHotkeys(
|
||||
'l',
|
||||
() =>
|
||||
state.matches('Sketch.Line tool')
|
||||
? send('CancelSketch')
|
||||
: send({
|
||||
type: 'change tool',
|
||||
data: 'line',
|
||||
}),
|
||||
{ enabled: !disableLineButton, scopes: ['sketch'] }
|
||||
)
|
||||
const disableTangentialArc =
|
||||
(!isEditingExistingSketch(context) &&
|
||||
!state.matches('Sketch.Tangential arc to')) ||
|
||||
disableAllButtons
|
||||
useHotkeys(
|
||||
'a',
|
||||
() =>
|
||||
state.matches('Sketch.Tangential arc to')
|
||||
? send('CancelSketch')
|
||||
: send({
|
||||
type: 'change tool',
|
||||
data: 'tangentialArc',
|
||||
}),
|
||||
{ enabled: !disableTangentialArc, scopes: ['sketch'] }
|
||||
)
|
||||
const disableRectangle =
|
||||
(!canRectangleTool(context) && !state.matches('Sketch.Rectangle tool')) ||
|
||||
disableAllButtons
|
||||
useHotkeys(
|
||||
'r',
|
||||
() =>
|
||||
state.matches('Sketch.Rectangle tool')
|
||||
? send('CancelSketch')
|
||||
: send({
|
||||
type: 'change tool',
|
||||
data: 'rectangle',
|
||||
}),
|
||||
{ enabled: !disableRectangle, scopes: ['sketch'] }
|
||||
)
|
||||
useHotkeys(
|
||||
's',
|
||||
() =>
|
||||
state.nextEvents.includes('Enter sketch') && pathId
|
||||
? send({ type: 'Enter sketch' })
|
||||
: send({ type: 'Enter sketch', data: { forceNewSketch: true } }),
|
||||
{ enabled: !disableAllButtons, scopes: ['modeling'] }
|
||||
)
|
||||
useHotkeys(
|
||||
'esc',
|
||||
() =>
|
||||
['Sketch no face', 'Sketch.SketchIdle'].some(state.matches)
|
||||
? send('Cancel')
|
||||
: send('CancelSketch'),
|
||||
{ enabled: !disableAllButtons, scopes: ['sketch'] }
|
||||
)
|
||||
useHotkeys(
|
||||
'e',
|
||||
() =>
|
||||
commandBarSend({
|
||||
type: 'Find and select command',
|
||||
data: { name: 'Extrude', groupId: 'modeling' },
|
||||
}),
|
||||
{ enabled: !disableAllButtons, scopes: ['modeling'] }
|
||||
const currentMode =
|
||||
(Object.entries(toolbarConfig).find(([_, mode]) =>
|
||||
mode.check(state)
|
||||
)?.[0] as ToolbarModeName) || 'modeling'
|
||||
|
||||
/** These are the props that will be passed to the callbacks in the toolbar config
|
||||
* They are memoized to prevent unnecessary re-renders,
|
||||
* but they still get a lot of churn from the state machine
|
||||
* so I think there's a lot of room for improvement here
|
||||
*/
|
||||
const configCallbackProps: ToolbarItemCallbackProps = useMemo(
|
||||
() => ({
|
||||
modelingStateMatches: state.matches,
|
||||
modelingSend: send,
|
||||
commandBarSend,
|
||||
sketchPathId,
|
||||
}),
|
||||
[state.matches, send, commandBarSend, sketchPathId]
|
||||
)
|
||||
|
||||
function handleToolbarButtonsWheelEvent(ev: WheelEvent<HTMLSpanElement>) {
|
||||
const span = toolbarButtonsRef.current
|
||||
if (!span) {
|
||||
return
|
||||
/**
|
||||
* Resolve all the callbacks and values for the current mode,
|
||||
* so we don't need to worry about the other modes
|
||||
*/
|
||||
const currentModeItems: (
|
||||
| ToolbarItemResolved
|
||||
| ToolbarItemResolved[]
|
||||
| 'break'
|
||||
)[] = useMemo(() => {
|
||||
return toolbarConfig[currentMode].items.map((maybeIconConfig) => {
|
||||
if (maybeIconConfig === 'break') {
|
||||
return 'break'
|
||||
} else if (Array.isArray(maybeIconConfig)) {
|
||||
return maybeIconConfig.map(resolveItemConfig)
|
||||
} else {
|
||||
return resolveItemConfig(maybeIconConfig)
|
||||
}
|
||||
})
|
||||
|
||||
function resolveItemConfig(
|
||||
maybeIconConfig: ToolbarItem
|
||||
): ToolbarItemResolved {
|
||||
return {
|
||||
...maybeIconConfig,
|
||||
title:
|
||||
typeof maybeIconConfig.title === 'string'
|
||||
? maybeIconConfig.title
|
||||
: maybeIconConfig.title(configCallbackProps),
|
||||
description: maybeIconConfig.description,
|
||||
links: maybeIconConfig.links || [],
|
||||
isActive: maybeIconConfig.isActive?.(state),
|
||||
hotkey:
|
||||
typeof maybeIconConfig.hotkey === 'string'
|
||||
? maybeIconConfig.hotkey
|
||||
: maybeIconConfig.hotkey?.(state),
|
||||
disabled:
|
||||
disableAllButtons ||
|
||||
maybeIconConfig.status !== 'available' ||
|
||||
maybeIconConfig.disabled?.(state) === true,
|
||||
disableHotkey: maybeIconConfig.disableHotkey?.(state),
|
||||
status: maybeIconConfig.status,
|
||||
}
|
||||
}
|
||||
}, [currentMode, disableAllButtons, configCallbackProps])
|
||||
|
||||
span.scrollLeft = span.scrollLeft += ev.deltaY
|
||||
}
|
||||
const nextEvents = useMemo(() => state.nextEvents, [state.nextEvents])
|
||||
const splitMenuItems = useMemo(
|
||||
() =>
|
||||
nextEvents
|
||||
.filter(
|
||||
(eventName) =>
|
||||
eventName.includes('Make segment') ||
|
||||
eventName.includes('Constrain')
|
||||
)
|
||||
.sort((a, b) => {
|
||||
const aisEnabled = nextEvents
|
||||
.filter((event) => state.can(event as any))
|
||||
.includes(a)
|
||||
const bIsEnabled = nextEvents
|
||||
.filter((event) => state.can(event as any))
|
||||
.includes(b)
|
||||
if (aisEnabled && !bIsEnabled) {
|
||||
return -1
|
||||
}
|
||||
if (!aisEnabled && bIsEnabled) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
.map((eventName) => ({
|
||||
label: eventName
|
||||
.replace('Make segment ', '')
|
||||
.replace('Constrain ', ''),
|
||||
onClick: () => send(eventName),
|
||||
disabled:
|
||||
!nextEvents
|
||||
.filter((event) => state.can(event as any))
|
||||
.includes(eventName) || disableAllButtons,
|
||||
})),
|
||||
|
||||
[JSON.stringify(nextEvents), state]
|
||||
)
|
||||
return (
|
||||
<menu className="max-w-full whitespace-nowrap rounded px-1.5 py-0.5 backdrop-blur-sm bg-chalkboard-10/80 dark:bg-chalkboard-110/70 relative">
|
||||
<menu className="max-w-full whitespace-nowrap rounded-b px-2 py-1 bg-chalkboard-10 dark:bg-chalkboard-90 relative border border-chalkboard-20 dark:border-chalkboard-80 border-t-0 shadow-sm">
|
||||
<ul
|
||||
{...props}
|
||||
ref={toolbarButtonsRef}
|
||||
onWheel={handleToolbarButtonsWheelEvent}
|
||||
className={'m-0 py-1 rounded-l-sm flex gap-2 items-center ' + className}
|
||||
style={{ scrollbarWidth: 'thin' }}
|
||||
className={
|
||||
'has-[[aria-expanded=true]]:!pointer-events-none m-0 py-1 rounded-l-sm flex gap-1.5 items-center ' +
|
||||
className
|
||||
}
|
||||
>
|
||||
{nextEvents.includes('Enter sketch') && (
|
||||
<li className="contents">
|
||||
<ActionButton
|
||||
className={buttonClassName}
|
||||
Element="button"
|
||||
onClick={() =>
|
||||
send({ type: 'Enter sketch', data: { forceNewSketch: true } })
|
||||
}
|
||||
iconStart={{
|
||||
icon: 'sketch',
|
||||
iconClassName,
|
||||
bgClassName,
|
||||
}}
|
||||
disabled={disableAllButtons}
|
||||
>
|
||||
<span data-testid="start-sketch">Start Sketch</span>
|
||||
<Tooltip
|
||||
delay={1250}
|
||||
position="bottom"
|
||||
className="!px-2 !text-xs"
|
||||
>
|
||||
Shortcut: S
|
||||
</Tooltip>
|
||||
</ActionButton>
|
||||
</li>
|
||||
)}
|
||||
{nextEvents.includes('Enter sketch') && pathId && (
|
||||
<li className="contents">
|
||||
<ActionButton
|
||||
className={buttonClassName}
|
||||
Element="button"
|
||||
onClick={() => send({ type: 'Enter sketch' })}
|
||||
iconStart={{
|
||||
icon: 'sketch',
|
||||
iconClassName,
|
||||
bgClassName,
|
||||
}}
|
||||
disabled={disableAllButtons}
|
||||
>
|
||||
Edit Sketch
|
||||
<Tooltip
|
||||
delay={1250}
|
||||
position="bottom"
|
||||
className="!px-2 !text-xs"
|
||||
>
|
||||
Shortcut: S
|
||||
</Tooltip>
|
||||
</ActionButton>
|
||||
</li>
|
||||
)}
|
||||
{nextEvents.includes('Cancel') && !state.matches('idle') && (
|
||||
<li className="contents">
|
||||
<ActionButton
|
||||
className={buttonClassName}
|
||||
Element="button"
|
||||
onClick={() => send({ type: 'Cancel' })}
|
||||
iconStart={{
|
||||
icon: 'arrowLeft',
|
||||
iconClassName,
|
||||
bgClassName,
|
||||
}}
|
||||
disabled={disableAllButtons}
|
||||
>
|
||||
Exit Sketch
|
||||
<Tooltip
|
||||
delay={1250}
|
||||
position="bottom"
|
||||
className="!px-2 !text-xs"
|
||||
>
|
||||
Shortcut: Esc
|
||||
</Tooltip>
|
||||
</ActionButton>
|
||||
</li>
|
||||
)}
|
||||
{state.matches('Sketch no face') && (
|
||||
<li className="contents">
|
||||
<div className="mx-2 text-sm">click plane or face to sketch on</div>
|
||||
</li>
|
||||
)}
|
||||
{state.matches('Sketch') && !state.matches('idle') && (
|
||||
<>
|
||||
<li className="contents" key="line-button">
|
||||
<ActionButton
|
||||
className={buttonClassName}
|
||||
{/* A menu item will either be a vertical line break, a button with a dropdown, or a single button */}
|
||||
{currentModeItems.map((maybeIconConfig, i) => {
|
||||
if (maybeIconConfig === 'break') {
|
||||
return (
|
||||
<div
|
||||
key={'break-' + i}
|
||||
className="h-5 w-[1px] block bg-chalkboard-30 dark:bg-chalkboard-80"
|
||||
/>
|
||||
)
|
||||
} else if (Array.isArray(maybeIconConfig)) {
|
||||
return (
|
||||
<ActionButtonDropdown
|
||||
Element="button"
|
||||
onClick={() =>
|
||||
state?.matches('Sketch.Line tool')
|
||||
? send('CancelSketch')
|
||||
: send({
|
||||
type: 'change tool',
|
||||
data: 'line',
|
||||
})
|
||||
key={maybeIconConfig[0].id}
|
||||
data-testid={maybeIconConfig[0].id + '-dropdown'}
|
||||
id={maybeIconConfig[0].id + '-dropdown'}
|
||||
name={maybeIconConfig[0].title}
|
||||
className={
|
||||
'group/wrapper ' +
|
||||
buttonBorderClassName +
|
||||
' !bg-transparent relative group !gap-0'
|
||||
}
|
||||
aria-pressed={state?.matches('Sketch.Line tool')}
|
||||
iconStart={{
|
||||
icon: 'line',
|
||||
iconClassName,
|
||||
bgClassName,
|
||||
}}
|
||||
disabled={disableLineButton}
|
||||
splitMenuItems={maybeIconConfig.map((itemConfig) => ({
|
||||
id: itemConfig.id,
|
||||
label: itemConfig.title,
|
||||
hotkey: itemConfig.hotkey,
|
||||
onClick: () => itemConfig.onClick(configCallbackProps),
|
||||
disabled:
|
||||
disableAllButtons ||
|
||||
itemConfig.status !== 'available' ||
|
||||
itemConfig.disabled === true,
|
||||
status: itemConfig.status,
|
||||
}))}
|
||||
>
|
||||
Line
|
||||
<Tooltip
|
||||
delay={1250}
|
||||
position="bottom"
|
||||
className="!px-2 !text-xs"
|
||||
<ActionButton
|
||||
Element="button"
|
||||
id={maybeIconConfig[0].id}
|
||||
data-testid={maybeIconConfig[0].id}
|
||||
iconStart={{
|
||||
icon: maybeIconConfig[0].icon,
|
||||
className: iconClassName,
|
||||
bgClassName: bgClassName,
|
||||
}}
|
||||
className={
|
||||
'!border-transparent !px-0 pressed:!text-chalkboard-10 pressed:enabled:hovered:!text-chalkboard-10 ' +
|
||||
buttonBgClassName
|
||||
}
|
||||
aria-pressed={maybeIconConfig[0].isActive}
|
||||
disabled={
|
||||
disableAllButtons ||
|
||||
maybeIconConfig[0].status !== 'available' ||
|
||||
maybeIconConfig[0].disabled
|
||||
}
|
||||
name={maybeIconConfig[0].title}
|
||||
aria-description={maybeIconConfig[0].description}
|
||||
onClick={() =>
|
||||
maybeIconConfig[0].onClick(configCallbackProps)
|
||||
}
|
||||
>
|
||||
Shortcut: L
|
||||
</Tooltip>
|
||||
</ActionButton>
|
||||
</li>
|
||||
<li className="contents" key="tangential-arc-button">
|
||||
<ActionButton
|
||||
className={buttonClassName}
|
||||
Element="button"
|
||||
onClick={() =>
|
||||
state.matches('Sketch.Tangential arc to')
|
||||
? send('CancelSketch')
|
||||
: send({
|
||||
type: 'change tool',
|
||||
data: 'tangentialArc',
|
||||
})
|
||||
}
|
||||
aria-pressed={state.matches('Sketch.Tangential arc to')}
|
||||
iconStart={{
|
||||
icon: 'arc',
|
||||
iconClassName,
|
||||
bgClassName,
|
||||
}}
|
||||
disabled={disableTangentialArc}
|
||||
>
|
||||
Tangential Arc
|
||||
<Tooltip
|
||||
delay={1250}
|
||||
position="bottom"
|
||||
className="!px-2 !text-xs"
|
||||
>
|
||||
Shortcut: A
|
||||
</Tooltip>
|
||||
</ActionButton>
|
||||
</li>
|
||||
<li className="contents" key="rectangle-button">
|
||||
<ActionButton
|
||||
className={buttonClassName}
|
||||
Element="button"
|
||||
onClick={() =>
|
||||
state.matches('Sketch.Rectangle tool')
|
||||
? send('CancelSketch')
|
||||
: send({
|
||||
type: 'change tool',
|
||||
data: 'rectangle',
|
||||
})
|
||||
}
|
||||
aria-pressed={state.matches('Sketch.Rectangle tool')}
|
||||
iconStart={{
|
||||
icon: 'rectangle',
|
||||
iconClassName,
|
||||
bgClassName,
|
||||
}}
|
||||
disabled={disableRectangle}
|
||||
title={
|
||||
canRectangleTool(context)
|
||||
? 'Rectangle'
|
||||
: 'Can only be used when a sketch is empty currently'
|
||||
}
|
||||
>
|
||||
Rectangle
|
||||
<Tooltip
|
||||
delay={1250}
|
||||
position="bottom"
|
||||
className="!px-2 !text-xs"
|
||||
>
|
||||
Shortcut: R
|
||||
</Tooltip>
|
||||
</ActionButton>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
{state.matches('Sketch.SketchIdle') &&
|
||||
nextEvents.filter(
|
||||
(eventName) =>
|
||||
eventName.includes('Make segment') ||
|
||||
eventName.includes('Constrain')
|
||||
).length > 0 && (
|
||||
<ActionButtonDropdown
|
||||
splitMenuItems={splitMenuItems}
|
||||
className={buttonClassName}
|
||||
Element="button"
|
||||
iconStart={{
|
||||
icon: 'dimension',
|
||||
iconClassName,
|
||||
bgClassName,
|
||||
}}
|
||||
>
|
||||
Constraints
|
||||
</ActionButtonDropdown>
|
||||
)}
|
||||
{state.matches('idle') && (
|
||||
<li className="contents">
|
||||
<ToolbarItemContents
|
||||
itemConfig={maybeIconConfig[0]}
|
||||
configCallbackProps={configCallbackProps}
|
||||
/>
|
||||
</ActionButton>
|
||||
</ActionButtonDropdown>
|
||||
)
|
||||
}
|
||||
const itemConfig = maybeIconConfig
|
||||
|
||||
return (
|
||||
<ActionButton
|
||||
className={buttonClassName}
|
||||
Element="button"
|
||||
onClick={() =>
|
||||
commandBarSend({
|
||||
type: 'Find and select command',
|
||||
data: { name: 'Extrude', groupId: 'modeling' },
|
||||
})
|
||||
}
|
||||
disabled={!state.can('Extrude') || disableAllButtons}
|
||||
title={
|
||||
state.can('Extrude')
|
||||
? 'extrude'
|
||||
: 'sketches need to be closed, or not already extruded'
|
||||
}
|
||||
key={itemConfig.id}
|
||||
id={itemConfig.id}
|
||||
data-testid={itemConfig.id}
|
||||
iconStart={{
|
||||
icon: 'extrude',
|
||||
iconClassName,
|
||||
bgClassName,
|
||||
icon: itemConfig.icon,
|
||||
className: iconClassName,
|
||||
bgClassName: bgClassName,
|
||||
}}
|
||||
className={
|
||||
'pressed:!text-chalkboard-10 pressed:enabled:hovered:!text-chalkboard-10 ' +
|
||||
buttonBorderClassName +
|
||||
' ' +
|
||||
buttonBgClassName +
|
||||
(!itemConfig.showTitle ? ' !px-0' : '')
|
||||
}
|
||||
name={itemConfig.title}
|
||||
aria-description={itemConfig.description}
|
||||
aria-pressed={itemConfig.isActive}
|
||||
disabled={
|
||||
disableAllButtons ||
|
||||
itemConfig.status !== 'available' ||
|
||||
itemConfig.disabled
|
||||
}
|
||||
onClick={() => itemConfig.onClick(configCallbackProps)}
|
||||
>
|
||||
Extrude
|
||||
<Tooltip
|
||||
delay={1250}
|
||||
position="bottom"
|
||||
className="!px-2 !text-xs"
|
||||
>
|
||||
Shortcut: E
|
||||
</Tooltip>
|
||||
<ToolbarItemContents
|
||||
itemConfig={itemConfig}
|
||||
configCallbackProps={configCallbackProps}
|
||||
/>
|
||||
</ActionButton>
|
||||
</li>
|
||||
)}
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
{state.matches('Sketch no face') && (
|
||||
<div className="absolute top-full left-1/2 -translate-x-1/2 mt-2 py-1 px-2 bg-chalkboard-10 dark:bg-chalkboard-90 border border-chalkboard-20 dark:border-chalkboard-80 rounded shadow-lg">
|
||||
<p className="text-xs">Select a plane or face to start sketching</p>
|
||||
</div>
|
||||
)}
|
||||
</menu>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The single button and dropdown button share content, so we extract it here
|
||||
* It contains a tooltip with the title, description, and links
|
||||
* and a hotkey listener
|
||||
*/
|
||||
const ToolbarItemContents = memo(function ToolbarItemContents({
|
||||
itemConfig,
|
||||
configCallbackProps,
|
||||
}: {
|
||||
itemConfig: ToolbarItemResolved
|
||||
configCallbackProps: ToolbarItemCallbackProps
|
||||
}) {
|
||||
useHotkeys(
|
||||
itemConfig.hotkey || '',
|
||||
() => {
|
||||
itemConfig.onClick(configCallbackProps)
|
||||
},
|
||||
{
|
||||
enabled:
|
||||
itemConfig.status === 'available' &&
|
||||
!!itemConfig.hotkey &&
|
||||
!itemConfig.disabled &&
|
||||
!itemConfig.disableHotkey,
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={!itemConfig.showTitle ? 'sr-only' : ''}>
|
||||
{itemConfig.title}
|
||||
</span>
|
||||
<Tooltip
|
||||
position="bottom"
|
||||
wrapperClassName="!p-4 !pointer-events-auto"
|
||||
contentClassName="!text-left text-wrap !text-xs !p-0 !pb-2 flex gap-2 !max-w-none !w-72 flex-col items-stretch"
|
||||
>
|
||||
<div className="rounded-top flex items-center gap-2 pt-3 pb-2 px-2 bg-chalkboard-20/50 dark:bg-chalkboard-80/50">
|
||||
<span
|
||||
className={`text-sm flex-1 ${
|
||||
itemConfig.status !== 'available'
|
||||
? 'text-chalkboard-70 dark:text-chalkboard-40'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
{itemConfig.title}
|
||||
</span>
|
||||
{itemConfig.status === 'available' && itemConfig.hotkey ? (
|
||||
<kbd className="flex-none hotkey">{itemConfig.hotkey}</kbd>
|
||||
) : itemConfig.status === 'kcl-only' ? (
|
||||
<>
|
||||
<span className="text-wrap font-sans flex-0 text-chalkboard-70 dark:text-chalkboard-40">
|
||||
KCL code only
|
||||
</span>
|
||||
<CustomIcon
|
||||
name="code"
|
||||
className="w-5 h-5 text-chalkboard-70 dark:text-chalkboard-40"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
itemConfig.status === 'unavailable' && (
|
||||
<>
|
||||
<span className="text-wrap font-sans flex-0 text-chalkboard-70 dark:text-chalkboard-40">
|
||||
In development
|
||||
</span>
|
||||
<CustomIcon
|
||||
name="lockClosed"
|
||||
className="w-5 h-5 text-chalkboard-70 dark:text-chalkboard-40"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<p className="px-2 text-ch font-sans">{itemConfig.description}</p>
|
||||
{itemConfig.links.length > 0 && (
|
||||
<>
|
||||
<hr className="border-chalkboard-20 dark:border-chalkboard-80" />
|
||||
<ul className="p-0 px-1 m-0 flex flex-col">
|
||||
{itemConfig.links.map((link) => (
|
||||
<li key={link.label} className="contents">
|
||||
<a
|
||||
href={link.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="flex items-center rounded-sm p-1 no-underline text-inherit hover:bg-primary/10 hover:text-primary dark:hover:bg-chalkboard-70 dark:hover:text-inherit"
|
||||
onClickCapture={(e) =>
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
}
|
||||
>
|
||||
<span className="flex-1">Open {link.label}</span>
|
||||
<CustomIcon name="link" className="w-4 h-4" />
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</Tooltip>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
@ -47,7 +47,6 @@ import {
|
||||
PipeExpression,
|
||||
Program,
|
||||
ProgramMemory,
|
||||
programMemoryInit,
|
||||
recast,
|
||||
SketchGroup,
|
||||
ExtrudeGroup,
|
||||
@ -130,7 +129,7 @@ export const HIDE_HOVER_SEGMENT_LENGTH = 60 // in pixels
|
||||
export class SceneEntities {
|
||||
engineCommandManager: EngineCommandManager
|
||||
scene: Scene
|
||||
sceneProgramMemory: ProgramMemory = { root: {}, return: null }
|
||||
sceneProgramMemory: ProgramMemory = ProgramMemory.empty()
|
||||
activeSegments: { [key: string]: Group } = {}
|
||||
intersectionPlane: Mesh | null = null
|
||||
axisGroup: Group | null = null
|
||||
@ -550,9 +549,9 @@ export class SceneEntities {
|
||||
const variableDeclarationName =
|
||||
_node1.node?.declarations?.[0]?.id?.name || ''
|
||||
|
||||
const sg = kclManager.programMemory.root[
|
||||
const sg = kclManager.programMemory.get(
|
||||
variableDeclarationName
|
||||
] as SketchGroup
|
||||
) as SketchGroup
|
||||
const lastSeg = sg.value.slice(-1)[0] || sg.start
|
||||
|
||||
const index = sg.value.length // because we've added a new segment that's not in the memory yet, no need for `-1`
|
||||
@ -768,9 +767,9 @@ export class SceneEntities {
|
||||
programMemoryOverride,
|
||||
})
|
||||
this.sceneProgramMemory = programMemory
|
||||
const sketchGroup = programMemory.root[
|
||||
const sketchGroup = programMemory.get(
|
||||
variableDeclarationName
|
||||
] as SketchGroup
|
||||
) as SketchGroup
|
||||
const sgPaths = sketchGroup.value
|
||||
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
||||
|
||||
@ -820,9 +819,9 @@ export class SceneEntities {
|
||||
|
||||
// Prepare to update the THREEjs scene
|
||||
this.sceneProgramMemory = programMemory
|
||||
const sketchGroup = programMemory.root[
|
||||
const sketchGroup = programMemory.get(
|
||||
variableDeclarationName
|
||||
] as SketchGroup
|
||||
) as SketchGroup
|
||||
const sgPaths = sketchGroup.value
|
||||
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
||||
|
||||
@ -1081,9 +1080,9 @@ export class SceneEntities {
|
||||
})
|
||||
this.sceneProgramMemory = programMemory
|
||||
|
||||
const maybeSketchGroup = programMemory.root[variableDeclarationName]
|
||||
const maybeSketchGroup = programMemory.get(variableDeclarationName)
|
||||
let sketchGroup = undefined
|
||||
if (maybeSketchGroup.type === 'SketchGroup') {
|
||||
if (maybeSketchGroup?.type === 'SketchGroup') {
|
||||
sketchGroup = maybeSketchGroup
|
||||
} else if ((maybeSketchGroup as ExtrudeGroup).sketchGroup) {
|
||||
sketchGroup = (maybeSketchGroup as ExtrudeGroup).sketchGroup
|
||||
@ -1773,7 +1772,7 @@ function prepareTruncatedMemoryAndAst(
|
||||
if (err(_node)) return _node
|
||||
const variableDeclarationName = _node.node?.declarations?.[0]?.id?.name || ''
|
||||
const lastSeg = (
|
||||
programMemory.root[variableDeclarationName] as SketchGroup
|
||||
programMemory.get(variableDeclarationName) as SketchGroup
|
||||
).value.slice(-1)[0]
|
||||
if (draftSegment) {
|
||||
// truncatedAst needs to setup with another segment at the end
|
||||
@ -1824,33 +1823,27 @@ function prepareTruncatedMemoryAndAst(
|
||||
..._ast,
|
||||
body: [JSON.parse(JSON.stringify(_ast.body[bodyIndex]))],
|
||||
}
|
||||
const programMemoryOverride = programMemoryInit()
|
||||
if (err(programMemoryOverride)) return programMemoryOverride
|
||||
|
||||
// Grab all the TagDeclarators and TagIdentifiers from memory.
|
||||
let start = _node.node.start
|
||||
for (const key in programMemory.root) {
|
||||
const value = programMemory.root[key]
|
||||
if (!('__meta' in value)) {
|
||||
continue
|
||||
}
|
||||
const programMemoryOverride = programMemory.filterVariables(true, (value) => {
|
||||
if (
|
||||
!('__meta' in value) ||
|
||||
value.__meta === undefined ||
|
||||
value.__meta.length === 0 ||
|
||||
value.__meta[0].sourceRange === undefined
|
||||
) {
|
||||
continue
|
||||
return false
|
||||
}
|
||||
|
||||
if (value.__meta[0].sourceRange[0] >= start) {
|
||||
// We only want things before our start point.
|
||||
continue
|
||||
return false
|
||||
}
|
||||
|
||||
if (value.type === 'TagIdentifier') {
|
||||
programMemoryOverride.root[key] = JSON.parse(JSON.stringify(value))
|
||||
}
|
||||
}
|
||||
return value.type === 'TagIdentifier'
|
||||
})
|
||||
if (err(programMemoryOverride)) return programMemoryOverride
|
||||
|
||||
for (let i = 0; i < bodyIndex; i++) {
|
||||
const node = _ast.body[i]
|
||||
@ -1858,12 +1851,15 @@ function prepareTruncatedMemoryAndAst(
|
||||
continue
|
||||
}
|
||||
const name = node.declarations[0].id.name
|
||||
// const memoryItem = kclManager.programMemory.root[name]
|
||||
const memoryItem = programMemory.root[name]
|
||||
const memoryItem = programMemory.get(name)
|
||||
if (!memoryItem) {
|
||||
continue
|
||||
}
|
||||
programMemoryOverride.root[name] = JSON.parse(JSON.stringify(memoryItem))
|
||||
const error = programMemoryOverride.set(
|
||||
name,
|
||||
JSON.parse(JSON.stringify(memoryItem))
|
||||
)
|
||||
if (err(error)) return error
|
||||
}
|
||||
return {
|
||||
truncatedAst,
|
||||
@ -1900,7 +1896,7 @@ export function sketchGroupFromPathToNode({
|
||||
)
|
||||
if (err(_varDec)) return _varDec
|
||||
const varDec = _varDec.node
|
||||
const result = programMemory.root[varDec?.id?.name || '']
|
||||
const result = programMemory.get(varDec?.id?.name || '')
|
||||
if (result?.type === 'ExtrudeGroup') {
|
||||
return result.sketchGroup
|
||||
}
|
||||
@ -2026,13 +2022,17 @@ export async function getFaceDetails(
|
||||
entity_id: entityId,
|
||||
},
|
||||
})
|
||||
const faceInfo: Models['GetSketchModePlane_type'] = (
|
||||
await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'get_sketch_mode_plane' },
|
||||
})
|
||||
)?.data?.data
|
||||
const resp = await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'get_sketch_mode_plane' },
|
||||
})
|
||||
const faceInfo =
|
||||
resp?.success &&
|
||||
resp?.resp.type === 'modeling' &&
|
||||
resp?.resp?.data?.modeling_response?.type === 'get_sketch_mode_plane'
|
||||
? resp?.resp?.data?.modeling_response.data
|
||||
: ({} as Models['GetSketchModePlane_type'])
|
||||
await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
|
@ -1,56 +1,93 @@
|
||||
import { Popover } from '@headlessui/react'
|
||||
import { ActionButton, ActionButtonProps } from './ActionButton'
|
||||
import { ActionButtonProps } from './ActionButton'
|
||||
import { CustomIcon } from './CustomIcon'
|
||||
|
||||
type ActionButtonSplitProps = Omit<ActionButtonProps, 'iconEnd'> & {
|
||||
type ActionButtonSplitProps = ActionButtonProps & { Element: 'button' } & {
|
||||
name?: string
|
||||
splitMenuItems: {
|
||||
id: string
|
||||
label: string
|
||||
shortcut?: string
|
||||
onClick: () => void
|
||||
disabled?: boolean
|
||||
status?: 'available' | 'unavailable' | 'kcl-only'
|
||||
}[]
|
||||
}
|
||||
|
||||
export function ActionButtonDropdown({
|
||||
splitMenuItems,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: ActionButtonSplitProps) {
|
||||
const baseClassNames = `action-button p-0 m-0 group mono text-xs leading-none flex items-center gap-2 rounded-sm border-solid border border-chalkboard-30 hover:border-chalkboard-40 enabled:dark:border-chalkboard-70 dark:hover:border-chalkboard-60 dark:bg-chalkboard-90/50 text-chalkboard-100 dark:text-chalkboard-10`
|
||||
return (
|
||||
<Popover className="relative">
|
||||
<Popover.Button
|
||||
as={ActionButton}
|
||||
className={className}
|
||||
{...props}
|
||||
Element="button"
|
||||
iconEnd={{
|
||||
icon: 'caretDown',
|
||||
className: 'ui-open:rotate-180',
|
||||
bgClassName:
|
||||
'bg-chalkboard-20 dark:bg-chalkboard-80 ui-open:bg-primary ui-open:text-chalkboard-10',
|
||||
}}
|
||||
/>
|
||||
<Popover.Panel
|
||||
as="ul"
|
||||
className="absolute z-20 left-1/2 -translate-x-1/2 top-full mt-1 w-fit max-h-[80vh] overflow-y-auto py-2 flex flex-col gap-1 align-stretch text-inherit dark:text-chalkboard-10 bg-chalkboard-10 dark:bg-chalkboard-100 rounded shadow-lg border border-solid border-chalkboard-30 dark:border-chalkboard-80 text-sm m-0 p-0"
|
||||
>
|
||||
{splitMenuItems.map((item) => (
|
||||
<li className="contents" key={item.label}>
|
||||
<button
|
||||
onClick={item.onClick}
|
||||
className="block px-3 py-1 hover:bg-primary/10 dark:hover:bg-chalkboard-80 border-0 m-0 text-sm w-full rounded-none text-left disabled:!bg-transparent dark:disabled:text-chalkboard-60"
|
||||
disabled={item.disabled}
|
||||
data-testid={item.label}
|
||||
>
|
||||
<span className="capitalize">{item.label}</span>
|
||||
{item.shortcut && (
|
||||
<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">
|
||||
{item.shortcut}
|
||||
</kbd>
|
||||
)}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</Popover.Panel>
|
||||
<Popover className={`${baseClassNames} ${className}`}>
|
||||
{({ close }) => (
|
||||
<>
|
||||
{children}
|
||||
<Popover.Button className="border-transparent dark:border-transparent p-0 m-0 rounded-none !outline-none ui-open:border-primary ui-open:bg-primary">
|
||||
<CustomIcon
|
||||
name="caretDown"
|
||||
className={
|
||||
'w-3.5 h-5 text-chalkboard-70 dark:text-chalkboard-40 rounded-none ' +
|
||||
'ui-open:rotate-180 ui-open:!text-chalkboard-10'
|
||||
}
|
||||
/>
|
||||
<span className="sr-only">
|
||||
{props.name ? props.name + ': ' : ''}open menu
|
||||
</span>
|
||||
</Popover.Button>
|
||||
<Popover.Panel
|
||||
as="ul"
|
||||
className="!pointer-events-auto absolute z-20 left-1/2 -translate-x-1/2 top-full mt-4 w-fit max-w-[280px] max-h-[80vh] overflow-y-auto py-2 flex flex-col align-stretch text-inherit dark:text-chalkboard-10 bg-chalkboard-10 dark:bg-chalkboard-100 rounded shadow-lg border border-solid border-chalkboard-30 dark:border-chalkboard-80 text-sm m-0 p-0"
|
||||
>
|
||||
{splitMenuItems.map((item) => (
|
||||
<li className="contents" key={item.label}>
|
||||
<button
|
||||
onClick={() => {
|
||||
item.onClick()
|
||||
// Close the popover
|
||||
close()
|
||||
}}
|
||||
className="group/button flex items-center gap-6 px-3 py-1 font-sans text-xs hover:bg-primary/10 dark:hover:bg-chalkboard-80 border-0 m-0 w-full rounded-none text-left disabled:!bg-transparent dark:disabled:text-chalkboard-60"
|
||||
disabled={item.disabled}
|
||||
data-testid={'dropdown-' + item.id}
|
||||
>
|
||||
<span className="capitalize flex-grow text-left">
|
||||
{item.label}
|
||||
</span>
|
||||
{item.status === 'unavailable' ? (
|
||||
<div className="flex flex-none items-center gap-1">
|
||||
<span className="text-chalkboard-70 dark:text-chalkboard-40">
|
||||
In development
|
||||
</span>
|
||||
<CustomIcon
|
||||
name="lockClosed"
|
||||
className="w-4 h-4 text-chalkboard-70 dark:text-chalkboard-40"
|
||||
/>
|
||||
</div>
|
||||
) : item.status === 'kcl-only' ? (
|
||||
<div className="flex flex-none items-center gap-1">
|
||||
<span className="text-chalkboard-70 dark:text-chalkboard-40">
|
||||
KCL code only
|
||||
</span>
|
||||
<CustomIcon
|
||||
name="code"
|
||||
className="w-4 h-4 text-chalkboard-70 dark:text-chalkboard-40"
|
||||
/>
|
||||
</div>
|
||||
) : item.shortcut ? (
|
||||
<kbd className="hotkey flex-none group-disabled/button:text-chalkboard-50 dark:group-disabled/button:text-chalkboard-70 group-disabled/button:border-chalkboard-20 dark:group-disabled/button:border-chalkboard-80">
|
||||
{item.shortcut}
|
||||
</kbd>
|
||||
) : null}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</Popover.Panel>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|