Compare commits
24 Commits
try-16-cor
...
v0.22.3
Author | SHA1 | Date | |
---|---|---|---|
1beb6b5186 | |||
17978ab1d7 | |||
a1bcad9dfb | |||
2e7bdf02cf | |||
6f76196b72 | |||
e7af064518 | |||
674d49e2ae | |||
4cb48674c6 | |||
82daec2aff | |||
f1ef9d5200 | |||
dc226d3270 | |||
7bf50d8fe0 | |||
b26764bc9a | |||
1b0c6298d7 | |||
fe9a483726 | |||
bd42ea037b | |||
fdb1b21af3 | |||
630ef316b8 | |||
e322926be9 | |||
a9e61da8b5 | |||
e2a835a437 | |||
c61273085f | |||
a79e365c0f | |||
2386ba24e5 |
40
.github/workflows/cargo-check.yml
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- '**/Cargo.toml'
|
||||
- '**/Cargo.lock'
|
||||
- '**/rust-toolchain.toml'
|
||||
- '**.rs'
|
||||
- .github/workflows/cargo-check.yml
|
||||
pull_request:
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
name: cargo check
|
||||
jobs:
|
||||
cargocheck:
|
||||
name: cargo check
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
dir: ['src/wasm-lib']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install latest rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2.6.1
|
||||
|
||||
- name: Run check
|
||||
run: |
|
||||
cd "${{ matrix.dir }}"
|
||||
# We specifically want to test the disable-println feature
|
||||
# Since it is not enabled by default, we need to specify it
|
||||
# This is used in kcl-lsp
|
||||
cargo check --all --features disable-println --features pyo3
|
6
.github/workflows/cargo-clippy.yml
vendored
@ -9,6 +9,12 @@ on:
|
||||
- '**.rs'
|
||||
- .github/workflows/cargo-clippy.yml
|
||||
pull_request:
|
||||
paths:
|
||||
- '**/Cargo.toml'
|
||||
- '**/Cargo.lock'
|
||||
- '**/rust-toolchain.toml'
|
||||
- '**.rs'
|
||||
- .github/workflows/cargo-clippy.yml
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
10
.github/workflows/playwright.yml
vendored
@ -35,7 +35,7 @@ jobs:
|
||||
|
||||
playwright-ubuntu:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest-16-cores
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
needs: check-rust-changes
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@ -57,7 +57,7 @@ jobs:
|
||||
- name: Download Wasm Cache
|
||||
id: download-wasm
|
||||
if: needs.check-rust-changes.outputs.rust-changed == 'false'
|
||||
uses: dawidd6/action-download-artifact@v5
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
continue-on-error: true
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
@ -133,7 +133,7 @@ jobs:
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
name: playwright-report-ubuntu
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
@ -162,7 +162,7 @@ jobs:
|
||||
- name: Download Wasm Cache
|
||||
id: download-wasm
|
||||
if: needs.check-rust-changes.outputs.rust-changed == 'false'
|
||||
uses: dawidd6/action-download-artifact@v5
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
continue-on-error: true
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
@ -204,6 +204,6 @@ jobs:
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
name: playwright-report-macos
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
|
@ -319,7 +319,7 @@ PS: for the debug panel, the following JSON is useful for snapping the camera
|
||||
|
||||
```
|
||||
yarn install
|
||||
yarn build:wasm
|
||||
yarn build:wasm-dev
|
||||
cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
|
||||
yarn vite build --mode development
|
||||
yarn tauri build --debug -b
|
||||
|
@ -18,7 +18,7 @@ chamfer(data: ChamferData, extrude_group: ExtrudeGroup) -> ExtrudeGroup
|
||||
const width = 20
|
||||
const length = 10
|
||||
const thickness = 1
|
||||
const chamferRadius = 2
|
||||
const chamferLength = 2
|
||||
|
||||
const mountingPlateSketch = startSketchOn("XY")
|
||||
|> startProfileAt([-width / 2, -length / 2], %)
|
||||
@ -29,7 +29,7 @@ const mountingPlateSketch = startSketchOn("XY")
|
||||
|
||||
const mountingPlate = extrude(thickness, mountingPlateSketch)
|
||||
|> chamfer({
|
||||
radius: chamferRadius,
|
||||
length: chamferLength,
|
||||
tags: [
|
||||
getNextAdjacentEdge('edge1', %),
|
||||
getNextAdjacentEdge('edge2', %),
|
||||
@ -46,8 +46,8 @@ const mountingPlate = extrude(thickness, mountingPlateSketch)
|
||||
* `data`: `ChamferData` - Data for chamfers. (REQUIRED)
|
||||
```js
|
||||
{
|
||||
// The radius of the chamfer.
|
||||
radius: number,
|
||||
// The length of the chamfer.
|
||||
length: number,
|
||||
// The tags of the paths you want to chamfer.
|
||||
tags: [uuid |
|
||||
string],
|
||||
|
@ -18155,12 +18155,12 @@
|
||||
"description": "Data for chamfers.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"radius",
|
||||
"length",
|
||||
"tags"
|
||||
],
|
||||
"properties": {
|
||||
"radius": {
|
||||
"description": "The radius of the chamfer.",
|
||||
"length": {
|
||||
"description": "The length of the chamfer.",
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
@ -19702,7 +19702,7 @@
|
||||
"unpublished": false,
|
||||
"deprecated": false,
|
||||
"examples": [
|
||||
"const width = 20\nconst length = 10\nconst thickness = 1\nconst chamferRadius = 2\n\nconst mountingPlateSketch = startSketchOn(\"XY\")\n |> startProfileAt([-width / 2, -length / 2], %)\n |> lineTo([width / 2, -length / 2], %, 'edge1')\n |> lineTo([width / 2, length / 2], %, 'edge2')\n |> lineTo([-width / 2, length / 2], %, 'edge3')\n |> close(%, 'edge4')\n\nconst mountingPlate = extrude(thickness, mountingPlateSketch)\n |> chamfer({\n radius: chamferRadius,\n tags: [\n getNextAdjacentEdge('edge1', %),\n getNextAdjacentEdge('edge2', %),\n getNextAdjacentEdge('edge3', %),\n getNextAdjacentEdge('edge4', %)\n ]\n }, %)"
|
||||
"const width = 20\nconst length = 10\nconst thickness = 1\nconst chamferLength = 2\n\nconst mountingPlateSketch = startSketchOn(\"XY\")\n |> startProfileAt([-width / 2, -length / 2], %)\n |> lineTo([width / 2, -length / 2], %, 'edge1')\n |> lineTo([width / 2, length / 2], %, 'edge2')\n |> lineTo([-width / 2, length / 2], %, 'edge3')\n |> close(%, 'edge4')\n\nconst mountingPlate = extrude(thickness, mountingPlateSketch)\n |> chamfer({\n length: chamferLength,\n tags: [\n getNextAdjacentEdge('edge1', %),\n getNextAdjacentEdge('edge2', %),\n getNextAdjacentEdge('edge3', %),\n getNextAdjacentEdge('edge4', %)\n ]\n }, %)"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -18,12 +18,16 @@ import {
|
||||
TEST_SETTINGS_ONBOARDING_EXPORT,
|
||||
TEST_SETTINGS_ONBOARDING_START,
|
||||
TEST_CODE_GIZMO,
|
||||
TEST_SETTINGS_ONBOARDING_USER_MENU,
|
||||
TEST_SETTINGS_ONBOARDING_PARAMETRIC_MODELING,
|
||||
} from './storageStates'
|
||||
import * as TOML from '@iarna/toml'
|
||||
import { LineInputsType } from 'lang/std/sketchcombos'
|
||||
import { Coords2d } from 'lang/std/sketch'
|
||||
import { KCL_DEFAULT_LENGTH } from 'lib/constants'
|
||||
import { EngineCommand } from 'lang/std/engineConnection'
|
||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||
import { bracket } from 'lib/exampleKcl'
|
||||
|
||||
/*
|
||||
debug helper: unfortunately we do rely on exact coord mouse clicks in a few places
|
||||
@ -594,6 +598,41 @@ test('if you write kcl with lint errors you get lints', async ({ page }) => {
|
||||
await expect(page.locator('.cm-lint-marker-info')).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('if you fixup kcl errors you clear lints', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([3.29, 7.86], %)
|
||||
|> line([2.48, 2.44], %)
|
||||
|> line([2.66, 1.17], %)
|
||||
|> close(%)
|
||||
`
|
||||
)
|
||||
})
|
||||
|
||||
await page.setViewportSize({ width: 1000, height: 500 })
|
||||
await page.goto('/')
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
// check no error to begin with
|
||||
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
||||
|
||||
await u.codeLocator.click()
|
||||
|
||||
await page.getByText(' |> line([2.48, 2.44], %)').click()
|
||||
|
||||
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
||||
await page.keyboard.press('End')
|
||||
await page.keyboard.press('Backspace')
|
||||
|
||||
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
|
||||
await page.keyboard.type(')')
|
||||
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('if you write invalid kcl you get inlined errors', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1000, height: 500 })
|
||||
@ -1252,87 +1291,214 @@ test('Keyboard shortcuts can be viewed through the help menu', async ({
|
||||
).toBeAttached()
|
||||
})
|
||||
|
||||
test('Click through each onboarding step', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
test.describe('Onboarding tests', () => {
|
||||
test('Onboarding code is shown in the editor', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
|
||||
// Override beforeEach test setup
|
||||
await page.addInitScript(
|
||||
async ({ settingsKey, settings }) => {
|
||||
// Give no initial code, so that the onboarding start is shown immediately
|
||||
localStorage.setItem('persistCode', '')
|
||||
localStorage.setItem(settingsKey, settings)
|
||||
},
|
||||
{
|
||||
settingsKey: TEST_SETTINGS_KEY,
|
||||
settings: TOML.stringify({ settings: TEST_SETTINGS_ONBOARDING_START }),
|
||||
// Override beforeEach test setup
|
||||
await page.addInitScript(
|
||||
async ({ settingsKey }) => {
|
||||
// Give no initial code, so that the onboarding start is shown immediately
|
||||
localStorage.removeItem('persistCode')
|
||||
localStorage.removeItem(settingsKey)
|
||||
},
|
||||
{ settingsKey: TEST_SETTINGS_KEY }
|
||||
)
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await page.goto('/')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
// Test that the onboarding pane loaded
|
||||
await expect(page.getByText('Welcome to Modeling App! This')).toBeVisible()
|
||||
|
||||
// *and* that the code is shown in the editor
|
||||
await expect(page.locator('.cm-content')).toContainText('// Shelf Bracket')
|
||||
})
|
||||
|
||||
test('Click through each onboarding step', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
|
||||
// Override beforeEach test setup
|
||||
await page.addInitScript(
|
||||
async ({ settingsKey, settings }) => {
|
||||
// Give no initial code, so that the onboarding start is shown immediately
|
||||
localStorage.setItem('persistCode', '')
|
||||
localStorage.setItem(settingsKey, settings)
|
||||
},
|
||||
{
|
||||
settingsKey: TEST_SETTINGS_KEY,
|
||||
settings: TOML.stringify({ settings: TEST_SETTINGS_ONBOARDING_START }),
|
||||
}
|
||||
)
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 1080 })
|
||||
await page.goto('/')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
// Test that the onboarding pane loaded
|
||||
await expect(page.getByText('Welcome to Modeling App! This')).toBeVisible()
|
||||
|
||||
const nextButton = page.getByTestId('onboarding-next')
|
||||
|
||||
while ((await nextButton.innerText()) !== 'Finish') {
|
||||
await expect(nextButton).toBeVisible()
|
||||
await nextButton.click()
|
||||
}
|
||||
)
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 1080 })
|
||||
await page.goto('/')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
// Test that the onboarding pane loaded
|
||||
await expect(page.getByText('Welcome to Modeling App! This')).toBeVisible()
|
||||
|
||||
const nextButton = page.getByTestId('onboarding-next')
|
||||
|
||||
while ((await nextButton.innerText()) !== 'Finish') {
|
||||
// Finish the onboarding
|
||||
await expect(nextButton).toBeVisible()
|
||||
await nextButton.click()
|
||||
}
|
||||
|
||||
// Finish the onboarding
|
||||
await expect(nextButton).toBeVisible()
|
||||
await nextButton.click()
|
||||
// Test that the onboarding pane is gone
|
||||
await expect(page.getByTestId('onboarding-content')).not.toBeVisible()
|
||||
await expect(page.url()).not.toContain('onboarding')
|
||||
})
|
||||
|
||||
// Test that the onboarding pane is gone
|
||||
await expect(page.getByTestId('onboarding-content')).not.toBeVisible()
|
||||
await expect(page.url()).not.toContain('onboarding')
|
||||
})
|
||||
test('Onboarding redirects and code updating', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
|
||||
test('Onboarding redirects and code updating', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
// Override beforeEach test setup
|
||||
await page.addInitScript(
|
||||
async ({ settingsKey, settings }) => {
|
||||
// Give some initial code, so we can test that it's cleared
|
||||
localStorage.setItem('persistCode', 'const sigmaAllow = 15000')
|
||||
localStorage.setItem(settingsKey, settings)
|
||||
},
|
||||
{
|
||||
settingsKey: TEST_SETTINGS_KEY,
|
||||
settings: TOML.stringify({ settings: TEST_SETTINGS_ONBOARDING_EXPORT }),
|
||||
}
|
||||
)
|
||||
|
||||
// Override beforeEach test setup
|
||||
await page.addInitScript(
|
||||
async ({ settingsKey, settings }) => {
|
||||
// Give some initial code, so we can test that it's cleared
|
||||
localStorage.setItem('persistCode', 'const sigmaAllow = 15000')
|
||||
localStorage.setItem(settingsKey, settings)
|
||||
},
|
||||
{
|
||||
settingsKey: TEST_SETTINGS_KEY,
|
||||
settings: TOML.stringify({ settings: TEST_SETTINGS_ONBOARDING_EXPORT }),
|
||||
}
|
||||
)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await page.goto('/')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await page.goto('/')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
// Test that the redirect happened
|
||||
await expect(page.url().split(':3000').slice(-1)[0]).toBe(
|
||||
`/file/%2Fbrowser%2Fmain.kcl/onboarding/export`
|
||||
)
|
||||
|
||||
// Test that the redirect happened
|
||||
await expect(page.url().split(':3000').slice(-1)[0]).toBe(
|
||||
`/file/%2Fbrowser%2Fmain.kcl/onboarding/export`
|
||||
)
|
||||
// Test that you come back to this page when you refresh
|
||||
await page.reload()
|
||||
await expect(page.url().split(':3000').slice(-1)[0]).toBe(
|
||||
`/file/%2Fbrowser%2Fmain.kcl/onboarding/export`
|
||||
)
|
||||
|
||||
// Test that you come back to this page when you refresh
|
||||
await page.reload()
|
||||
await expect(page.url().split(':3000').slice(-1)[0]).toBe(
|
||||
`/file/%2Fbrowser%2Fmain.kcl/onboarding/export`
|
||||
)
|
||||
// Test that the onboarding pane loaded
|
||||
const title = page.locator('[data-testid="onboarding-content"]')
|
||||
await expect(title).toBeAttached()
|
||||
|
||||
// Test that the onboarding pane loaded
|
||||
const title = page.locator('[data-testid="onboarding-content"]')
|
||||
await expect(title).toBeAttached()
|
||||
// Test that the code changes when you advance to the next step
|
||||
await page.locator('[data-testid="onboarding-next"]').click()
|
||||
await expect(page.locator('.cm-content')).toHaveText('')
|
||||
|
||||
// Test that the code changes when you advance to the next step
|
||||
await page.locator('[data-testid="onboarding-next"]').click()
|
||||
await expect(page.locator('.cm-content')).toHaveText('')
|
||||
// Test that the code is not empty when you click on the next step
|
||||
await page.locator('[data-testid="onboarding-next"]').click()
|
||||
await expect(page.locator('.cm-content')).toHaveText(/.+/)
|
||||
})
|
||||
|
||||
// Test that the code is not empty when you click on the next step
|
||||
await page.locator('[data-testid="onboarding-next"]').click()
|
||||
await expect(page.locator('.cm-content')).toHaveText(/.+/)
|
||||
test('Onboarding code gets reset to demo on Interactive Numbers step', async ({
|
||||
page,
|
||||
}) => {
|
||||
test.skip(
|
||||
process.platform === 'darwin',
|
||||
"Skip on macOS, because Playwright isn't behaving the same as the actual browser"
|
||||
)
|
||||
const u = await getUtils(page)
|
||||
const badCode = `// This is bad code we shouldn't see`
|
||||
// Override beforeEach test setup
|
||||
await page.addInitScript(
|
||||
async ({ settingsKey, settings, badCode }) => {
|
||||
localStorage.setItem('persistCode', badCode)
|
||||
localStorage.setItem(settingsKey, settings)
|
||||
},
|
||||
{
|
||||
settingsKey: TEST_SETTINGS_KEY,
|
||||
settings: TOML.stringify({
|
||||
settings: TEST_SETTINGS_ONBOARDING_PARAMETRIC_MODELING,
|
||||
}),
|
||||
badCode,
|
||||
}
|
||||
)
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 1080 })
|
||||
await page.goto('/')
|
||||
await page.waitForURL('**' + onboardingPaths.PARAMETRIC_MODELING, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
})
|
||||
|
||||
const bracketNoNewLines = bracket.replace(/\n/g, '')
|
||||
|
||||
// Check the code got reset on load
|
||||
await expect(page.locator('#code-pane')).toBeVisible()
|
||||
await expect(u.codeLocator).toHaveText(bracketNoNewLines, {
|
||||
timeout: 10_000,
|
||||
})
|
||||
|
||||
// Mess with the code again
|
||||
await u.codeLocator.selectText()
|
||||
await u.codeLocator.fill(badCode)
|
||||
await expect(u.codeLocator).toHaveText(badCode)
|
||||
|
||||
// Click to the next step
|
||||
await page.locator('[data-testid="onboarding-next"]').click()
|
||||
await page.waitForURL('**' + onboardingPaths.INTERACTIVE_NUMBERS, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
})
|
||||
|
||||
// Check that the code has been reset
|
||||
await expect(u.codeLocator).toHaveText(bracketNoNewLines)
|
||||
})
|
||||
|
||||
test('Avatar text updates depending on image load success', async ({
|
||||
page,
|
||||
}) => {
|
||||
// Override beforeEach test setup
|
||||
await page.addInitScript(
|
||||
async ({ settingsKey, settings }) => {
|
||||
localStorage.setItem(settingsKey, settings)
|
||||
},
|
||||
{
|
||||
settingsKey: TEST_SETTINGS_KEY,
|
||||
settings: TOML.stringify({
|
||||
settings: TEST_SETTINGS_ONBOARDING_USER_MENU,
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 1080 })
|
||||
await page.goto('/')
|
||||
await page.waitForURL('**/file/**', { waitUntil: 'domcontentloaded' })
|
||||
|
||||
// Test that the text in this step is correct
|
||||
const avatarLocator = page.getByTestId('user-sidebar-toggle').locator('img')
|
||||
const onboardingOverlayLocator = page
|
||||
.getByTestId('onboarding-content')
|
||||
.locator('div')
|
||||
.nth(1)
|
||||
|
||||
// Expect the avatar to be visible and for the text to reference it
|
||||
await expect(avatarLocator).toBeVisible()
|
||||
await expect(onboardingOverlayLocator).toBeVisible()
|
||||
await expect(onboardingOverlayLocator).toContainText('your avatar')
|
||||
|
||||
await page.route('https://lh3.googleusercontent.com/**', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 404,
|
||||
contentType: 'text/plain',
|
||||
body: 'Not Found!',
|
||||
})
|
||||
})
|
||||
|
||||
await page.reload({ waitUntil: 'domcontentloaded' })
|
||||
|
||||
// Now expect the text to be different
|
||||
await expect(avatarLocator).not.toBeVisible()
|
||||
await expect(onboardingOverlayLocator).toBeVisible()
|
||||
await expect(onboardingOverlayLocator).toContainText('the menu button')
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Testing selections', () => {
|
||||
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 45 KiB |
@ -1,5 +1,6 @@
|
||||
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
|
||||
import { Themes } from 'lib/theme'
|
||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||
|
||||
export const TEST_SETTINGS_KEY = '/settings.toml'
|
||||
export const TEST_SETTINGS = {
|
||||
@ -22,9 +23,22 @@ export const TEST_SETTINGS = {
|
||||
},
|
||||
} satisfies Partial<SaveSettingsPayload>
|
||||
|
||||
export const TEST_SETTINGS_ONBOARDING_USER_MENU = {
|
||||
...TEST_SETTINGS,
|
||||
app: { ...TEST_SETTINGS.app, onboardingStatus: onboardingPaths.USER_MENU },
|
||||
} satisfies Partial<SaveSettingsPayload>
|
||||
|
||||
export const TEST_SETTINGS_ONBOARDING_EXPORT = {
|
||||
...TEST_SETTINGS,
|
||||
app: { ...TEST_SETTINGS.app, onboardingStatus: '/export' },
|
||||
app: { ...TEST_SETTINGS.app, onboardingStatus: onboardingPaths.EXPORT },
|
||||
} satisfies Partial<SaveSettingsPayload>
|
||||
|
||||
export const TEST_SETTINGS_ONBOARDING_PARAMETRIC_MODELING = {
|
||||
...TEST_SETTINGS,
|
||||
app: {
|
||||
...TEST_SETTINGS.app,
|
||||
onboardingStatus: onboardingPaths.PARAMETRIC_MODELING,
|
||||
},
|
||||
} satisfies Partial<SaveSettingsPayload>
|
||||
|
||||
export const TEST_SETTINGS_ONBOARDING_START = {
|
||||
|
@ -2,6 +2,7 @@ import { browser, $, expect } from '@wdio/globals'
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import os from 'os'
|
||||
import { click, setDatasetValue } from '../utils'
|
||||
|
||||
const isWin32 = os.platform() === 'win32'
|
||||
const documentsDir = path.join(os.homedir(), 'Documents')
|
||||
@ -15,25 +16,8 @@ const newProjectDir = path.join(documentsDir, 'a-different-directory')
|
||||
const tmp = process.env.TEMP || '/tmp'
|
||||
const userCodeDir = path.join(tmp, 'kittycad_user_code')
|
||||
|
||||
async function click(element: WebdriverIO.Element): Promise<void> {
|
||||
// Workaround for .click(), see https://github.com/tauri-apps/tauri/issues/6541
|
||||
await element.waitForClickable()
|
||||
await browser.execute('arguments[0].click();', element)
|
||||
}
|
||||
|
||||
/* Shoutout to @Sheap on Github for a great workaround utility:
|
||||
* https://github.com/tauri-apps/tauri/issues/6541#issue-1638944060
|
||||
*/
|
||||
async function setDatasetValue(
|
||||
field: WebdriverIO.Element,
|
||||
property: string,
|
||||
value: string
|
||||
) {
|
||||
await browser.execute(`arguments[0].dataset.${property} = "${value}"`, field)
|
||||
}
|
||||
|
||||
describe('ZMA (Tauri)', () => {
|
||||
it('opens the auth page and signs in', async () => {
|
||||
describe('ZMA sign in flow', () => {
|
||||
before(async () => {
|
||||
// Clean up filesystem from previous tests
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
await fs.rm(defaultProjectDir, { force: true, recursive: true })
|
||||
@ -42,7 +26,9 @@ describe('ZMA (Tauri)', () => {
|
||||
await fs.rm(userSettingsDir, { force: true, recursive: true })
|
||||
await fs.mkdir(defaultProjectDir, { recursive: true })
|
||||
await fs.mkdir(newProjectDir, { recursive: true })
|
||||
})
|
||||
|
||||
it('opens the auth page and signs in', async () => {
|
||||
const signInButton = await $('[data-testid="sign-in-button"]')
|
||||
expect(await signInButton.getText()).toEqual('Sign in')
|
||||
|
||||
@ -82,6 +68,10 @@ describe('ZMA (Tauri)', () => {
|
||||
const newFileButton = await $('[data-testid="home-new-file"]')
|
||||
expect(await newFileButton.getText()).toEqual('New project')
|
||||
})
|
||||
})
|
||||
|
||||
describe('ZMA authorized user flows', () => {
|
||||
// Note: each flow below is intended to start *and* end from the home page
|
||||
|
||||
it('opens the settings page, checks filesystem settings, and closes the settings page', async () => {
|
||||
const menuButton = await $('[data-testid="user-sidebar-toggle"]')
|
||||
@ -150,7 +140,9 @@ describe('ZMA (Tauri)', () => {
|
||||
const base = isWin32 ? 'http://tauri.localhost' : 'tauri://localhost'
|
||||
await browser.execute(`window.location.href = "${base}/home"`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('ZMA sign out flow', () => {
|
||||
it('signs out', async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
const menuButton = await $('[data-testid="user-sidebar-toggle"]')
|
18
e2e/tauri/utils.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { browser } from '@wdio/globals'
|
||||
|
||||
export async function click(element: WebdriverIO.Element): Promise<void> {
|
||||
// Workaround for .click(), see https://github.com/tauri-apps/tauri/issues/6541
|
||||
await element.waitForClickable()
|
||||
await browser.execute('arguments[0].click();', element)
|
||||
}
|
||||
|
||||
/* Shoutout to @Sheap on Github for a great workaround utility:
|
||||
* https://github.com/tauri-apps/tauri/issues/6541#issue-1638944060
|
||||
*/
|
||||
export async function setDatasetValue(
|
||||
field: WebdriverIO.Element,
|
||||
property: string,
|
||||
value: string
|
||||
) {
|
||||
await browser.execute(`arguments[0].dataset.${property} = "${value}"`, field)
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "untitled-app",
|
||||
"version": "0.22.2",
|
||||
"version": "0.22.3",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.16.0",
|
||||
|
254
src-tauri/Cargo.lock
generated
@ -2358,124 +2358,6 @@ dependencies = [
|
||||
"png",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_locid"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"litemap",
|
||||
"tinystr",
|
||||
"writeable",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_locid_transform"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_locid",
|
||||
"icu_locid_transform_data",
|
||||
"icu_provider",
|
||||
"tinystr",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_locid_transform_data"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e"
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_collections",
|
||||
"icu_normalizer_data",
|
||||
"icu_properties",
|
||||
"icu_provider",
|
||||
"smallvec",
|
||||
"utf16_iter",
|
||||
"utf8_iter",
|
||||
"write16",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer_data"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516"
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f8ac670d7422d7f76b32e17a5db556510825b29ec9154f235977c9caba61036"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_collections",
|
||||
"icu_locid_transform",
|
||||
"icu_properties_data",
|
||||
"icu_provider",
|
||||
"tinystr",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties_data"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569"
|
||||
|
||||
[[package]]
|
||||
name = "icu_provider"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_locid",
|
||||
"icu_provider_macros",
|
||||
"stable_deref_trait",
|
||||
"tinystr",
|
||||
"writeable",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_provider_macros"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
@ -2502,18 +2384,6 @@ dependencies = [
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4716a3a0933a1d01c2f72450e89596eb51dd34ef3c211ccd875acdf1f8fe47ed"
|
||||
dependencies = [
|
||||
"icu_normalizer",
|
||||
"icu_properties",
|
||||
"smallvec",
|
||||
"utf8_iter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.3"
|
||||
@ -2888,12 +2758,6 @@ version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.12"
|
||||
@ -5953,16 +5817,6 @@ dependencies = [
|
||||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.6.0"
|
||||
@ -6322,10 +6176,12 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "ts-rs"
|
||||
version = "8.1.0"
|
||||
source = "git+https://github.com/Aleph-Alpha/ts-rs#be0349d5fb07a8ccab713887a61e90e3bc773c7a"
|
||||
version = "9.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e2dcf58e612adda9a83800731e8e4aba04d8a302b9029617b0b6e4b021d5357"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"ts-rs-macros",
|
||||
"url",
|
||||
@ -6334,8 +6190,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ts-rs-macros"
|
||||
version = "8.1.0"
|
||||
source = "git+https://github.com/Aleph-Alpha/ts-rs#be0349d5fb07a8ccab713887a61e90e3bc773c7a"
|
||||
version = "9.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cbdee324e50a7402416d9c25270d3df4241ed528af5d36dda18b6f219551c577"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -6471,12 +6328,12 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.1"
|
||||
version = "2.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7c25da092f0a868cdf09e8674cd3b7ef3a7d92a24253e663a2fb85e2496de56"
|
||||
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna 1.0.0",
|
||||
"idna 0.5.0",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
]
|
||||
@ -6500,24 +6357,12 @@ version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "utf16_iter"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
|
||||
|
||||
[[package]]
|
||||
name = "utf8-width"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3"
|
||||
|
||||
[[package]]
|
||||
name = "utf8_iter"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
@ -7272,18 +7117,6 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "write16"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
|
||||
|
||||
[[package]]
|
||||
name = "wry"
|
||||
version = "0.40.1"
|
||||
@ -7386,30 +7219,6 @@ dependencies = [
|
||||
"linked-hash-map",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"stable_deref_trait",
|
||||
"yoke-derive",
|
||||
"zerofrom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke-derive"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.66",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus"
|
||||
version = "4.3.0"
|
||||
@ -7487,55 +7296,12 @@ dependencies = [
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55"
|
||||
dependencies = [
|
||||
"zerofrom-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom-derive"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.66",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
|
||||
|
||||
[[package]]
|
||||
name = "zerovec"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb2cc8827d6c0994478a15c53f374f46fbd41bea663d809b14744bc42e6b109c"
|
||||
dependencies = [
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerovec-derive"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97cf56601ee5052b4417d90c8755c6683473c926039908196cf35d99f893ebe7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "2.1.3"
|
||||
|
@ -75,5 +75,5 @@
|
||||
}
|
||||
},
|
||||
"productName": "Zoo Modeling App",
|
||||
"version": "0.22.2"
|
||||
"version": "0.22.3"
|
||||
}
|
||||
|
@ -32,9 +32,7 @@ import {
|
||||
SKETCH_GROUP_SEGMENTS,
|
||||
SKETCH_LAYER,
|
||||
X_AXIS,
|
||||
XZ_PLANE,
|
||||
Y_AXIS,
|
||||
YZ_PLANE,
|
||||
} from './sceneInfra'
|
||||
import { isQuaternionVertical, quaternionFromUpNForward } from './helpers'
|
||||
import {
|
||||
|
@ -47,7 +47,6 @@ import {
|
||||
TANGENTIAL_ARC_TO_SEGMENT,
|
||||
getParentGroup,
|
||||
getSketchOrientationDetails,
|
||||
getSketchQuaternion,
|
||||
} from 'clientSideScene/sceneEntities'
|
||||
import {
|
||||
moveValueIntoNewVariablePath,
|
||||
@ -122,7 +121,24 @@ export const ModelingMachineProvider = ({
|
||||
htmlRef,
|
||||
token
|
||||
)
|
||||
useHotkeyWrapper(['meta + shift + .'], () => coreDump(coreDumpManager, true))
|
||||
useHotkeyWrapper(['meta + shift + .'], () => {
|
||||
console.warn('CoreDump: Initializing core dump')
|
||||
toast.promise(
|
||||
coreDump(coreDumpManager, true),
|
||||
{
|
||||
loading: 'Starting core dump...',
|
||||
success: 'Core dump completed successfully',
|
||||
error: 'Error while exporting core dump',
|
||||
},
|
||||
{
|
||||
success: {
|
||||
// Note: this extended duration is especially important for Playwright e2e testing
|
||||
// default duration is 2000 - https://react-hot-toast.com/docs/toast#default-durations
|
||||
duration: 6000,
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
// Settings machine setup
|
||||
// const retrievedSettings = useRef(
|
||||
|
@ -2,7 +2,7 @@
|
||||
@apply relative z-0 rounded-r max-w-full h-full flex-1;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
@apply bg-chalkboard-10/50 backdrop-blur-sm border border-chalkboard-20;
|
||||
@apply bg-chalkboard-10/50 focus-within:bg-chalkboard-10/90 backdrop-blur-sm border border-chalkboard-20;
|
||||
scroll-margin-block-start: 41px;
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
}
|
||||
|
||||
:global(.dark) .panel {
|
||||
@apply bg-chalkboard-100/50 backdrop-blur-[3px] border-chalkboard-80;
|
||||
@apply bg-chalkboard-100/50 focus-within:bg-chalkboard-100/90 backdrop-blur-[3px] border-chalkboard-80;
|
||||
}
|
||||
|
||||
.header {
|
||||
|
@ -46,7 +46,11 @@ export const ModelingPane = ({
|
||||
data-testid={detailsTestId}
|
||||
id={id}
|
||||
className={
|
||||
pointerEventsCssClass + styles.panel + ' group ' + (className || '')
|
||||
'group-focus-within:border-primary dark:group-focus-within:border-chalkboard-50 ' +
|
||||
pointerEventsCssClass +
|
||||
styles.panel +
|
||||
' group ' +
|
||||
(className || '')
|
||||
}
|
||||
>
|
||||
<ModelingPaneHeader title={title} Menu={Menu} />
|
||||
|
@ -123,70 +123,73 @@ function ModelingSidebarSection({
|
||||
}, [showDebugPanel.current, togglePane, openPanes])
|
||||
|
||||
return (
|
||||
<Tab.Group
|
||||
vertical
|
||||
selectedIndex={
|
||||
currentPane === 'none' ? 0 : paneIds.indexOf(currentPane) + 1
|
||||
}
|
||||
onChange={(index) => {
|
||||
const newPane = index === 0 ? 'none' : paneIds[index - 1]
|
||||
togglePane(newPane)
|
||||
}}
|
||||
>
|
||||
<Tab.List
|
||||
className={
|
||||
'pointer-events-auto ' +
|
||||
(alignButtons === 'start'
|
||||
? 'justify-start self-start'
|
||||
: 'justify-end self-end') +
|
||||
(currentPane === 'none'
|
||||
? ' rounded-r focus-within:!border-primary/50'
|
||||
: ' border-r-0') +
|
||||
' p-2 col-start-1 col-span-1 h-fit w-fit flex flex-col items-start gap-2 bg-chalkboard-10 border border-solid border-chalkboard-20 dark:bg-chalkboard-90 dark:border-chalkboard-80 ' +
|
||||
(openPanes.length === 1 && currentPane === 'none' ? 'pr-0.5' : '')
|
||||
<div className="group contents">
|
||||
<Tab.Group
|
||||
vertical
|
||||
selectedIndex={
|
||||
currentPane === 'none' ? 0 : paneIds.indexOf(currentPane) + 1
|
||||
}
|
||||
onChange={(index) => {
|
||||
const newPane = index === 0 ? 'none' : paneIds[index - 1]
|
||||
togglePane(newPane)
|
||||
}}
|
||||
>
|
||||
<Tab key="none" className="sr-only">
|
||||
No panes open
|
||||
</Tab>
|
||||
{filteredPanes.map((pane) => (
|
||||
<ModelingPaneButton
|
||||
key={pane.id}
|
||||
paneConfig={pane}
|
||||
currentPane={currentPane}
|
||||
togglePane={() => togglePane(pane.id)}
|
||||
/>
|
||||
))}
|
||||
</Tab.List>
|
||||
<Tab.Panels
|
||||
as="article"
|
||||
className={
|
||||
'col-start-2 col-span-1 ' +
|
||||
(openPanes.length === 1
|
||||
? currentPane !== 'none'
|
||||
? `row-start-1 row-end-3`
|
||||
: `hidden`
|
||||
: ``)
|
||||
}
|
||||
>
|
||||
<Tab.Panel key="none" />
|
||||
{filteredPanes.map((pane) => (
|
||||
<Tab.Panel key={pane.id} className="h-full">
|
||||
<ModelingPane
|
||||
id={`${pane.id}-pane`}
|
||||
title={pane.title}
|
||||
Menu={pane.Menu}
|
||||
>
|
||||
{pane.Content instanceof Function ? (
|
||||
<pane.Content />
|
||||
) : (
|
||||
pane.Content
|
||||
)}
|
||||
</ModelingPane>
|
||||
</Tab.Panel>
|
||||
))}
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
<Tab.List
|
||||
className={
|
||||
'pointer-events-auto ' +
|
||||
(alignButtons === 'start'
|
||||
? 'justify-start self-start'
|
||||
: 'justify-end self-end') +
|
||||
(currentPane === 'none'
|
||||
? ' rounded-r focus-within:!border-primary/50'
|
||||
: ' border-r-0') +
|
||||
' p-2 col-start-1 col-span-1 h-fit w-fit flex flex-col items-start gap-2 ' +
|
||||
'bg-chalkboard-10 border border-solid border-chalkboard-20 dark:bg-chalkboard-90 dark:border-chalkboard-80 group-focus-within:border-primary dark:group-focus-within:border-chalkboard-50 ' +
|
||||
(openPanes.length === 1 && currentPane === 'none' ? 'pr-0.5' : '')
|
||||
}
|
||||
>
|
||||
<Tab key="none" className="sr-only">
|
||||
No panes open
|
||||
</Tab>
|
||||
{filteredPanes.map((pane) => (
|
||||
<ModelingPaneButton
|
||||
key={pane.id}
|
||||
paneConfig={pane}
|
||||
currentPane={currentPane}
|
||||
togglePane={() => togglePane(pane.id)}
|
||||
/>
|
||||
))}
|
||||
</Tab.List>
|
||||
<Tab.Panels
|
||||
as="article"
|
||||
className={
|
||||
'col-start-2 col-span-1 ' +
|
||||
(openPanes.length === 1
|
||||
? currentPane !== 'none'
|
||||
? `row-start-1 row-end-3`
|
||||
: `hidden`
|
||||
: ``)
|
||||
}
|
||||
>
|
||||
<Tab.Panel key="none" />
|
||||
{filteredPanes.map((pane) => (
|
||||
<Tab.Panel key={pane.id} className="h-full">
|
||||
<ModelingPane
|
||||
id={`${pane.id}-pane`}
|
||||
title={pane.title}
|
||||
Menu={pane.Menu}
|
||||
>
|
||||
{pane.Content instanceof Function ? (
|
||||
<pane.Content />
|
||||
) : (
|
||||
pane.Content
|
||||
)}
|
||||
</ModelingPane>
|
||||
</Tab.Panel>
|
||||
))}
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
||||
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
|
||||
import { APP_NAME } from 'lib/constants'
|
||||
import { CommandBarProvider } from './CommandBar/CommandBarProvider'
|
||||
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
||||
|
||||
|
@ -39,7 +39,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
|
||||
<Popover className="relative">
|
||||
{user?.image && !imageLoadFailed ? (
|
||||
<Popover.Button
|
||||
className="border-0 rounded-full w-fit min-w-max p-0 group"
|
||||
className="relative border-0 rounded-full w-fit min-w-max p-0 group"
|
||||
data-testid="user-sidebar-toggle"
|
||||
>
|
||||
<div className="rounded-full border overflow-hidden">
|
||||
@ -51,6 +51,9 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
|
||||
onError={() => setImageLoadFailed(true)}
|
||||
/>
|
||||
</div>
|
||||
<Tooltip position="bottom-right" delay={1000}>
|
||||
User menu
|
||||
</Tooltip>
|
||||
</Popover.Button>
|
||||
) : (
|
||||
<ActionButton
|
||||
@ -59,7 +62,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
|
||||
className="border-transparent !px-0"
|
||||
data-testid="user-sidebar-toggle"
|
||||
>
|
||||
<Tooltip position="left" delay={1000}>
|
||||
<Tooltip position="bottom-right" delay={1000}>
|
||||
User menu
|
||||
</Tooltip>
|
||||
</ActionButton>
|
||||
|
@ -147,15 +147,33 @@ code {
|
||||
|
||||
#code-mirror-override .cm-activeLine,
|
||||
#code-mirror-override .cm-activeLineGutter {
|
||||
@apply bg-primary/10;
|
||||
@apply bg-primary/5;
|
||||
}
|
||||
|
||||
.dark #code-mirror-override .cm-activeLine,
|
||||
.dark #code-mirror-override .cm-activeLineGutter {
|
||||
@apply bg-primary/20;
|
||||
@apply bg-chalkboard-70/20;
|
||||
mix-blend-mode: lighten;
|
||||
}
|
||||
|
||||
#code-mirror-override .cm-focused .cm-activeLine,
|
||||
#code-mirror-override .cm-focused .cm-activeLineGutter {
|
||||
@apply bg-primary/10;
|
||||
}
|
||||
|
||||
.dark #code-mirror-override .cm-focused .cm-activeLine,
|
||||
.dark #code-mirror-override .cm-focused .cm-activeLineGutter {
|
||||
@apply bg-chalkboard-70/40;
|
||||
}
|
||||
|
||||
#code-mirror-override .cm-matchingBracket {
|
||||
@apply bg-primary/20;
|
||||
}
|
||||
|
||||
.dark #code-mirror-override .cm-matchingBracket {
|
||||
@apply bg-chalkboard-70/60;
|
||||
}
|
||||
|
||||
#code-mirror-override .cm-gutters {
|
||||
@apply bg-chalkboard-10/30;
|
||||
}
|
||||
@ -171,22 +189,8 @@ code {
|
||||
@apply caret-chalkboard-10;
|
||||
}
|
||||
|
||||
#code-mirror-override .cm-focused .cm-cursor {
|
||||
width: 0px;
|
||||
}
|
||||
#code-mirror-override .cm-cursor {
|
||||
display: block;
|
||||
width: 1ch;
|
||||
@apply mix-blend-multiply;
|
||||
@apply border-l-primary;
|
||||
}
|
||||
|
||||
.dark #code-mirror-override .cm-cursor {
|
||||
@apply border-l-chalkboard-10;
|
||||
}
|
||||
|
||||
#code-mirror-override.blink .cm-cursor {
|
||||
animation: blink 1200ms ease-out infinite;
|
||||
#code-mirror-override .cm-focused {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
|
@ -41,7 +41,10 @@ export class KclManager {
|
||||
engineCommandManager: EngineCommandManager
|
||||
private _defferer = deferExecution((code: string) => {
|
||||
const ast = this.safeParse(code)
|
||||
if (!ast) return
|
||||
if (!ast) {
|
||||
this.clearAst()
|
||||
return
|
||||
}
|
||||
try {
|
||||
const fmtAndStringify = (ast: Program) =>
|
||||
JSON.stringify(parse(recast(ast)))
|
||||
@ -89,7 +92,6 @@ export class KclManager {
|
||||
return this._kclErrors
|
||||
}
|
||||
set kclErrors(kclErrors) {
|
||||
console.log('[lsp] not lsp, actually typescript: ', kclErrors)
|
||||
this._kclErrors = kclErrors
|
||||
let diagnostics = kclErrorsToDiagnostics(kclErrors)
|
||||
editorManager.addDiagnostics(diagnostics)
|
||||
@ -146,6 +148,18 @@ export class KclManager {
|
||||
this._executeCallback = callback
|
||||
}
|
||||
|
||||
clearAst() {
|
||||
this._ast = {
|
||||
body: [],
|
||||
start: 0,
|
||||
end: 0,
|
||||
nonCodeMeta: {
|
||||
nonCodeNodes: {},
|
||||
start: [],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
safeParse(code: string): Program | null {
|
||||
try {
|
||||
const ast = parse(code)
|
||||
@ -293,14 +307,20 @@ export class KclManager {
|
||||
if (!force) return this._defferer(codeManager.code)
|
||||
|
||||
const ast = this.safeParse(codeManager.code)
|
||||
if (!ast) return
|
||||
if (!ast) {
|
||||
this.clearAst()
|
||||
return
|
||||
}
|
||||
this.ast = { ...ast }
|
||||
return this.executeAst(ast, zoomToFit)
|
||||
}
|
||||
format() {
|
||||
const originalCode = codeManager.code
|
||||
const ast = this.safeParse(originalCode)
|
||||
if (!ast) return
|
||||
if (!ast) {
|
||||
this.clearAst()
|
||||
return
|
||||
}
|
||||
const code = recast(ast)
|
||||
if (originalCode === code) return
|
||||
|
||||
|
@ -22,7 +22,7 @@ export default class CodeManager {
|
||||
return
|
||||
}
|
||||
|
||||
const storedCode = safeLSGetItem(PERSIST_CODE_TOKEN) || ''
|
||||
const storedCode = safeLSGetItem(PERSIST_CODE_TOKEN)
|
||||
// TODO #819 remove zustand persistence logic in a few months
|
||||
// short term migration, shouldn't make a difference for tauri app users
|
||||
// anyway since that's filesystem based.
|
||||
@ -68,7 +68,9 @@ export default class CodeManager {
|
||||
this._currentFilePath = path
|
||||
}
|
||||
|
||||
// This updates the code state and calls the updateState function.
|
||||
/**
|
||||
* This updates the code state and calls the updateState function.
|
||||
*/
|
||||
updateCodeState(code: string): void {
|
||||
if (this._code !== code) {
|
||||
this.code = code
|
||||
@ -76,7 +78,9 @@ export default class CodeManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Update the code in the editor.
|
||||
/**
|
||||
* Update the code in the editor.
|
||||
*/
|
||||
updateCodeEditor(code: string): void {
|
||||
this.code = code
|
||||
if (editorManager.editorView) {
|
||||
@ -90,7 +94,9 @@ export default class CodeManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Update the code, state, and the code the code mirror editor sees.
|
||||
/**
|
||||
* Update the code, state, and the code the code mirror editor sees.
|
||||
*/
|
||||
updateCodeStateEditor(code: string): void {
|
||||
if (this._code !== code) {
|
||||
this.code = code
|
||||
|
@ -58,6 +58,9 @@ function isHighlightSetEntity_type(
|
||||
|
||||
type WebSocketResponse = Models['WebSocketResponse_type']
|
||||
type OkWebSocketResponseData = Models['OkWebSocketResponseData_type']
|
||||
type BatchResponseMap = {
|
||||
[key: string]: Models['BatchResponse_type']
|
||||
}
|
||||
|
||||
type ResultCommand = CommandInfo & {
|
||||
type: 'result'
|
||||
@ -1316,7 +1319,8 @@ export class EngineCommandManager extends EventTarget {
|
||||
)
|
||||
if (
|
||||
message.success &&
|
||||
message.resp.type === 'modeling' &&
|
||||
(message.resp.type === 'modeling' ||
|
||||
message.resp.type === 'modeling_batch') &&
|
||||
message.request_id
|
||||
) {
|
||||
this.handleModelingCommand(
|
||||
@ -1380,19 +1384,60 @@ export class EngineCommandManager extends EventTarget {
|
||||
id: string,
|
||||
raw: WebSocketResponse
|
||||
) {
|
||||
if (message.type !== 'modeling') {
|
||||
if (!(message.type === 'modeling' || message.type === 'modeling_batch')) {
|
||||
return
|
||||
}
|
||||
const modelingResponse = message.data.modeling_response
|
||||
|
||||
const command = this.artifactMap[id]
|
||||
let modelingResponse: Models['OkModelingCmdResponse_type'] = {
|
||||
type: 'empty',
|
||||
}
|
||||
if ('modeling_response' in message.data) {
|
||||
modelingResponse = message.data.modeling_response
|
||||
}
|
||||
if (
|
||||
command?.type === 'pending' &&
|
||||
command.commandType === 'batch' &&
|
||||
command?.additionalData?.type === 'batch-ids'
|
||||
) {
|
||||
command.additionalData.ids.forEach((id) => {
|
||||
this.handleModelingCommand(message, id, raw)
|
||||
})
|
||||
if ('responses' in message.data) {
|
||||
const batchResponse = message.data.responses as BatchResponseMap
|
||||
// Iterate over the map of responses.
|
||||
Object.entries(batchResponse).forEach(([key, response]) => {
|
||||
// If the response is a success, we resolve the promise.
|
||||
if ('response' in response && response.response) {
|
||||
this.handleModelingCommand(
|
||||
{
|
||||
type: 'modeling',
|
||||
data: {
|
||||
modeling_response: response.response,
|
||||
},
|
||||
},
|
||||
key,
|
||||
{
|
||||
request_id: key,
|
||||
resp: {
|
||||
type: 'modeling',
|
||||
data: {
|
||||
modeling_response: response.response,
|
||||
},
|
||||
},
|
||||
success: true,
|
||||
}
|
||||
)
|
||||
} else if ('errors' in response) {
|
||||
this.handleFailedModelingCommand(key, {
|
||||
request_id: key,
|
||||
success: false,
|
||||
errors: response.errors,
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
command.additionalData.ids.forEach((id) => {
|
||||
this.handleModelingCommand(message, id, raw)
|
||||
})
|
||||
}
|
||||
// batch artifact is just a container, we don't need to keep it
|
||||
// once we process all the commands inside it
|
||||
const resolve = command.resolve
|
||||
@ -1401,7 +1446,6 @@ export class EngineCommandManager extends EventTarget {
|
||||
id,
|
||||
commandType: command.commandType,
|
||||
range: command.range,
|
||||
data: modelingResponse,
|
||||
raw,
|
||||
})
|
||||
return
|
||||
@ -1733,7 +1777,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
command: EngineCommand
|
||||
ast: Program
|
||||
idToRangeMap?: { [key: string]: SourceRange }
|
||||
}): Promise<any> {
|
||||
}): Promise<ResolveCommand | void> {
|
||||
if (this.engineConnection === undefined) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
@ -1802,11 +1846,13 @@ export class EngineCommandManager extends EventTarget {
|
||||
command: Models['ModelingCmd_type'],
|
||||
ast?: Program,
|
||||
range?: SourceRange
|
||||
) {
|
||||
): Promise<ResolveCommand | void> {
|
||||
let resolve: (val: any) => void = () => {}
|
||||
const promise = new Promise((_resolve, reject) => {
|
||||
resolve = _resolve
|
||||
})
|
||||
const promise: Promise<ResolveCommand | void> = new Promise(
|
||||
(_resolve, reject) => {
|
||||
resolve = _resolve
|
||||
}
|
||||
)
|
||||
const getParentId = (): string | undefined => {
|
||||
if (command.type === 'extend_path') return command.path
|
||||
if (command.type === 'solid3d_get_extrusion_face_info') {
|
||||
@ -1867,11 +1913,13 @@ export class EngineCommandManager extends EventTarget {
|
||||
idToRangeMap?: { [key: string]: SourceRange },
|
||||
ast?: Program,
|
||||
range?: SourceRange
|
||||
) {
|
||||
): Promise<ResolveCommand | void> {
|
||||
let resolve: (val: any) => void = () => {}
|
||||
const promise = new Promise((_resolve, reject) => {
|
||||
resolve = _resolve
|
||||
})
|
||||
const promise: Promise<ResolveCommand | void> = new Promise(
|
||||
(_resolve, reject) => {
|
||||
resolve = _resolve
|
||||
}
|
||||
)
|
||||
|
||||
if (!idToRangeMap) {
|
||||
throw new Error('idToRangeMap is required for batch commands')
|
||||
@ -1891,7 +1939,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
resolve,
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
Promise.all(
|
||||
commands.map((c) =>
|
||||
this.handlePendingCommand(c.cmd_id, c.cmd, ast, idToRangeMap[c.cmd_id])
|
||||
)
|
||||
@ -1903,7 +1951,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
rangeStr: string,
|
||||
commandStr: string,
|
||||
idToRangeStr: string
|
||||
): Promise<any> {
|
||||
): Promise<string | void> {
|
||||
if (this.engineConnection === undefined) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
@ -1932,13 +1980,13 @@ export class EngineCommandManager extends EventTarget {
|
||||
command,
|
||||
ast: this.getAst(),
|
||||
idToRangeMap,
|
||||
}).then(({ raw }: { raw: WebSocketResponse | undefined | null }) => {
|
||||
if (raw === undefined || raw === null) {
|
||||
}).then((resp) => {
|
||||
if (!resp) {
|
||||
throw new Error(
|
||||
'returning modeling cmd response to the rust side is undefined or null'
|
||||
)
|
||||
}
|
||||
return JSON.stringify(raw)
|
||||
return JSON.stringify(resp.raw)
|
||||
})
|
||||
}
|
||||
commandResult(id: string): Promise<any> {
|
||||
|
@ -25,7 +25,7 @@ import type { Program } from '../wasm-lib/kcl/bindings/Program'
|
||||
import type { Token } from '../wasm-lib/kcl/bindings/Token'
|
||||
import { Coords2d } from './std/sketch'
|
||||
import { fileSystemManager } from 'lang/std/fileSystemManager'
|
||||
import { AppInfo } from 'wasm-lib/kcl/bindings/AppInfo'
|
||||
import { CoreDumpInfo } from 'wasm-lib/kcl/bindings/CoreDumpInfo'
|
||||
import { CoreDumpManager } from 'lib/coredump'
|
||||
import openWindow from 'lib/openWindow'
|
||||
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
|
||||
@ -335,14 +335,27 @@ export function programMemoryInit(): ProgramMemory {
|
||||
export async function coreDump(
|
||||
coreDumpManager: CoreDumpManager,
|
||||
openGithubIssue: boolean = false
|
||||
): Promise<AppInfo> {
|
||||
): Promise<CoreDumpInfo> {
|
||||
try {
|
||||
const dump: AppInfo = await coredump(coreDumpManager)
|
||||
const dump: CoreDumpInfo = await coredump(coreDumpManager)
|
||||
/* NOTE: this console output of the coredump should include the field
|
||||
`github_issue_url` which is not in the uploaded coredump file.
|
||||
`github_issue_url` is added after the file is uploaded
|
||||
and is only needed for the openWindow operation which creates
|
||||
a new GitHub issue for the user.
|
||||
*/
|
||||
if (openGithubIssue && dump.github_issue_url) {
|
||||
openWindow(dump.github_issue_url)
|
||||
} else {
|
||||
console.error(
|
||||
'github_issue_url undefined. Unable to create GitHub issue for coredump.'
|
||||
)
|
||||
}
|
||||
console.log('CoreDump: final coredump', dump)
|
||||
console.log('CoreDump: final coredump JSON', JSON.stringify(dump))
|
||||
return dump
|
||||
} catch (e: any) {
|
||||
console.error('CoreDump: error', e)
|
||||
throw new Error(`Error getting core dump: ${e}`)
|
||||
}
|
||||
}
|
||||
|
@ -13,8 +13,15 @@ import screenshot from 'lib/screenshot'
|
||||
import React from 'react'
|
||||
import { VITE_KC_API_BASE_URL } from 'env'
|
||||
|
||||
// This is a class for getting all the values from the JS world to pass to the Rust world
|
||||
// for a core dump.
|
||||
/**
|
||||
* CoreDumpManager module
|
||||
* - for getting all the values from the JS world to pass to the Rust world for a core dump.
|
||||
* @module lib/coredump
|
||||
* @class
|
||||
*/
|
||||
// CoreDumpManager is instantiated in ModelingMachineProvider and passed to coreDump() in wasm.ts
|
||||
// The async function coreDump() handles any errors thrown in its Promise catch method and rethrows
|
||||
// them to so the toast handler in ModelingMachineProvider can show the user an error message toast
|
||||
export class CoreDumpManager {
|
||||
engineCommandManager: EngineCommandManager
|
||||
htmlRef: React.RefObject<HTMLDivElement> | null
|
||||
@ -144,6 +151,293 @@ export class CoreDumpManager {
|
||||
})
|
||||
}
|
||||
|
||||
// Currently just a placeholder to begin loading singleton and xstate data into
|
||||
getClientState(): Promise<string> {
|
||||
/**
|
||||
* Deep clone a JavaScript Object
|
||||
* - NOTE: this function throws on parse errors from things like circular references
|
||||
* - It is also synchronous and could be more performant
|
||||
* - There is a whole rabbit hole to explore here if you like.
|
||||
* - This works for our use case.
|
||||
* @param {object} obj - The object to clone.
|
||||
*/
|
||||
const deepClone = (obj: any) => JSON.parse(JSON.stringify(obj))
|
||||
|
||||
/**
|
||||
* Check if a function is private method
|
||||
*/
|
||||
const isPrivateMethod = (key: string) => {
|
||||
return key.length && key[0] === '_'
|
||||
}
|
||||
|
||||
// Turn off verbose logging by default
|
||||
const verboseLogging = false
|
||||
|
||||
/**
|
||||
* Toggle verbose debug logging of step-by-step client state coredump data
|
||||
*/
|
||||
const debugLog = verboseLogging ? console.log : () => {}
|
||||
|
||||
console.warn('CoreDump: Gathering client state')
|
||||
|
||||
// Initialize the clientState object
|
||||
let clientState = {
|
||||
// singletons
|
||||
engine_command_manager: {
|
||||
artifact_map: {},
|
||||
command_logs: [],
|
||||
engine_connection: { state: { type: '' } },
|
||||
default_planes: {},
|
||||
scene_command_artifacts: {},
|
||||
},
|
||||
kcl_manager: {
|
||||
ast: {},
|
||||
kcl_errors: [],
|
||||
},
|
||||
scene_infra: {},
|
||||
scene_entities_manager: {},
|
||||
editor_manager: {},
|
||||
// xstate
|
||||
auth_machine: {},
|
||||
command_bar_machine: {},
|
||||
file_machine: {},
|
||||
home_machine: {},
|
||||
modeling_machine: {},
|
||||
settings_machine: {},
|
||||
}
|
||||
debugLog('CoreDump: initialized clientState', clientState)
|
||||
debugLog('CoreDump: globalThis.window', globalThis.window)
|
||||
|
||||
try {
|
||||
// Singletons
|
||||
|
||||
// engine_command_manager
|
||||
debugLog('CoreDump: engineCommandManager', this.engineCommandManager)
|
||||
|
||||
// artifact map - this.engineCommandManager.artifactMap
|
||||
if (this.engineCommandManager?.artifactMap) {
|
||||
debugLog(
|
||||
'CoreDump: Engine Command Manager artifact map',
|
||||
this.engineCommandManager.artifactMap
|
||||
)
|
||||
clientState.engine_command_manager.artifact_map = deepClone(
|
||||
this.engineCommandManager.artifactMap
|
||||
)
|
||||
}
|
||||
|
||||
// command logs - this.engineCommandManager.commandLogs
|
||||
if (this.engineCommandManager?.commandLogs) {
|
||||
debugLog(
|
||||
'CoreDump: Engine Command Manager command logs',
|
||||
this.engineCommandManager.commandLogs
|
||||
)
|
||||
clientState.engine_command_manager.command_logs = deepClone(
|
||||
this.engineCommandManager.commandLogs
|
||||
)
|
||||
}
|
||||
|
||||
// default planes - this.engineCommandManager.defaultPlanes
|
||||
if (this.engineCommandManager?.defaultPlanes) {
|
||||
debugLog(
|
||||
'CoreDump: Engine Command Manager default planes',
|
||||
this.engineCommandManager.defaultPlanes
|
||||
)
|
||||
clientState.engine_command_manager.default_planes = deepClone(
|
||||
this.engineCommandManager.defaultPlanes
|
||||
)
|
||||
}
|
||||
|
||||
// engine connection state
|
||||
if (this.engineCommandManager?.engineConnection?.state) {
|
||||
debugLog(
|
||||
'CoreDump: Engine Command Manager engine connection state',
|
||||
this.engineCommandManager.engineConnection.state
|
||||
)
|
||||
clientState.engine_command_manager.engine_connection.state =
|
||||
this.engineCommandManager.engineConnection.state
|
||||
}
|
||||
|
||||
// in sequence - this.engineCommandManager.inSequence
|
||||
if (this.engineCommandManager?.inSequence) {
|
||||
debugLog(
|
||||
'CoreDump: Engine Command Manager in sequence',
|
||||
this.engineCommandManager.inSequence
|
||||
)
|
||||
;(clientState.engine_command_manager as any).in_sequence =
|
||||
this.engineCommandManager.inSequence
|
||||
}
|
||||
|
||||
// out sequence - this.engineCommandManager.outSequence
|
||||
if (this.engineCommandManager?.outSequence) {
|
||||
debugLog(
|
||||
'CoreDump: Engine Command Manager out sequence',
|
||||
this.engineCommandManager.outSequence
|
||||
)
|
||||
;(clientState.engine_command_manager as any).out_sequence =
|
||||
this.engineCommandManager.outSequence
|
||||
}
|
||||
|
||||
// scene command artifacts - this.engineCommandManager.sceneCommandArtifacts
|
||||
if (this.engineCommandManager?.sceneCommandArtifacts) {
|
||||
debugLog(
|
||||
'CoreDump: Engine Command Manager scene command artifacts',
|
||||
this.engineCommandManager.sceneCommandArtifacts
|
||||
)
|
||||
clientState.engine_command_manager.scene_command_artifacts = deepClone(
|
||||
this.engineCommandManager.sceneCommandArtifacts
|
||||
)
|
||||
}
|
||||
|
||||
// KCL Manager - globalThis?.window?.kclManager
|
||||
const kclManager = (globalThis?.window as any)?.kclManager
|
||||
debugLog('CoreDump: kclManager', kclManager)
|
||||
|
||||
if (kclManager) {
|
||||
// KCL Manager AST
|
||||
debugLog('CoreDump: KCL Manager AST', kclManager?.ast)
|
||||
if (kclManager?.ast) {
|
||||
clientState.kcl_manager.ast = deepClone(kclManager.ast)
|
||||
}
|
||||
|
||||
// KCL Errors
|
||||
debugLog('CoreDump: KCL Errors', kclManager?.kclErrors)
|
||||
if (kclManager?.kclErrors) {
|
||||
clientState.kcl_manager.kcl_errors = deepClone(kclManager.kclErrors)
|
||||
}
|
||||
|
||||
// KCL isExecuting
|
||||
debugLog('CoreDump: KCL isExecuting', kclManager?.isExecuting)
|
||||
if (kclManager?.isExecuting) {
|
||||
;(clientState.kcl_manager as any).isExecuting = kclManager.isExecuting
|
||||
}
|
||||
|
||||
// KCL logs
|
||||
debugLog('CoreDump: KCL logs', kclManager?.logs)
|
||||
if (kclManager?.logs) {
|
||||
;(clientState.kcl_manager as any).logs = deepClone(kclManager.logs)
|
||||
}
|
||||
|
||||
// KCL programMemory
|
||||
debugLog('CoreDump: KCL programMemory', kclManager?.programMemory)
|
||||
if (kclManager?.programMemory) {
|
||||
;(clientState.kcl_manager as any).programMemory = deepClone(
|
||||
kclManager.programMemory
|
||||
)
|
||||
}
|
||||
|
||||
// KCL wasmInitFailed
|
||||
debugLog('CoreDump: KCL wasmInitFailed', kclManager?.wasmInitFailed)
|
||||
if (kclManager?.wasmInitFailed) {
|
||||
;(clientState.kcl_manager as any).wasmInitFailed =
|
||||
kclManager.wasmInitFailed
|
||||
}
|
||||
}
|
||||
|
||||
// Scene Infra - globalThis?.window?.sceneInfra
|
||||
const sceneInfra = (globalThis?.window as any)?.sceneInfra
|
||||
debugLog('CoreDump: Scene Infra', sceneInfra)
|
||||
|
||||
if (sceneInfra) {
|
||||
const sceneInfraSkipKeys = ['camControls']
|
||||
const sceneInfraKeys = Object.keys(sceneInfra)
|
||||
.sort()
|
||||
.filter((entry) => {
|
||||
return (
|
||||
typeof sceneInfra[entry] !== 'function' &&
|
||||
!sceneInfraSkipKeys.includes(entry)
|
||||
)
|
||||
})
|
||||
|
||||
debugLog('CoreDump: Scene Infra keys', sceneInfraKeys)
|
||||
sceneInfraKeys.forEach((key: string) => {
|
||||
debugLog('CoreDump: Scene Infra', key, sceneInfra[key])
|
||||
try {
|
||||
;(clientState.scene_infra as any)[key] = sceneInfra[key]
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'CoreDump: unable to parse Scene Infra ' + key + ' data due to ',
|
||||
error
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Scene Entities Manager - globalThis?.window?.sceneEntitiesManager
|
||||
const sceneEntitiesManager = (globalThis?.window as any)
|
||||
?.sceneEntitiesManager
|
||||
debugLog('CoreDump: sceneEntitiesManager', sceneEntitiesManager)
|
||||
|
||||
if (sceneEntitiesManager) {
|
||||
// Scene Entities Manager active segments
|
||||
debugLog(
|
||||
'CoreDump: Scene Entities Manager active segments',
|
||||
sceneEntitiesManager?.activeSegments
|
||||
)
|
||||
if (sceneEntitiesManager?.activeSegments) {
|
||||
;(clientState.scene_entities_manager as any).activeSegments =
|
||||
deepClone(sceneEntitiesManager.activeSegments)
|
||||
}
|
||||
}
|
||||
|
||||
// Editor Manager - globalThis?.window?.editorManager
|
||||
const editorManager = (globalThis?.window as any)?.editorManager
|
||||
debugLog('CoreDump: editorManager', editorManager)
|
||||
|
||||
if (editorManager) {
|
||||
const editorManagerSkipKeys = ['camControls']
|
||||
const editorManagerKeys = Object.keys(editorManager)
|
||||
.sort()
|
||||
.filter((entry) => {
|
||||
return (
|
||||
typeof editorManager[entry] !== 'function' &&
|
||||
!isPrivateMethod(entry) &&
|
||||
!editorManagerSkipKeys.includes(entry)
|
||||
)
|
||||
})
|
||||
|
||||
debugLog('CoreDump: Editor Manager keys', editorManagerKeys)
|
||||
editorManagerKeys.forEach((key: string) => {
|
||||
debugLog('CoreDump: Editor Manager', key, editorManager[key])
|
||||
try {
|
||||
;(clientState.editor_manager as any)[key] = deepClone(
|
||||
editorManager[key]
|
||||
)
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'CoreDump: unable to parse Editor Manager ' +
|
||||
key +
|
||||
' data due to ',
|
||||
error
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// enableMousePositionLogs - Not coredumped
|
||||
// See https://github.com/KittyCAD/modeling-app/issues/2338#issuecomment-2136441998
|
||||
debugLog(
|
||||
'CoreDump: enableMousePositionLogs [not coredumped]',
|
||||
(globalThis?.window as any)?.enableMousePositionLogs
|
||||
)
|
||||
|
||||
// XState Machines
|
||||
debugLog(
|
||||
'CoreDump: xstate services',
|
||||
(globalThis?.window as any)?.__xstate__?.services
|
||||
)
|
||||
|
||||
debugLog('CoreDump: final clientState', clientState)
|
||||
|
||||
const clientStateJson = JSON.stringify(clientState)
|
||||
debugLog('CoreDump: final clientState JSON', clientStateJson)
|
||||
|
||||
return Promise.resolve(clientStateJson)
|
||||
} catch (error) {
|
||||
console.error('CoreDump: unable to return data due to ', error)
|
||||
return Promise.reject(JSON.stringify(error))
|
||||
}
|
||||
}
|
||||
|
||||
// Return a data URL (png format) of the screenshot of the current page.
|
||||
screenshot(): Promise<string> {
|
||||
return screenshot(this.htmlRef)
|
||||
|
@ -157,7 +157,7 @@ export function createSettings() {
|
||||
),
|
||||
}),
|
||||
enableSSAO: new Setting<boolean>({
|
||||
defaultValue: true,
|
||||
defaultValue: false,
|
||||
description:
|
||||
'Whether or not Screen Space Ambient Occlusion (SSAO) is enabled',
|
||||
validate: (v) => typeof v === 'boolean',
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { OnboardingButtons, useDismiss, useNextClick } from '.'
|
||||
import { OnboardingButtons, useDemoCode, useDismiss, useNextClick } from '.'
|
||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||
import { useStore } from '../../useStore'
|
||||
|
||||
export default function CodeEditor() {
|
||||
export default function OnboardingCodeEditor() {
|
||||
useDemoCode()
|
||||
const { buttonDownInStream } = useStore((s) => ({
|
||||
buttonDownInStream: s.buttonDownInStream,
|
||||
}))
|
||||
|
@ -1,24 +1,19 @@
|
||||
import { OnboardingButtons, useDismiss } from '.'
|
||||
import { OnboardingButtons, useDemoCode, useDismiss } from '.'
|
||||
import { useEffect } from 'react'
|
||||
import { bracket } from 'lib/exampleKcl'
|
||||
import { codeManager, kclManager } from 'lib/singletons'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { APP_NAME } from 'lib/constants'
|
||||
import { onboardingPaths } from './paths'
|
||||
import { sceneInfra } from 'lib/singletons'
|
||||
|
||||
export default function FutureWork() {
|
||||
const { send } = useModelingContext()
|
||||
const dismiss = useDismiss()
|
||||
|
||||
// Reset the code, the camera, and the modeling state
|
||||
useDemoCode()
|
||||
useEffect(() => {
|
||||
// We do want to update both the state and editor here.
|
||||
codeManager.updateCodeEditor(bracket)
|
||||
if (kclManager.engineCommandManager.engineConnection?.isReady()) {
|
||||
// If the engine is ready, promptly execute the loaded code
|
||||
kclManager.executeCode(true, true)
|
||||
}
|
||||
|
||||
send({ type: 'Cancel' }) // in case the user hit 'Next' while still in sketch mode
|
||||
sceneInfra.camControls.resetCameraPosition()
|
||||
}, [send])
|
||||
|
||||
return (
|
||||
|
@ -1,9 +1,16 @@
|
||||
import { OnboardingButtons, kbdClasses, useDismiss, useNextClick } from '.'
|
||||
import {
|
||||
OnboardingButtons,
|
||||
kbdClasses,
|
||||
useDemoCode,
|
||||
useDismiss,
|
||||
useNextClick,
|
||||
} from '.'
|
||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||
import { useStore } from '../../useStore'
|
||||
import { bracketWidthConstantLine } from 'lib/exampleKcl'
|
||||
|
||||
export default function InteractiveNumbers() {
|
||||
export default function OnboardingInteractiveNumbers() {
|
||||
useDemoCode()
|
||||
const { buttonDownInStream } = useStore((s) => ({
|
||||
buttonDownInStream: s.buttonDownInStream,
|
||||
}))
|
||||
@ -33,8 +40,10 @@ export default function InteractiveNumbers() {
|
||||
<kbd className={kbdClasses}>Option</kbd>) key
|
||||
</li>
|
||||
<li>
|
||||
Hover over the number assigned to <code>width</code> on line{' '}
|
||||
{bracketWidthConstantLine}
|
||||
Hover over the number assigned to "width" on{' '}
|
||||
<em>
|
||||
<strong>line {bracketWidthConstantLine}</strong>
|
||||
</em>
|
||||
</li>
|
||||
<li>Drag the number left and right to change its value</li>
|
||||
</ol>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { OnboardingButtons, useDismiss, useNextClick } from '.'
|
||||
import { OnboardingButtons, useDemoCode, useDismiss, useNextClick } from '.'
|
||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { Themes, getSystemTheme } from 'lib/theme'
|
||||
@ -10,7 +10,6 @@ import {
|
||||
import { isTauri } from 'lib/isTauri'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { paths } from 'lib/paths'
|
||||
import { useEffect } from 'react'
|
||||
import { codeManager, kclManager } from 'lib/singletons'
|
||||
import { join } from '@tauri-apps/api/path'
|
||||
import {
|
||||
@ -92,7 +91,7 @@ function OnboardingWithNewFile() {
|
||||
)
|
||||
}
|
||||
|
||||
export default function Introduction() {
|
||||
export default function OnboardingIntroduction() {
|
||||
const {
|
||||
settings: {
|
||||
state: {
|
||||
@ -112,9 +111,7 @@ export default function Introduction() {
|
||||
const currentCode = codeManager.code
|
||||
const isStarterCode = currentCode === '' || currentCode === bracket
|
||||
|
||||
useEffect(() => {
|
||||
if (codeManager.code === '') codeManager.updateCodeEditor(bracket)
|
||||
}, [])
|
||||
useDemoCode()
|
||||
|
||||
return isStarterCode ? (
|
||||
<div className="fixed inset-0 z-50 grid place-content-center bg-chalkboard-110/50">
|
||||
@ -159,6 +156,12 @@ export default function Introduction() {
|
||||
! We are trying to release as early as possible to get feedback from
|
||||
users like you.
|
||||
</p>
|
||||
<p>
|
||||
As you go through the onboarding, we'll be changing and resetting
|
||||
your code occasionally, so that we can reference specific code
|
||||
features. So hold off on writing production KCL code until you're
|
||||
done with the onboarding 😉
|
||||
</p>
|
||||
</section>
|
||||
<OnboardingButtons
|
||||
currentSlug={onboardingPaths.INDEX}
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { OnboardingButtons, useDismiss, useNextClick } from '.'
|
||||
import { OnboardingButtons, useDemoCode, useDismiss, useNextClick } from '.'
|
||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||
import { useStore } from '../../useStore'
|
||||
import { Themes, getSystemTheme } from 'lib/theme'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { bracketThicknessCalculationLine } from 'lib/exampleKcl'
|
||||
|
||||
export default function ParametricModeling() {
|
||||
export default function OnboardingParametricModeling() {
|
||||
useDemoCode()
|
||||
const { buttonDownInStream } = useStore((s) => ({
|
||||
buttonDownInStream: s.buttonDownInStream,
|
||||
}))
|
||||
@ -44,8 +45,10 @@ export default function ParametricModeling() {
|
||||
|
||||
<p className="my-4">
|
||||
We've received this sketch from a designer highlighting an{' '}
|
||||
<em className="text-primary">aluminum bracket</em> they need for
|
||||
this shelf:
|
||||
<em>
|
||||
<strong>aluminum bracket</strong>
|
||||
</em>{' '}
|
||||
they need for this shelf:
|
||||
</p>
|
||||
<figure className="my-4 w-2/3 mx-auto">
|
||||
<img
|
||||
@ -59,8 +62,8 @@ export default function ParametricModeling() {
|
||||
<p className="my-4">
|
||||
We are able to easily calculate the thickness of the material based
|
||||
on the width of the bracket to meet a set safety factor on{' '}
|
||||
<em className="text-primary">
|
||||
line {bracketThicknessCalculationLine}
|
||||
<em>
|
||||
<strong>line {bracketThicknessCalculationLine}</strong>
|
||||
</em>
|
||||
.
|
||||
</p>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { OnboardingButtons, useDismiss, useNextClick } from '.'
|
||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||
import { useStore } from '../../useStore'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export default function UserMenu() {
|
||||
const { buttonDownInStream } = useStore((s) => ({
|
||||
@ -8,6 +9,20 @@ export default function UserMenu() {
|
||||
}))
|
||||
const dismiss = useDismiss()
|
||||
const next = useNextClick(onboardingPaths.PROJECT_MENU)
|
||||
const [avatarErrored, setAvatarErrored] = useState(false)
|
||||
const buttonDescription = !avatarErrored ? 'your avatar' : 'the menu button'
|
||||
|
||||
// Set up error handling for the user's avatar image,
|
||||
// so the onboarding text can be updated if it fails to load.
|
||||
useEffect(() => {
|
||||
const element = globalThis.document.querySelector(
|
||||
'[data-testid="user-sidebar-toggle"] img'
|
||||
)
|
||||
|
||||
if (element?.tagName === 'IMG') {
|
||||
element.addEventListener('error', () => setAvatarErrored(true))
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="fixed grid justify-center items-start inset-0 z-50 pointer-events-none">
|
||||
@ -20,8 +35,8 @@ export default function UserMenu() {
|
||||
<section className="flex-1">
|
||||
<h2 className="text-2xl font-bold">User Menu</h2>
|
||||
<p className="my-4">
|
||||
Click your avatar on the upper right to open the user menu. You can
|
||||
change your settings, sign out, or request a feature.
|
||||
Click {buttonDescription} in the upper right to open the user menu.
|
||||
You can change your settings, sign out, or request a feature.
|
||||
</p>
|
||||
<p className="my-4">
|
||||
We only support global settings at the moment, but we are working to
|
||||
|
@ -19,9 +19,11 @@ import { paths } from 'lib/paths'
|
||||
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
||||
import { ActionButton } from 'components/ActionButton'
|
||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||
import { codeManager, editorManager } from 'lib/singletons'
|
||||
import { bracket } from 'lib/exampleKcl'
|
||||
|
||||
export const kbdClasses =
|
||||
'p-0.5 text-sm rounded-sm bg-chalkboard-10 dark:bg-chalkboard-100 border border-chalkboard-50'
|
||||
'py-0.5 px-1 text-sm rounded bg-chalkboard-10 dark:bg-chalkboard-100 border border-chalkboard-50 border-b-2'
|
||||
|
||||
export const onboardingRoutes = [
|
||||
{
|
||||
@ -75,6 +77,13 @@ export const onboardingRoutes = [
|
||||
},
|
||||
]
|
||||
|
||||
export function useDemoCode() {
|
||||
useEffect(() => {
|
||||
if (!editorManager.editorView) return
|
||||
setTimeout(() => codeManager.updateCodeStateEditor(bracket))
|
||||
}, [editorManager.editorView, codeManager.updateCodeStateEditor])
|
||||
}
|
||||
|
||||
export function useNextClick(newStatus: string) {
|
||||
const filePath = useAbsoluteFilePath()
|
||||
const {
|
||||
|
139
src/wasm-lib/Cargo.lock
generated
@ -1277,6 +1277,12 @@ dependencies = [
|
||||
"hashbrown 0.14.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indoc"
|
||||
version = "2.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
|
||||
|
||||
[[package]]
|
||||
name = "inflections"
|
||||
version = "1.1.1"
|
||||
@ -1399,6 +1405,7 @@ dependencies = [
|
||||
"mime_guess",
|
||||
"parse-display",
|
||||
"pretty_assertions",
|
||||
"pyo3",
|
||||
"reqwest",
|
||||
"ropey",
|
||||
"schemars",
|
||||
@ -1434,6 +1441,19 @@ dependencies = [
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kcl-test-server"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"hyper",
|
||||
"kcl-lib",
|
||||
"pico-args",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kittycad"
|
||||
version = "0.3.5"
|
||||
@ -1556,6 +1576,15 @@ version = "2.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
@ -1815,6 +1844,12 @@ dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pico-args"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.1.5"
|
||||
@ -1888,6 +1923,12 @@ dependencies = [
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0"
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
@ -1943,6 +1984,69 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3"
|
||||
version = "0.21.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5e00b96a521718e08e03b1a622f01c8a8deb50719335de3f60b3b3950f069d8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"indoc",
|
||||
"libc",
|
||||
"memoffset",
|
||||
"parking_lot 0.12.1",
|
||||
"portable-atomic",
|
||||
"pyo3-build-config",
|
||||
"pyo3-ffi",
|
||||
"pyo3-macros",
|
||||
"unindent",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-build-config"
|
||||
version = "0.21.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7883df5835fafdad87c0d888b266c8ec0f4c9ca48a5bed6bbb592e8dedee1b50"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"target-lexicon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-ffi"
|
||||
version = "0.21.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01be5843dc60b916ab4dad1dca6d20b9b4e6ddc8e15f50c47fe6d85f1fb97403"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pyo3-build-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-macros"
|
||||
version = "0.21.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77b34069fc0682e11b31dbd10321cbf94808394c56fd996796ce45217dfac53c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"pyo3-macros-backend",
|
||||
"quote",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-macros-backend"
|
||||
version = "0.21.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08260721f32db5e1a5beae69a55553f56b99bd0e1c3e6e0a5e8851a9d0f5a85c"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"proc-macro2",
|
||||
"pyo3-build-config",
|
||||
"quote",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.28.2"
|
||||
@ -2492,9 +2596,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.116"
|
||||
version = "1.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813"
|
||||
checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
|
||||
dependencies = [
|
||||
"indexmap 2.2.5",
|
||||
"itoa",
|
||||
@ -2524,9 +2628,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_tokenstream"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a00ffd23fd882d096f09fcaae2a9de8329a328628e86027e049ee051dc1621f"
|
||||
checksum = "8790a7c3fe883e443eaa2af6f705952bc5d6e8671a220b9335c8cae92c037e74"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -2776,6 +2880,12 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.12.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f"
|
||||
|
||||
[[package]]
|
||||
name = "task-local-extensions"
|
||||
version = "0.1.4"
|
||||
@ -3157,10 +3267,12 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "ts-rs"
|
||||
version = "8.1.0"
|
||||
source = "git+https://github.com/Aleph-Alpha/ts-rs#be0349d5fb07a8ccab713887a61e90e3bc773c7a"
|
||||
version = "9.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e2dcf58e612adda9a83800731e8e4aba04d8a302b9029617b0b6e4b021d5357"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"ts-rs-macros",
|
||||
"url",
|
||||
@ -3169,8 +3281,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ts-rs-macros"
|
||||
version = "8.1.0"
|
||||
source = "git+https://github.com/Aleph-Alpha/ts-rs#be0349d5fb07a8ccab713887a61e90e3bc773c7a"
|
||||
version = "9.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cbdee324e50a7402416d9c25270d3df4241ed528af5d36dda18b6f219551c577"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -3258,6 +3371,12 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
|
||||
|
||||
[[package]]
|
||||
name = "unindent"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
@ -3266,9 +3385,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.0"
|
||||
version = "2.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
|
||||
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
|
@ -65,6 +65,7 @@ members = [
|
||||
"derive-docs",
|
||||
"kcl",
|
||||
"kcl-macros",
|
||||
"kcl-test-server",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
|
13
src/wasm-lib/kcl-test-server/Cargo.toml
Normal file
@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "kcl-test-server"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.86"
|
||||
hyper = { version = "0.14.29", features = ["server"] }
|
||||
kcl-lib = { path = "../kcl" }
|
||||
pico-args = "0.5.0"
|
||||
serde = { version = "1.0.203", features = ["derive"] }
|
||||
serde_json = "1.0.117"
|
||||
tokio = { version = "1.38.0", features = ["macros", "rt-multi-thread"] }
|
207
src/wasm-lib/kcl-test-server/src/lib.rs
Normal file
@ -0,0 +1,207 @@
|
||||
//! Executes KCL programs.
|
||||
//! The server reuses the same engine session for each KCL program it receives.
|
||||
use std::{
|
||||
net::SocketAddr,
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use hyper::{
|
||||
body::Bytes,
|
||||
header::CONTENT_TYPE,
|
||||
service::{make_service_fn, service_fn},
|
||||
Body, Error, Response, Server,
|
||||
};
|
||||
use kcl_lib::{executor::ExecutorContext, settings::types::UnitLength, test_server::RequestBody};
|
||||
use tokio::{
|
||||
sync::{mpsc, oneshot},
|
||||
task::JoinHandle,
|
||||
time::sleep,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ServerArgs {
|
||||
/// What port this server should listen on.
|
||||
pub listen_on: SocketAddr,
|
||||
/// How many connections to establish with the engine.
|
||||
pub num_engine_conns: u8,
|
||||
}
|
||||
|
||||
impl ServerArgs {
|
||||
pub fn parse(mut pargs: pico_args::Arguments) -> Result<Self, pico_args::Error> {
|
||||
let args = ServerArgs {
|
||||
listen_on: pargs
|
||||
.opt_value_from_str("--listen-on")?
|
||||
.unwrap_or("0.0.0.0:3333".parse().unwrap()),
|
||||
num_engine_conns: pargs.opt_value_from_str("--num-engine-conns")?.unwrap_or(1),
|
||||
};
|
||||
println!("Config is {args:?}");
|
||||
Ok(args)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sent from the server to each worker.
|
||||
struct WorkerReq {
|
||||
/// A KCL program, in UTF-8.
|
||||
body: Bytes,
|
||||
/// A channel to send the HTTP response back.
|
||||
resp: oneshot::Sender<Response<Body>>,
|
||||
}
|
||||
|
||||
/// Each worker has a connection to the engine, and accepts
|
||||
/// KCL programs. When it receives one (over the mpsc channel)
|
||||
/// it executes it and returns the result via a oneshot channel.
|
||||
fn start_worker(i: u8) -> mpsc::Sender<WorkerReq> {
|
||||
println!("Starting worker {i}");
|
||||
// Make a work queue for this worker.
|
||||
let (tx, mut rx) = mpsc::channel(1);
|
||||
tokio::task::spawn(async move {
|
||||
let state = ExecutorContext::new_for_unit_test(UnitLength::Mm).await.unwrap();
|
||||
println!("Worker {i} ready");
|
||||
while let Some(req) = rx.recv().await {
|
||||
let req: WorkerReq = req;
|
||||
let resp = snapshot_endpoint(req.body, state.clone()).await;
|
||||
if req.resp.send(resp).is_err() {
|
||||
println!("\tWorker {i} exiting");
|
||||
}
|
||||
}
|
||||
println!("\tWorker {i} exiting");
|
||||
});
|
||||
tx
|
||||
}
|
||||
|
||||
struct ServerState {
|
||||
workers: Vec<mpsc::Sender<WorkerReq>>,
|
||||
req_num: AtomicUsize,
|
||||
}
|
||||
|
||||
pub async fn start_server(args: ServerArgs) -> anyhow::Result<()> {
|
||||
let ServerArgs {
|
||||
listen_on,
|
||||
num_engine_conns,
|
||||
} = args;
|
||||
let workers: Vec<_> = (0..num_engine_conns).map(start_worker).collect();
|
||||
let state = Arc::new(ServerState {
|
||||
workers,
|
||||
req_num: 0.into(),
|
||||
});
|
||||
// In hyper, a `MakeService` is basically your server.
|
||||
// It makes a `Service` for each connection, which manages the connection.
|
||||
let make_service = make_service_fn(
|
||||
// This closure is run for each connection.
|
||||
move |_conn_info| {
|
||||
// eprintln!("Connected to a client");
|
||||
let state = state.clone();
|
||||
async move {
|
||||
// This is the `Service` which handles the connection.
|
||||
// `service_fn` converts a function which returns a Response
|
||||
// into a `Service`.
|
||||
Ok::<_, Error>(service_fn(move |req| {
|
||||
// eprintln!("Received a request");
|
||||
let state = state.clone();
|
||||
async move { handle_request(req, state).await }
|
||||
}))
|
||||
}
|
||||
},
|
||||
);
|
||||
let server = Server::bind(&listen_on).serve(make_service);
|
||||
println!("Listening on {listen_on}");
|
||||
println!("PID is {}", std::process::id());
|
||||
if let Err(e) = server.await {
|
||||
eprintln!("Server error: {e}");
|
||||
return Err(e.into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_request(req: hyper::Request<Body>, state3: Arc<ServerState>) -> Result<Response<Body>, Error> {
|
||||
let body = hyper::body::to_bytes(req.into_body()).await?;
|
||||
|
||||
// Round robin requests between each available worker.
|
||||
let req_num = state3.req_num.fetch_add(1, Ordering::Relaxed);
|
||||
let worker_id = req_num % state3.workers.len();
|
||||
// println!("Sending request {req_num} to worker {worker_id}");
|
||||
let worker = state3.workers[worker_id].clone();
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let req_sent = worker.send(WorkerReq { body, resp: tx }).await;
|
||||
req_sent.unwrap();
|
||||
let resp = rx.await.unwrap();
|
||||
Ok(resp)
|
||||
}
|
||||
|
||||
/// Execute a KCL program, then respond with a PNG snapshot.
|
||||
/// KCL errors (from engine or the executor) respond with HTTP Bad Gateway.
|
||||
/// Malformed requests are HTTP Bad Request.
|
||||
/// Successful requests contain a PNG as the body.
|
||||
async fn snapshot_endpoint(body: Bytes, state: ExecutorContext) -> Response<Body> {
|
||||
let body = match serde_json::from_slice::<RequestBody>(body.as_ref()) {
|
||||
Ok(bd) => bd,
|
||||
Err(e) => return bad_request(format!("Invalid request JSON: {e}")),
|
||||
};
|
||||
let RequestBody { kcl_program, test_name } = body;
|
||||
let parser = match kcl_lib::token::lexer(&kcl_program) {
|
||||
Ok(ts) => kcl_lib::parser::Parser::new(ts),
|
||||
Err(e) => return bad_request(format!("tokenization error: {e}")),
|
||||
};
|
||||
let program = match parser.ast() {
|
||||
Ok(pr) => pr,
|
||||
Err(e) => return bad_request(format!("Parse error: {e}")),
|
||||
};
|
||||
eprintln!("Executing {test_name}");
|
||||
if let Err(e) = state.reset_scene().await {
|
||||
return kcl_err(e);
|
||||
}
|
||||
// Let users know if the test is taking a long time.
|
||||
let (done_tx, done_rx) = oneshot::channel::<()>();
|
||||
let timer = time_until(done_rx);
|
||||
let snapshot = match state.execute_and_prepare_snapshot(program).await {
|
||||
Ok(sn) => sn,
|
||||
Err(e) => return kcl_err(e),
|
||||
};
|
||||
let _ = done_tx.send(());
|
||||
timer.abort();
|
||||
eprintln!("\tServing response");
|
||||
let png_bytes = snapshot.contents.0;
|
||||
let mut resp = Response::new(Body::from(png_bytes));
|
||||
resp.headers_mut().insert(CONTENT_TYPE, "image/png".parse().unwrap());
|
||||
resp
|
||||
}
|
||||
|
||||
fn bad_request(msg: String) -> Response<Body> {
|
||||
eprintln!("\tBad request");
|
||||
let mut resp = Response::new(Body::from(msg));
|
||||
*resp.status_mut() = hyper::StatusCode::BAD_REQUEST;
|
||||
resp
|
||||
}
|
||||
|
||||
fn bad_gateway(msg: String) -> Response<Body> {
|
||||
eprintln!("\tBad gateway");
|
||||
let mut resp = Response::new(Body::from(msg));
|
||||
*resp.status_mut() = hyper::StatusCode::BAD_GATEWAY;
|
||||
resp
|
||||
}
|
||||
|
||||
fn kcl_err(err: anyhow::Error) -> Response<Body> {
|
||||
eprintln!("\tBad KCL");
|
||||
bad_gateway(format!("{err}"))
|
||||
}
|
||||
|
||||
fn time_until(done: oneshot::Receiver<()>) -> JoinHandle<()> {
|
||||
tokio::task::spawn(async move {
|
||||
let period = 10;
|
||||
tokio::pin!(done);
|
||||
for i in 1..=3 {
|
||||
tokio::select! {
|
||||
biased;
|
||||
// If the test is done, no need for this timer anymore.
|
||||
_ = &mut done => return,
|
||||
_ = sleep(Duration::from_secs(period)) => {
|
||||
eprintln!("\tTest has taken {}s", period * i);
|
||||
},
|
||||
};
|
||||
}
|
||||
})
|
||||
}
|
@ -28,6 +28,7 @@ kittycad = { workspace = true, features = ["clap"] }
|
||||
lazy_static = "1.4.0"
|
||||
mime_guess = "2.0.4"
|
||||
parse-display = "0.9.1"
|
||||
pyo3 = {version = "0.21.2", optional = true}
|
||||
reqwest = { version = "0.11.26", default-features = false, features = ["stream", "rustls-tls"] }
|
||||
ropey = "1.6.1"
|
||||
schemars = { version = "0.8.17", features = ["impl_json_schema", "url", "uuid1"] }
|
||||
@ -36,9 +37,8 @@ serde_json = "1.0.116"
|
||||
sha2 = "0.10.8"
|
||||
thiserror = "1.0.61"
|
||||
toml = "0.8.14"
|
||||
# TODO: change this to a cargo release once 8.1.1 comes out
|
||||
ts-rs = { git = "https://github.com/Aleph-Alpha/ts-rs", features = ["uuid-impl", "url-impl", "chrono-impl", "no-serde-warnings"] }
|
||||
url = { version = "2.5.0", features = ["serde"] }
|
||||
ts-rs = { version = "9.0.0", features = ["uuid-impl", "url-impl", "chrono-impl", "no-serde-warnings", "serde-json-impl"] }
|
||||
url = { version = "2.5.2", features = ["serde"] }
|
||||
uuid = { version = "1.8.0", features = ["v4", "js", "serde"] }
|
||||
validator = { version = "0.18.1", features = ["derive"] }
|
||||
winnow = "0.5.40"
|
||||
@ -62,7 +62,11 @@ tower-lsp = { version = "0.20.0", features = ["proposed"] }
|
||||
[features]
|
||||
default = ["cli", "engine"]
|
||||
cli = ["dep:clap"]
|
||||
# For the lsp server, when run with stdout for rpc we want to disable println.
|
||||
# This is used for editor extensions that use the lsp server.
|
||||
disable-println = []
|
||||
engine = []
|
||||
pyo3 = ["dep:pyo3"]
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
|
@ -1,6 +1,11 @@
|
||||
//! Data types for the AST.
|
||||
|
||||
use std::{collections::HashMap, fmt::Write, ops::RangeInclusive};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::Write,
|
||||
ops::RangeInclusive,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use databake::*;
|
||||
@ -147,6 +152,21 @@ impl Program {
|
||||
}
|
||||
}
|
||||
|
||||
/// Check the provided Program for any lint findings.
|
||||
pub fn lint<'a, RuleT>(&'a self, rule: RuleT) -> Result<Vec<crate::lint::Discovered>>
|
||||
where
|
||||
RuleT: crate::lint::rule::Rule<'a>,
|
||||
{
|
||||
let v = Arc::new(Mutex::new(vec![]));
|
||||
crate::lint::walk(self, &|node: crate::lint::Node<'a>| {
|
||||
let mut findings = v.lock().map_err(|_| anyhow::anyhow!("mutex"))?;
|
||||
findings.append(&mut rule.check(node)?);
|
||||
Ok(true)
|
||||
})?;
|
||||
let x = v.lock().unwrap();
|
||||
Ok(x.clone())
|
||||
}
|
||||
|
||||
/// Returns the body item that includes the given character position.
|
||||
pub fn get_body_item_for_position(&self, pos: usize) -> Option<&BodyItem> {
|
||||
for item in &self.body {
|
||||
@ -1076,7 +1096,12 @@ impl CallExpression {
|
||||
|
||||
fn recast(&self, options: &FormatOptions, indentation_level: usize, is_in_pipe: bool) -> String {
|
||||
format!(
|
||||
"{}({})",
|
||||
"{}{}({})",
|
||||
if is_in_pipe {
|
||||
"".to_string()
|
||||
} else {
|
||||
options.get_indentation(indentation_level)
|
||||
},
|
||||
self.callee.name,
|
||||
self.arguments
|
||||
.iter()
|
||||
@ -1335,7 +1360,7 @@ impl VariableDeclaration {
|
||||
indentation,
|
||||
self.kind,
|
||||
declaration.id.name,
|
||||
declaration.init.recast(options, indentation_level, false)
|
||||
declaration.init.recast(options, indentation_level, false).trim()
|
||||
);
|
||||
output
|
||||
})
|
||||
@ -1751,7 +1776,7 @@ impl ArrayExpression {
|
||||
inner_indentation,
|
||||
self.elements
|
||||
.iter()
|
||||
.map(|el| el.recast(options, indentation_level, false))
|
||||
.map(|el| el.recast(options, indentation_level, is_in_pipe))
|
||||
.collect::<Vec<String>>()
|
||||
.join(format!(",\n{}", inner_indentation).as_str()),
|
||||
if is_in_pipe {
|
||||
@ -2678,7 +2703,8 @@ impl PipeExpression {
|
||||
}
|
||||
|
||||
fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
|
||||
self.body
|
||||
let pipe = self
|
||||
.body
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, statement)| {
|
||||
@ -2710,7 +2736,8 @@ impl PipeExpression {
|
||||
}
|
||||
s
|
||||
})
|
||||
.collect::<String>()
|
||||
.collect::<String>();
|
||||
format!("{}{}", options.get_indentation(indentation_level), pipe)
|
||||
}
|
||||
|
||||
/// Returns a hover value that includes the given character position.
|
||||
@ -2997,6 +3024,7 @@ pub enum Hover {
|
||||
|
||||
/// Format options.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[cfg_attr(feature = "pyo3", pyo3::pyclass)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FormatOptions {
|
||||
@ -3265,6 +3293,132 @@ fn ghi = (x) => {
|
||||
assert_eq!(symbols.len(), 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_recast_bug_fn_in_fn() {
|
||||
let some_program_string = r#"// Start point (top left)
|
||||
const zoo_x = -20
|
||||
const zoo_y = 7
|
||||
// Scale
|
||||
const s = 1 // s = 1 -> height of Z is 13.4mm
|
||||
// Depth
|
||||
const d = 1
|
||||
|
||||
fn rect = (x, y, w, h) => {
|
||||
startSketchOn('XY')
|
||||
|> startProfileAt([x, y], %)
|
||||
|> xLine(w, %)
|
||||
|> yLine(h, %)
|
||||
|> xLine(-w, %)
|
||||
|> close(%)
|
||||
|> extrude(d, %)
|
||||
}
|
||||
|
||||
fn quad = (x1, y1, x2, y2, x3, y3, x4, y4) => {
|
||||
startSketchOn('XY')
|
||||
|> startProfileAt([x1, y1], %)
|
||||
|> lineTo([x2, y2], %)
|
||||
|> lineTo([x3, y3], %)
|
||||
|> lineTo([x4, y4], %)
|
||||
|> close(%)
|
||||
|> extrude(d, %)
|
||||
}
|
||||
|
||||
fn crosshair = (x, y) => {
|
||||
startSketchOn('XY')
|
||||
|> startProfileAt([x, y], %)
|
||||
|> yLine(1, %)
|
||||
|> yLine(-2, %)
|
||||
|> yLine(1, %)
|
||||
|> xLine(1, %)
|
||||
|> xLine(-2, %)
|
||||
}
|
||||
|
||||
fn z = (z_x, z_y) => {
|
||||
const z_end_w = s * 8.4
|
||||
const z_end_h = s * 3
|
||||
const z_corner = s * 2
|
||||
const z_w = z_end_w + 2 * z_corner
|
||||
const z_h = z_w * 1.08130081300813
|
||||
rect(z_x, z_y, z_end_w, -z_end_h)
|
||||
rect(z_x + z_w, z_y, -z_corner, -z_corner)
|
||||
rect(z_x + z_w, z_y - z_h, -z_end_w, z_end_h)
|
||||
rect(z_x, z_y - z_h, z_corner, z_corner)
|
||||
quad(z_x, z_y - z_h + z_corner, z_x + z_w - z_corner, z_y, z_x + z_w, z_y - z_corner, z_x + z_corner, z_y - z_h)
|
||||
}
|
||||
|
||||
fn o = (c_x, c_y) => {
|
||||
// Outer and inner radii
|
||||
const o_r = s * 6.95
|
||||
const i_r = 0.5652173913043478 * o_r
|
||||
|
||||
// Angle offset for diagonal break
|
||||
const a = 7
|
||||
|
||||
// Start point for the top sketch
|
||||
const o_x1 = c_x + o_r * cos((45 + a) / 360 * tau())
|
||||
const o_y1 = c_y + o_r * sin((45 + a) / 360 * tau())
|
||||
|
||||
// Start point for the bottom sketch
|
||||
const o_x2 = c_x + o_r * cos((225 + a) / 360 * tau())
|
||||
const o_y2 = c_y + o_r * sin((225 + a) / 360 * tau())
|
||||
|
||||
// End point for the bottom startSketchAt
|
||||
const o_x3 = c_x + o_r * cos((45 - a) / 360 * tau())
|
||||
const o_y3 = c_y + o_r * sin((45 - a) / 360 * tau())
|
||||
|
||||
// Where is the center?
|
||||
// crosshair(c_x, c_y)
|
||||
|
||||
|
||||
startSketchOn('XY')
|
||||
|> startProfileAt([o_x1, o_y1], %)
|
||||
|> arc({
|
||||
radius: o_r,
|
||||
angle_start: 45 + a,
|
||||
angle_end: 225 - a
|
||||
}, %)
|
||||
|> angledLine([45, o_r - i_r], %)
|
||||
|> arc({
|
||||
radius: i_r,
|
||||
angle_start: 225 - a,
|
||||
angle_end: 45 + a
|
||||
}, %)
|
||||
|> close(%)
|
||||
|> extrude(d, %)
|
||||
|
||||
startSketchOn('XY')
|
||||
|> startProfileAt([o_x2, o_y2], %)
|
||||
|> arc({
|
||||
radius: o_r,
|
||||
angle_start: 225 + a,
|
||||
angle_end: 360 + 45 - a
|
||||
}, %)
|
||||
|> angledLine([225, o_r - i_r], %)
|
||||
|> arc({
|
||||
radius: i_r,
|
||||
angle_start: 45 - a,
|
||||
angle_end: 225 + a - 360
|
||||
}, %)
|
||||
|> close(%)
|
||||
|> extrude(d, %)
|
||||
}
|
||||
|
||||
fn zoo = (x0, y0) => {
|
||||
z(x0, y0)
|
||||
o(x0 + s * 20, y0 - (s * 6.7))
|
||||
o(x0 + s * 35, y0 - (s * 6.7))
|
||||
}
|
||||
|
||||
zoo(zoo_x, zoo_y)
|
||||
"#;
|
||||
let tokens = crate::token::lexer(some_program_string).unwrap();
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
|
||||
let recasted = program.recast(&Default::default(), 0);
|
||||
assert_eq!(recasted, some_program_string);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_recast_bug_extra_parens() {
|
||||
let some_program_string = r#"// Ball Bearing
|
||||
@ -3332,8 +3486,6 @@ const outsideRevolve = startSketchOn('XZ')
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
|
||||
println!("{:#?}", program);
|
||||
|
||||
let recasted = program.recast(&Default::default(), 0);
|
||||
assert_eq!(
|
||||
recasted,
|
||||
@ -3656,7 +3808,6 @@ const tabs_l = startSketchOn({
|
||||
let tokens = crate::token::lexer(some_program_string).unwrap();
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
println!("{:#?}", program);
|
||||
|
||||
let recasted = program.recast(&Default::default(), 0);
|
||||
// Its VERY important this comes back with zero new lines.
|
||||
|
@ -3,7 +3,7 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value as JValue;
|
||||
|
||||
use super::{Literal, Value};
|
||||
use crate::ast::types::{Literal, Value};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Bake)]
|
||||
#[databake(path = kcl_lib::ast::types)]
|
||||
|
@ -4,8 +4,10 @@ use databake::*;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::ConstraintLevel;
|
||||
use crate::executor::{MemoryItem, SourceRange, UserVal};
|
||||
use crate::{
|
||||
ast::types::ConstraintLevel,
|
||||
executor::{MemoryItem, SourceRange, UserVal},
|
||||
};
|
||||
|
||||
/// KCL value for an optional parameter which was not given an argument.
|
||||
/// (remember, parameters are in the function declaration,
|
||||
|
@ -3,6 +3,7 @@
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::coredump::CoreDump;
|
||||
use serde_json::Value as JValue;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CoreDumper {}
|
||||
@ -55,6 +56,10 @@ impl CoreDump for CoreDumper {
|
||||
Ok(crate::coredump::WebrtcStats::default())
|
||||
}
|
||||
|
||||
async fn get_client_state(&self) -> Result<JValue> {
|
||||
Ok(JValue::default())
|
||||
}
|
||||
|
||||
async fn screenshot(&self) -> Result<String> {
|
||||
// Take a screenshot of the engine.
|
||||
todo!()
|
||||
|
@ -7,8 +7,13 @@ pub mod wasm;
|
||||
|
||||
use anyhow::Result;
|
||||
use base64::Engine;
|
||||
use kittycad::Client;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
/// "Value" would be OK. This is imported as "JValue" throughout the rest of this crate.
|
||||
use serde_json::Value as JValue;
|
||||
use std::path::Path;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
pub trait CoreDump: Clone {
|
||||
@ -27,25 +32,24 @@ pub trait CoreDump: Clone {
|
||||
|
||||
async fn get_webrtc_stats(&self) -> Result<WebrtcStats>;
|
||||
|
||||
async fn get_client_state(&self) -> Result<JValue>;
|
||||
|
||||
/// Return a screenshot of the app.
|
||||
async fn screenshot(&self) -> Result<String>;
|
||||
|
||||
/// Get a screenshot of the app and upload it to public cloud storage.
|
||||
async fn upload_screenshot(&self) -> Result<String> {
|
||||
async fn upload_screenshot(&self, coredump_id: &Uuid, zoo_client: &Client) -> Result<String> {
|
||||
let screenshot = self.screenshot().await?;
|
||||
let cleaned = screenshot.trim_start_matches("data:image/png;base64,");
|
||||
// Create the zoo client.
|
||||
let mut zoo = kittycad::Client::new(self.token()?);
|
||||
zoo.set_base_url(&self.base_api_url()?);
|
||||
|
||||
// Base64 decode the screenshot.
|
||||
let data = base64::engine::general_purpose::STANDARD.decode(cleaned)?;
|
||||
// Upload the screenshot.
|
||||
let links = zoo
|
||||
let links = zoo_client
|
||||
.meta()
|
||||
.create_debug_uploads(vec![kittycad::types::multipart::Attachment {
|
||||
name: "".to_string(),
|
||||
filename: Some("modeling-app/core-dump-screenshot.png".to_string()),
|
||||
filename: Some(format!(r#"modeling-app/coredump-{coredump_id}-screenshot.png"#)),
|
||||
content_type: Some("image/png".to_string()),
|
||||
data,
|
||||
}])
|
||||
@ -60,12 +64,19 @@ pub trait CoreDump: Clone {
|
||||
}
|
||||
|
||||
/// Dump the app info.
|
||||
async fn dump(&self) -> Result<AppInfo> {
|
||||
async fn dump(&self) -> Result<CoreDumpInfo> {
|
||||
// Create the zoo client.
|
||||
let mut zoo_client = kittycad::Client::new(self.token()?);
|
||||
zoo_client.set_base_url(&self.base_api_url()?);
|
||||
|
||||
let coredump_id = uuid::Uuid::new_v4();
|
||||
let client_state = self.get_client_state().await?;
|
||||
let webrtc_stats = self.get_webrtc_stats().await?;
|
||||
let os = self.os().await?;
|
||||
let screenshot_url = self.upload_screenshot().await?;
|
||||
let screenshot_url = self.upload_screenshot(&coredump_id, &zoo_client).await?;
|
||||
|
||||
let mut app_info = AppInfo {
|
||||
let mut core_dump_info = CoreDumpInfo {
|
||||
id: coredump_id,
|
||||
version: self.version()?,
|
||||
git_rev: git_rev::try_revision_string!().map_or_else(|| "unknown".to_string(), |s| s.to_string()),
|
||||
timestamp: chrono::Utc::now(),
|
||||
@ -74,18 +85,44 @@ pub trait CoreDump: Clone {
|
||||
webrtc_stats,
|
||||
github_issue_url: None,
|
||||
pool: self.pool()?,
|
||||
client_state,
|
||||
};
|
||||
app_info.set_github_issue_url(&screenshot_url)?;
|
||||
|
||||
Ok(app_info)
|
||||
// pretty-printed JSON byte vector of the coredump.
|
||||
let data = serde_json::to_vec_pretty(&core_dump_info)?;
|
||||
|
||||
// Upload the coredump.
|
||||
let links = zoo_client
|
||||
.meta()
|
||||
.create_debug_uploads(vec![kittycad::types::multipart::Attachment {
|
||||
name: "".to_string(),
|
||||
filename: Some(format!(r#"modeling-app/coredump-{}.json"#, coredump_id)),
|
||||
content_type: Some("application/json".to_string()),
|
||||
data,
|
||||
}])
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
|
||||
|
||||
if links.is_empty() {
|
||||
anyhow::bail!("Failed to upload coredump");
|
||||
}
|
||||
|
||||
let coredump_url = &links[0];
|
||||
|
||||
core_dump_info.set_github_issue_url(&screenshot_url, coredump_url, &coredump_id)?;
|
||||
|
||||
Ok(core_dump_info)
|
||||
}
|
||||
}
|
||||
|
||||
/// The app info structure.
|
||||
/// The Core Dump Info structure.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct AppInfo {
|
||||
pub struct CoreDumpInfo {
|
||||
/// The unique id for the core dump - this helps correlate uploaded files with coredump data.
|
||||
pub id: Uuid,
|
||||
/// The version of the app.
|
||||
pub version: String,
|
||||
/// The git revision of the app.
|
||||
@ -95,45 +132,44 @@ pub struct AppInfo {
|
||||
pub timestamp: chrono::DateTime<chrono::Utc>,
|
||||
/// If the app is running in tauri or the browser.
|
||||
pub tauri: bool,
|
||||
|
||||
/// The os info.
|
||||
pub os: OsInfo,
|
||||
|
||||
/// The webrtc stats.
|
||||
pub webrtc_stats: WebrtcStats,
|
||||
|
||||
/// A GitHub issue url to report the core dump.
|
||||
/// This gets prepoulated with all the core dump info.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub github_issue_url: Option<String>,
|
||||
|
||||
/// Engine pool the client is connected to.
|
||||
pub pool: String,
|
||||
/// The client state (singletons and xstate).
|
||||
pub client_state: JValue,
|
||||
}
|
||||
|
||||
impl AppInfo {
|
||||
impl CoreDumpInfo {
|
||||
/// Set the github issue url.
|
||||
pub fn set_github_issue_url(&mut self, screenshot_url: &str) -> Result<()> {
|
||||
pub fn set_github_issue_url(&mut self, screenshot_url: &str, coredump_url: &str, coredump_id: &Uuid) -> Result<()> {
|
||||
let coredump_filename = Path::new(coredump_url).file_name().unwrap().to_str().unwrap();
|
||||
let tauri_or_browser_label = if self.tauri { "tauri" } else { "browser" };
|
||||
let labels = ["coredump", "bug", tauri_or_browser_label];
|
||||
let body = format!(
|
||||
r#"[Insert a description of the issue here]
|
||||
r#"[Add a title above and insert a description of the issue here]
|
||||
|
||||

|
||||

|
||||
|
||||
<details>
|
||||
<summary><b>Core Dump</b></summary>
|
||||
|
||||
```json
|
||||
{}
|
||||
```
|
||||
[{coredump_filename}]({coredump_url})
|
||||
|
||||
Reference ID: {coredump_id}
|
||||
</details>
|
||||
"#,
|
||||
screenshot_url,
|
||||
serde_json::to_string_pretty(&self)?
|
||||
"#
|
||||
);
|
||||
let urlencoded: String = form_urlencoded::byte_serialize(body.as_bytes()).collect();
|
||||
|
||||
// Note that `github_issue_url` is not included in the coredump file.
|
||||
// It has already been encoded and uploaded at this point.
|
||||
// The `github_issue_url` is used in openWindow in wasm.ts.
|
||||
self.github_issue_url = Some(format!(
|
||||
r#"https://github.com/{}/{}/issues/new?body={}&labels={}"#,
|
||||
"KittyCAD",
|
||||
|
@ -4,6 +4,7 @@ use anyhow::Result;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
use crate::{coredump::CoreDump, wasm::JsFuture};
|
||||
use serde_json::Value as JValue;
|
||||
|
||||
#[wasm_bindgen(module = "/../../lib/coredump.ts")]
|
||||
extern "C" {
|
||||
@ -31,6 +32,9 @@ extern "C" {
|
||||
#[wasm_bindgen(method, js_name = getWebrtcStats, catch)]
|
||||
fn get_webrtc_stats(this: &CoreDumpManager) -> Result<js_sys::Promise, js_sys::Error>;
|
||||
|
||||
#[wasm_bindgen(method, js_name = getClientState, catch)]
|
||||
fn get_client_state(this: &CoreDumpManager) -> Result<js_sys::Promise, js_sys::Error>;
|
||||
|
||||
#[wasm_bindgen(method, js_name = screenshot, catch)]
|
||||
fn screenshot(this: &CoreDumpManager) -> Result<js_sys::Promise, js_sys::Error>;
|
||||
}
|
||||
@ -123,6 +127,27 @@ impl CoreDump for CoreDumper {
|
||||
Ok(stats)
|
||||
}
|
||||
|
||||
async fn get_client_state(&self) -> Result<JValue> {
|
||||
let promise = self
|
||||
.manager
|
||||
.get_client_state()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to get promise from get client state: {:?}", e))?;
|
||||
|
||||
let value = JsFuture::from(promise)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to get response from client state: {:?}", e))?;
|
||||
|
||||
// Parse the value as a string.
|
||||
let s = value
|
||||
.as_string()
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to get string from response from client stat: `{:?}`", value))?;
|
||||
|
||||
let client_state: JValue =
|
||||
serde_json::from_str(&s).map_err(|e| anyhow::anyhow!("Failed to parse client state: {:?}", e))?;
|
||||
|
||||
Ok(client_state)
|
||||
}
|
||||
|
||||
async fn screenshot(&self) -> Result<String> {
|
||||
let promise = self
|
||||
.manager
|
||||
|
@ -1,15 +1,17 @@
|
||||
//! Functions for generating docs for our stdlib functions.
|
||||
|
||||
use crate::std::Primitive;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
use tower_lsp::lsp_types::{
|
||||
CompletionItem, CompletionItemKind, CompletionItemLabelDetails, Documentation, InsertTextFormat, MarkupContent,
|
||||
MarkupKind, ParameterInformation, ParameterLabel, SignatureHelp, SignatureInformation,
|
||||
};
|
||||
|
||||
use crate::std::Primitive;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, ts_rs::TS)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
@ -6,7 +6,7 @@ use std::sync::{Arc, Mutex};
|
||||
use anyhow::{anyhow, Result};
|
||||
use dashmap::DashMap;
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use kittycad::types::{OkWebSocketResponseData, WebSocketRequest, WebSocketResponse};
|
||||
use kittycad::types::{WebSocketRequest, WebSocketResponse};
|
||||
use tokio::sync::{mpsc, oneshot, RwLock};
|
||||
use tokio_tungstenite::tungstenite::Message as WsMsg;
|
||||
|
||||
@ -40,23 +40,54 @@ pub struct TcpRead {
|
||||
stream: futures::stream::SplitStream<tokio_tungstenite::WebSocketStream<reqwest::Upgraded>>,
|
||||
}
|
||||
|
||||
/// Occurs when client couldn't read from the WebSocket to the engine.
|
||||
// #[derive(Debug)]
|
||||
pub enum WebSocketReadError {
|
||||
/// Could not read a message due to WebSocket errors.
|
||||
Read(tokio_tungstenite::tungstenite::Error),
|
||||
/// WebSocket message didn't contain a valid message that the KCL Executor could parse.
|
||||
Deser(anyhow::Error),
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for WebSocketReadError {
|
||||
fn from(e: anyhow::Error) -> Self {
|
||||
Self::Deser(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl TcpRead {
|
||||
pub async fn read(&mut self) -> Result<WebSocketResponse> {
|
||||
pub async fn read(&mut self) -> std::result::Result<WebSocketResponse, WebSocketReadError> {
|
||||
let Some(msg) = self.stream.next().await else {
|
||||
anyhow::bail!("Failed to read from websocket");
|
||||
return Err(anyhow::anyhow!("Failed to read from WebSocket").into());
|
||||
};
|
||||
let msg: WebSocketResponse = match msg? {
|
||||
WsMsg::Text(text) => serde_json::from_str(&text)?,
|
||||
WsMsg::Binary(bin) => bson::from_slice(&bin)?,
|
||||
other => anyhow::bail!("Unexpected websocket message from server: {}", other),
|
||||
let msg = match msg {
|
||||
Ok(msg) => msg,
|
||||
Err(e) if matches!(e, tokio_tungstenite::tungstenite::Error::Protocol(_)) => {
|
||||
return Err(WebSocketReadError::Read(e))
|
||||
}
|
||||
Err(e) => return Err(anyhow::anyhow!("Error reading from engine's WebSocket: {e}").into()),
|
||||
};
|
||||
let msg: WebSocketResponse = match msg {
|
||||
WsMsg::Text(text) => serde_json::from_str(&text)
|
||||
.map_err(anyhow::Error::from)
|
||||
.map_err(WebSocketReadError::from)?,
|
||||
WsMsg::Binary(bin) => bson::from_slice(&bin)
|
||||
.map_err(anyhow::Error::from)
|
||||
.map_err(WebSocketReadError::from)?,
|
||||
other => return Err(anyhow::anyhow!("Unexpected WebSocket message from engine API: {other}").into()),
|
||||
};
|
||||
Ok(msg)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TcpReadHandle {
|
||||
handle: Arc<tokio::task::JoinHandle<Result<()>>>,
|
||||
handle: Arc<tokio::task::JoinHandle<Result<(), WebSocketReadError>>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for TcpReadHandle {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "TcpReadHandle")
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TcpReadHandle {
|
||||
@ -149,15 +180,47 @@ impl EngineConnection {
|
||||
loop {
|
||||
match tcp_read.read().await {
|
||||
Ok(ws_resp) => {
|
||||
for e in ws_resp.errors.iter().flatten() {
|
||||
println!("got error message: {e}");
|
||||
// If we got a batch response, add all the inner responses.
|
||||
if let Some(kittycad::types::OkWebSocketResponseData::ModelingBatch { responses }) =
|
||||
&ws_resp.resp
|
||||
{
|
||||
for (resp_id, batch_response) in responses {
|
||||
let id: uuid::Uuid = resp_id.parse().unwrap();
|
||||
if let Some(response) = &batch_response.response {
|
||||
responses_clone.insert(
|
||||
id,
|
||||
kittycad::types::WebSocketResponse {
|
||||
request_id: Some(id),
|
||||
resp: Some(kittycad::types::OkWebSocketResponseData::Modeling {
|
||||
modeling_response: response.clone(),
|
||||
}),
|
||||
errors: None,
|
||||
success: Some(true),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
responses_clone.insert(
|
||||
id,
|
||||
kittycad::types::WebSocketResponse {
|
||||
request_id: Some(id),
|
||||
resp: None,
|
||||
errors: batch_response.errors.clone(),
|
||||
success: Some(false),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(id) = ws_resp.request_id {
|
||||
responses_clone.insert(id, ws_resp.clone());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("got ws error: {:?}", e);
|
||||
match &e {
|
||||
WebSocketReadError::Read(e) => eprintln!("could not read from WS: {:?}", e),
|
||||
WebSocketReadError::Deser(e) => eprintln!("could not deserialize msg from WS: {:?}", e),
|
||||
}
|
||||
*socket_health_tcp_read.lock().unwrap() = SocketHealth::Inactive;
|
||||
return Err(e);
|
||||
}
|
||||
@ -212,7 +275,7 @@ impl EngineManager for EngineConnection {
|
||||
source_range: crate::executor::SourceRange,
|
||||
cmd: kittycad::types::WebSocketRequest,
|
||||
_id_to_source_range: std::collections::HashMap<uuid::Uuid, crate::executor::SourceRange>,
|
||||
) -> Result<OkWebSocketResponseData, KclError> {
|
||||
) -> Result<WebSocketResponse, KclError> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
// Send the request to the engine, via the actor.
|
||||
@ -257,14 +320,7 @@ impl EngineManager for EngineConnection {
|
||||
}
|
||||
// We pop off the responses to cleanup our mappings.
|
||||
if let Some((_, resp)) = self.responses.remove(&id) {
|
||||
return if let Some(data) = &resp.resp {
|
||||
Ok(data.clone())
|
||||
} else {
|
||||
Err(KclError::Engine(KclErrorDetails {
|
||||
message: format!("Modeling command failed: {:?}", resp.errors),
|
||||
source_ranges: vec![source_range],
|
||||
}))
|
||||
};
|
||||
return Ok(resp);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,13 @@
|
||||
//! Functions for setting up our WebSocket and WebRTC connections for communications with the
|
||||
//! engine.
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use kittycad::types::{OkWebSocketResponseData, WebSocketRequest};
|
||||
use kittycad::types::{OkWebSocketResponseData, WebSocketRequest, WebSocketResponse};
|
||||
|
||||
use crate::{errors::KclError, executor::DefaultPlanes};
|
||||
|
||||
@ -37,13 +40,43 @@ impl crate::engine::EngineManager for EngineConnection {
|
||||
|
||||
async fn inner_send_modeling_cmd(
|
||||
&self,
|
||||
_id: uuid::Uuid,
|
||||
id: uuid::Uuid,
|
||||
_source_range: crate::executor::SourceRange,
|
||||
_cmd: kittycad::types::WebSocketRequest,
|
||||
cmd: kittycad::types::WebSocketRequest,
|
||||
_id_to_source_range: std::collections::HashMap<uuid::Uuid, crate::executor::SourceRange>,
|
||||
) -> Result<OkWebSocketResponseData, KclError> {
|
||||
Ok(OkWebSocketResponseData::Modeling {
|
||||
modeling_response: kittycad::types::OkModelingCmdResponse::Empty {},
|
||||
})
|
||||
) -> Result<WebSocketResponse, KclError> {
|
||||
match cmd {
|
||||
WebSocketRequest::ModelingCmdBatchReq {
|
||||
ref requests,
|
||||
batch_id: _,
|
||||
responses: _,
|
||||
} => {
|
||||
// Create the empty responses.
|
||||
let mut responses = HashMap::new();
|
||||
for request in requests {
|
||||
responses.insert(
|
||||
request.cmd_id.to_string(),
|
||||
kittycad::types::BatchResponse {
|
||||
response: Some(kittycad::types::OkModelingCmdResponse::Empty {}),
|
||||
errors: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
Ok(WebSocketResponse {
|
||||
request_id: Some(id),
|
||||
resp: Some(OkWebSocketResponseData::ModelingBatch { responses }),
|
||||
success: Some(true),
|
||||
errors: None,
|
||||
})
|
||||
}
|
||||
_ => Ok(WebSocketResponse {
|
||||
request_id: Some(id),
|
||||
resp: Some(OkWebSocketResponseData::Modeling {
|
||||
modeling_response: kittycad::types::OkModelingCmdResponse::Empty {},
|
||||
}),
|
||||
success: Some(true),
|
||||
errors: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -130,7 +130,7 @@ impl crate::engine::EngineManager for EngineConnection {
|
||||
source_range: crate::executor::SourceRange,
|
||||
cmd: kittycad::types::WebSocketRequest,
|
||||
id_to_source_range: std::collections::HashMap<uuid::Uuid, crate::executor::SourceRange>,
|
||||
) -> Result<kittycad::types::OkWebSocketResponseData, KclError> {
|
||||
) -> Result<kittycad::types::WebSocketResponse, KclError> {
|
||||
let source_range_str = serde_json::to_string(&source_range).map_err(|e| {
|
||||
KclError::Engine(KclErrorDetails {
|
||||
message: format!("Failed to serialize source range: {:?}", e),
|
||||
@ -182,18 +182,6 @@ impl crate::engine::EngineManager for EngineConnection {
|
||||
})
|
||||
})?;
|
||||
|
||||
if let Some(data) = &ws_result.resp {
|
||||
Ok(data.clone())
|
||||
} else if let Some(errors) = &ws_result.errors {
|
||||
Err(KclError::Engine(KclErrorDetails {
|
||||
message: format!("Modeling command failed: {:?}", errors),
|
||||
source_ranges: vec![source_range],
|
||||
}))
|
||||
} else {
|
||||
Err(KclError::Engine(KclErrorDetails {
|
||||
message: format!("Modeling command failed: {:?}", ws_result),
|
||||
source_ranges: vec![source_range],
|
||||
}))
|
||||
}
|
||||
Ok(ws_result)
|
||||
}
|
||||
}
|
||||
|
@ -47,13 +47,13 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
||||
source_range: crate::executor::SourceRange,
|
||||
cmd: kittycad::types::WebSocketRequest,
|
||||
id_to_source_range: std::collections::HashMap<uuid::Uuid, crate::executor::SourceRange>,
|
||||
) -> Result<kittycad::types::OkWebSocketResponseData, crate::errors::KclError>;
|
||||
) -> Result<kittycad::types::WebSocketResponse, crate::errors::KclError>;
|
||||
|
||||
async fn clear_scene(&self, source_range: crate::executor::SourceRange) -> Result<(), crate::errors::KclError> {
|
||||
self.send_modeling_cmd(
|
||||
self.batch_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
source_range,
|
||||
kittycad::types::ModelingCmd::SceneClearAll {},
|
||||
&kittycad::types::ModelingCmd::SceneClearAll {},
|
||||
)
|
||||
.await?;
|
||||
|
||||
@ -67,12 +67,13 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_modeling_cmd(
|
||||
// Add a modeling command to the batch but don't fire it right away.
|
||||
async fn batch_modeling_cmd(
|
||||
&self,
|
||||
id: uuid::Uuid,
|
||||
source_range: crate::executor::SourceRange,
|
||||
cmd: kittycad::types::ModelingCmd,
|
||||
) -> Result<kittycad::types::OkWebSocketResponseData, crate::errors::KclError> {
|
||||
cmd: &kittycad::types::ModelingCmd,
|
||||
) -> Result<(), crate::errors::KclError> {
|
||||
let req = WebSocketRequest::ModelingCmdReq {
|
||||
cmd: cmd.clone(),
|
||||
cmd_id: id,
|
||||
@ -81,16 +82,17 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
||||
// Add cmd to the batch.
|
||||
self.batch().lock().unwrap().push((req, source_range));
|
||||
|
||||
// If the batch only has this one command that expects a return value,
|
||||
// fire it right away, or if we want to flush batch queue.
|
||||
let is_sending = is_cmd_with_return_values(&cmd);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Return a fake modeling_request empty response.
|
||||
if !is_sending {
|
||||
return Ok(OkWebSocketResponseData::Modeling {
|
||||
modeling_response: kittycad::types::OkModelingCmdResponse::Empty {},
|
||||
});
|
||||
}
|
||||
/// Send the modeling cmd and wait for the response.
|
||||
async fn send_modeling_cmd(
|
||||
&self,
|
||||
id: uuid::Uuid,
|
||||
source_range: crate::executor::SourceRange,
|
||||
cmd: kittycad::types::ModelingCmd,
|
||||
) -> Result<kittycad::types::OkWebSocketResponseData, crate::errors::KclError> {
|
||||
self.batch_modeling_cmd(id, source_range, &cmd).await?;
|
||||
|
||||
// Flush the batch queue.
|
||||
self.flush_batch(source_range).await
|
||||
@ -124,7 +126,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
||||
let batched_requests = WebSocketRequest::ModelingCmdBatchReq {
|
||||
requests,
|
||||
batch_id: uuid::Uuid::new_v4(),
|
||||
responses: false,
|
||||
responses: true,
|
||||
};
|
||||
|
||||
let final_req = if self.batch().lock().unwrap().len() == 1 {
|
||||
@ -155,23 +157,41 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
||||
self.batch().lock().unwrap().clear();
|
||||
|
||||
// We pop off the responses to cleanup our mappings.
|
||||
let id_final = match final_req {
|
||||
match final_req {
|
||||
WebSocketRequest::ModelingCmdBatchReq {
|
||||
requests: _,
|
||||
ref requests,
|
||||
batch_id,
|
||||
responses: _,
|
||||
} => batch_id,
|
||||
WebSocketRequest::ModelingCmdReq { cmd: _, cmd_id } => cmd_id,
|
||||
_ => {
|
||||
return Err(KclError::Engine(KclErrorDetails {
|
||||
message: format!("The final request is not a modeling command: {:?}", final_req),
|
||||
source_ranges: vec![source_range],
|
||||
}));
|
||||
}
|
||||
};
|
||||
} => {
|
||||
// Get the last command ID.
|
||||
let last_id = requests.last().unwrap().cmd_id;
|
||||
let ws_resp = self
|
||||
.inner_send_modeling_cmd(batch_id, source_range, final_req, id_to_source_range.clone())
|
||||
.await?;
|
||||
let response = self.parse_websocket_response(ws_resp, source_range)?;
|
||||
|
||||
self.inner_send_modeling_cmd(id_final, source_range, final_req, id_to_source_range)
|
||||
.await
|
||||
// If we have a batch response, we want to return the specific id we care about.
|
||||
if let kittycad::types::OkWebSocketResponseData::ModelingBatch { responses } = &response {
|
||||
self.parse_batch_responses(last_id, id_to_source_range, responses.clone())
|
||||
} else {
|
||||
// We should never get here.
|
||||
Err(KclError::Engine(KclErrorDetails {
|
||||
message: format!("Failed to get batch response: {:?}", response),
|
||||
source_ranges: vec![source_range],
|
||||
}))
|
||||
}
|
||||
}
|
||||
WebSocketRequest::ModelingCmdReq { cmd: _, cmd_id } => {
|
||||
let ws_resp = self
|
||||
.inner_send_modeling_cmd(cmd_id, source_range, final_req, id_to_source_range)
|
||||
.await?;
|
||||
self.parse_websocket_response(ws_resp, source_range)
|
||||
}
|
||||
_ => Err(KclError::Engine(KclErrorDetails {
|
||||
message: format!("The final request is not a modeling command: {:?}", final_req),
|
||||
source_ranges: vec![source_range],
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
async fn make_default_plane(
|
||||
@ -186,10 +206,10 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
||||
let default_origin = Point3d { x: 0.0, y: 0.0, z: 0.0 }.into();
|
||||
|
||||
let plane_id = uuid::Uuid::new_v4();
|
||||
self.send_modeling_cmd(
|
||||
self.batch_modeling_cmd(
|
||||
plane_id,
|
||||
source_range,
|
||||
ModelingCmd::MakePlane {
|
||||
&ModelingCmd::MakePlane {
|
||||
clobber: false,
|
||||
origin: default_origin,
|
||||
size: default_size,
|
||||
@ -202,10 +222,10 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
||||
|
||||
if let Some(color) = color {
|
||||
// Set the color.
|
||||
self.send_modeling_cmd(
|
||||
self.batch_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
source_range,
|
||||
ModelingCmd::PlaneSetColor { color, plane_id },
|
||||
&ModelingCmd::PlaneSetColor { color, plane_id },
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@ -312,62 +332,79 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
||||
neg_yz: planes[&PlaneName::NegYz],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_cmd_with_return_values(cmd: &kittycad::types::ModelingCmd) -> bool {
|
||||
let (kittycad::types::ModelingCmd::Export { .. }
|
||||
| kittycad::types::ModelingCmd::Extrude { .. }
|
||||
| kittycad::types::ModelingCmd::DefaultCameraLookAt { .. }
|
||||
| kittycad::types::ModelingCmd::DefaultCameraFocusOn { .. }
|
||||
| kittycad::types::ModelingCmd::DefaultCameraGetSettings { .. }
|
||||
| kittycad::types::ModelingCmd::DefaultCameraPerspectiveSettings { .. }
|
||||
| kittycad::types::ModelingCmd::DefaultCameraZoom { .. }
|
||||
| kittycad::types::ModelingCmd::SketchModeDisable { .. }
|
||||
| kittycad::types::ModelingCmd::ObjectBringToFront { .. }
|
||||
| kittycad::types::ModelingCmd::SelectWithPoint { .. }
|
||||
| kittycad::types::ModelingCmd::HighlightSetEntity { .. }
|
||||
| kittycad::types::ModelingCmd::EntityGetChildUuid { .. }
|
||||
| kittycad::types::ModelingCmd::EntityGetNumChildren { .. }
|
||||
| kittycad::types::ModelingCmd::EntityGetParentId { .. }
|
||||
| kittycad::types::ModelingCmd::EntityGetAllChildUuids { .. }
|
||||
| kittycad::types::ModelingCmd::CameraDragMove { .. }
|
||||
| kittycad::types::ModelingCmd::CameraDragEnd { .. }
|
||||
| kittycad::types::ModelingCmd::SelectGet { .. }
|
||||
| kittycad::types::ModelingCmd::Solid3DGetAllEdgeFaces { .. }
|
||||
| kittycad::types::ModelingCmd::Solid3DGetAllOppositeEdges { .. }
|
||||
| kittycad::types::ModelingCmd::Solid3DGetOppositeEdge { .. }
|
||||
| kittycad::types::ModelingCmd::Solid3DGetNextAdjacentEdge { .. }
|
||||
| kittycad::types::ModelingCmd::Solid3DGetPrevAdjacentEdge { .. }
|
||||
| kittycad::types::ModelingCmd::GetEntityType { .. }
|
||||
| kittycad::types::ModelingCmd::CurveGetControlPoints { .. }
|
||||
| kittycad::types::ModelingCmd::CurveGetType { .. }
|
||||
| kittycad::types::ModelingCmd::MouseClick { .. }
|
||||
| kittycad::types::ModelingCmd::TakeSnapshot { .. }
|
||||
| kittycad::types::ModelingCmd::PathGetInfo { .. }
|
||||
| kittycad::types::ModelingCmd::PathGetCurveUuidsForVertices { .. }
|
||||
| kittycad::types::ModelingCmd::PathGetVertexUuids { .. }
|
||||
| kittycad::types::ModelingCmd::CurveGetEndPoints { .. }
|
||||
| kittycad::types::ModelingCmd::FaceIsPlanar { .. }
|
||||
| kittycad::types::ModelingCmd::FaceGetPosition { .. }
|
||||
| kittycad::types::ModelingCmd::FaceGetGradient { .. }
|
||||
| kittycad::types::ModelingCmd::PlaneIntersectAndProject { .. }
|
||||
| kittycad::types::ModelingCmd::ImportFiles { .. }
|
||||
| kittycad::types::ModelingCmd::Mass { .. }
|
||||
| kittycad::types::ModelingCmd::Volume { .. }
|
||||
| kittycad::types::ModelingCmd::Density { .. }
|
||||
| kittycad::types::ModelingCmd::SurfaceArea { .. }
|
||||
| kittycad::types::ModelingCmd::CenterOfMass { .. }
|
||||
| kittycad::types::ModelingCmd::GetSketchModePlane { .. }
|
||||
| kittycad::types::ModelingCmd::EntityGetDistance { .. }
|
||||
| kittycad::types::ModelingCmd::EntityLinearPattern { .. }
|
||||
| kittycad::types::ModelingCmd::EntityCircularPattern { .. }
|
||||
| kittycad::types::ModelingCmd::ZoomToFit { .. }
|
||||
| kittycad::types::ModelingCmd::Solid3DGetExtrusionFaceInfo { .. }) = cmd
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
fn parse_websocket_response(
|
||||
&self,
|
||||
response: kittycad::types::WebSocketResponse,
|
||||
source_range: crate::executor::SourceRange,
|
||||
) -> Result<kittycad::types::OkWebSocketResponseData, crate::errors::KclError> {
|
||||
if let Some(data) = &response.resp {
|
||||
Ok(data.clone())
|
||||
} else if let Some(errors) = &response.errors {
|
||||
Err(KclError::Engine(KclErrorDetails {
|
||||
message: format!("Modeling command failed: {:?}", errors),
|
||||
source_ranges: vec![source_range],
|
||||
}))
|
||||
} else {
|
||||
// We should never get here.
|
||||
Err(KclError::Engine(KclErrorDetails {
|
||||
message: "Modeling command failed: no response or errors".to_string(),
|
||||
source_ranges: vec![source_range],
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
fn parse_batch_responses(
|
||||
&self,
|
||||
// The last response we are looking for.
|
||||
id: uuid::Uuid,
|
||||
// The mapping of source ranges to command IDs.
|
||||
id_to_source_range: std::collections::HashMap<uuid::Uuid, crate::executor::SourceRange>,
|
||||
// The response from the engine.
|
||||
responses: HashMap<String, kittycad::types::BatchResponse>,
|
||||
) -> Result<kittycad::types::OkWebSocketResponseData, crate::errors::KclError> {
|
||||
// Iterate over the responses and check for errors.
|
||||
for (cmd_id, resp) in responses.iter() {
|
||||
let cmd_id = uuid::Uuid::parse_str(cmd_id).map_err(|e| {
|
||||
KclError::Engine(KclErrorDetails {
|
||||
message: format!("Failed to parse command ID: {:?}", e),
|
||||
source_ranges: vec![id_to_source_range[&id]],
|
||||
})
|
||||
})?;
|
||||
|
||||
if let Some(errors) = resp.errors.as_ref() {
|
||||
// Get the source range for the command.
|
||||
let source_range = id_to_source_range.get(&cmd_id).cloned().ok_or_else(|| {
|
||||
KclError::Engine(KclErrorDetails {
|
||||
message: format!("Failed to get source range for command ID: {:?}", cmd_id),
|
||||
source_ranges: vec![],
|
||||
})
|
||||
})?;
|
||||
return Err(KclError::Engine(KclErrorDetails {
|
||||
message: format!("Modeling command failed: {:?}", errors),
|
||||
source_ranges: vec![source_range],
|
||||
}));
|
||||
}
|
||||
if let Some(response) = resp.response.as_ref() {
|
||||
if cmd_id == id {
|
||||
// This is the response we care about.
|
||||
return Ok(kittycad::types::OkWebSocketResponseData::Modeling {
|
||||
modeling_response: response.clone(),
|
||||
});
|
||||
} else {
|
||||
// Continue the loop if this is not the response we care about.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return an error that we did not get an error or the response we wanted.
|
||||
// This should never happen but who knows.
|
||||
Err(KclError::Engine(KclErrorDetails {
|
||||
message: format!("Failed to find response for command ID: {:?}", id),
|
||||
source_ranges: vec![],
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Hash, Eq, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
|
@ -168,3 +168,20 @@ impl From<String> for KclError {
|
||||
serde_json::from_str(&error).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "pyo3")]
|
||||
impl From<pyo3::PyErr> for KclError {
|
||||
fn from(error: pyo3::PyErr) -> Self {
|
||||
KclError::Internal(KclErrorDetails {
|
||||
source_ranges: vec![],
|
||||
message: error.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "pyo3")]
|
||||
impl From<KclError> for pyo3::PyErr {
|
||||
fn from(error: KclError) -> Self {
|
||||
pyo3::exceptions::PyException::new_err(error.to_string())
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ use crate::{
|
||||
engine::EngineManager,
|
||||
errors::{KclError, KclErrorDetails},
|
||||
fs::FileManager,
|
||||
settings::types::UnitLength,
|
||||
std::{FunctionKind, StdLib},
|
||||
};
|
||||
|
||||
@ -616,6 +617,7 @@ impl From<Position> for Point3d {
|
||||
pub struct Rotation(#[ts(type = "[number, number, number, number]")] pub [f64; 4]);
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize, PartialEq, Copy, Clone, ts_rs::TS, JsonSchema, Hash, Eq)]
|
||||
#[cfg_attr(feature = "pyo3", pyo3::pyclass)]
|
||||
#[ts(export)]
|
||||
pub struct SourceRange(#[ts(type = "[number, number]")] pub [usize; 2]);
|
||||
|
||||
@ -992,7 +994,7 @@ pub struct ExecutorContext {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExecutorSettings {
|
||||
/// The unit to use in modeling dimensions.
|
||||
pub units: crate::settings::types::UnitLength,
|
||||
pub units: UnitLength,
|
||||
/// Highlight edges of 3D objects?
|
||||
pub highlight_edges: bool,
|
||||
/// Whether or not Screen Space Ambient Occlusion (SSAO) is enabled.
|
||||
@ -1065,10 +1067,10 @@ impl ExecutorContext {
|
||||
|
||||
// Set the edge visibility.
|
||||
engine
|
||||
.send_modeling_cmd(
|
||||
.batch_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
SourceRange::default(),
|
||||
kittycad::types::ModelingCmd::EdgeLinesVisible {
|
||||
&kittycad::types::ModelingCmd::EdgeLinesVisible {
|
||||
hidden: !settings.highlight_edges,
|
||||
},
|
||||
)
|
||||
@ -1083,6 +1085,57 @@ impl ExecutorContext {
|
||||
})
|
||||
}
|
||||
|
||||
/// For executing unit tests.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub async fn new_for_unit_test(units: UnitLength) -> Result<Self> {
|
||||
let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"),);
|
||||
let http_client = reqwest::Client::builder()
|
||||
.user_agent(user_agent)
|
||||
// For file conversions we need this to be long.
|
||||
.timeout(std::time::Duration::from_secs(600))
|
||||
.connect_timeout(std::time::Duration::from_secs(60));
|
||||
let ws_client = reqwest::Client::builder()
|
||||
.user_agent(user_agent)
|
||||
// For file conversions we need this to be long.
|
||||
.timeout(std::time::Duration::from_secs(600))
|
||||
.connect_timeout(std::time::Duration::from_secs(60))
|
||||
.connection_verbose(true)
|
||||
.tcp_keepalive(std::time::Duration::from_secs(600))
|
||||
.http1_only();
|
||||
|
||||
let token = std::env::var("KITTYCAD_API_TOKEN").expect("KITTYCAD_API_TOKEN not set");
|
||||
|
||||
// Create the client.
|
||||
let mut client = kittycad::Client::new_from_reqwest(token, http_client, ws_client);
|
||||
// Set a local engine address if it's set.
|
||||
if let Ok(addr) = std::env::var("LOCAL_ENGINE_ADDR") {
|
||||
client.set_base_url(addr);
|
||||
}
|
||||
|
||||
let ctx = ExecutorContext::new(
|
||||
&client,
|
||||
ExecutorSettings {
|
||||
units,
|
||||
highlight_edges: true,
|
||||
enable_ssao: false,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(ctx)
|
||||
}
|
||||
|
||||
/// Clear everything in the scene.
|
||||
pub async fn reset_scene(&self) -> Result<()> {
|
||||
self.engine
|
||||
.send_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
SourceRange::default(),
|
||||
kittycad::types::ModelingCmd::SceneClearAll {},
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Perform the execution of a program.
|
||||
/// You can optionally pass in some initialization memory.
|
||||
/// Kurt uses this for partial execution.
|
||||
@ -1093,11 +1146,11 @@ impl ExecutorContext {
|
||||
) -> Result<ProgramMemory, KclError> {
|
||||
// Before we even start executing the program, set the units.
|
||||
self.engine
|
||||
.send_modeling_cmd(
|
||||
.batch_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
SourceRange::default(),
|
||||
kittycad::types::ModelingCmd::SetSceneUnits {
|
||||
unit: self.settings.units.clone().into(),
|
||||
&kittycad::types::ModelingCmd::SetSceneUnits {
|
||||
unit: self.settings.units.into(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
@ -1309,7 +1362,7 @@ impl ExecutorContext {
|
||||
}
|
||||
|
||||
/// Update the units for the executor.
|
||||
pub fn update_units(&mut self, units: crate::settings::types::UnitLength) {
|
||||
pub fn update_units(&mut self, units: UnitLength) {
|
||||
self.settings.units = units;
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,13 @@
|
||||
//! the standard library implementation, a LSP implementation, generator for the docs, and more.
|
||||
#![recursion_limit = "1024"]
|
||||
|
||||
macro_rules! println {
|
||||
($($rest:tt)*) => {
|
||||
#[cfg(not(feature = "disable-println"))]
|
||||
std::println!($($rest)*)
|
||||
}
|
||||
}
|
||||
|
||||
pub mod ast;
|
||||
pub mod coredump;
|
||||
pub mod docs;
|
||||
@ -16,6 +23,7 @@ pub mod lsp;
|
||||
pub mod parser;
|
||||
pub mod settings;
|
||||
pub mod std;
|
||||
pub mod test_server;
|
||||
pub mod thread;
|
||||
pub mod token;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
|
@ -1,10 +1,13 @@
|
||||
use super::Node;
|
||||
use crate::ast::types::{
|
||||
BinaryPart, BodyItem, LiteralIdentifier, MemberExpression, MemberObject, ObjectExpression, ObjectProperty,
|
||||
Parameter, Program, UnaryExpression, Value, VariableDeclarator,
|
||||
};
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::{
|
||||
ast::types::{
|
||||
BinaryPart, BodyItem, LiteralIdentifier, MemberExpression, MemberObject, ObjectExpression, ObjectProperty,
|
||||
Parameter, Program, UnaryExpression, Value, VariableDeclarator,
|
||||
},
|
||||
lint::Node,
|
||||
};
|
||||
|
||||
/// Walker is implemented by things that are able to walk an AST tree to
|
||||
/// produce lints. This trait is implemented automatically for a few of the
|
||||
/// common types, but can be manually implemented too.
|
||||
|
@ -1,3 +1,5 @@
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::{
|
||||
ast::types::VariableDeclarator,
|
||||
executor::SourceRange,
|
||||
@ -7,8 +9,6 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
def_finding!(
|
||||
Z0001,
|
||||
"Identifiers must be lowerCamelCase",
|
||||
|
@ -1,9 +1,9 @@
|
||||
mod ast_node;
|
||||
mod ast_walk;
|
||||
pub mod checks;
|
||||
mod rule;
|
||||
pub mod rule;
|
||||
|
||||
pub use ast_node::Node;
|
||||
pub use ast_walk::walk;
|
||||
// pub(crate) use rule::{def_finding, finding};
|
||||
pub use rule::{lint, Discovered, Finding};
|
||||
pub use rule::{Discovered, Finding};
|
||||
|
@ -1,9 +1,8 @@
|
||||
use super::{walk, Node};
|
||||
use crate::{ast::types::Program, executor::SourceRange, lsp::IntoDiagnostic};
|
||||
use anyhow::Result;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity};
|
||||
|
||||
use crate::{executor::SourceRange, lint::Node, lsp::IntoDiagnostic};
|
||||
|
||||
/// Check the provided AST for any found rule violations.
|
||||
///
|
||||
/// The Rule trait is automatically implemented for a few other types,
|
||||
@ -24,6 +23,7 @@ where
|
||||
|
||||
/// Specific discovered lint rule Violation of a particular Finding.
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(feature = "pyo3", pyo3::pyclass)]
|
||||
pub struct Discovered {
|
||||
/// Zoo Lint Finding information.
|
||||
pub finding: Finding,
|
||||
@ -38,6 +38,30 @@ pub struct Discovered {
|
||||
pub overridden: bool,
|
||||
}
|
||||
|
||||
#[cfg(feature = "pyo3")]
|
||||
#[pyo3::pymethods]
|
||||
impl Discovered {
|
||||
#[getter]
|
||||
pub fn finding(&self) -> Finding {
|
||||
self.finding.clone()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
pub fn description(&self) -> String {
|
||||
self.description.clone()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
pub fn pos(&self) -> SourceRange {
|
||||
self.pos
|
||||
}
|
||||
|
||||
#[getter]
|
||||
pub fn overridden(&self) -> bool {
|
||||
self.overridden
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDiagnostic for Discovered {
|
||||
fn to_lsp_diagnostic(&self, code: &str) -> Diagnostic {
|
||||
let message = self.finding.title.to_owned();
|
||||
@ -60,6 +84,7 @@ impl IntoDiagnostic for Discovered {
|
||||
|
||||
/// Abstract lint problem type.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "pyo3", pyo3::pyclass)]
|
||||
pub struct Finding {
|
||||
/// Unique identifier for this particular issue.
|
||||
pub code: &'static str,
|
||||
@ -86,6 +111,30 @@ impl Finding {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "pyo3")]
|
||||
#[pyo3::pymethods]
|
||||
impl Finding {
|
||||
#[getter]
|
||||
pub fn code(&self) -> &'static str {
|
||||
self.code
|
||||
}
|
||||
|
||||
#[getter]
|
||||
pub fn title(&self) -> &'static str {
|
||||
self.title
|
||||
}
|
||||
|
||||
#[getter]
|
||||
pub fn description(&self) -> &'static str {
|
||||
self.description
|
||||
}
|
||||
|
||||
#[getter]
|
||||
pub fn experimental(&self) -> bool {
|
||||
self.experimental
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! def_finding {
|
||||
( $code:ident, $title:expr, $description:expr ) => {
|
||||
/// Generated Finding
|
||||
@ -105,25 +154,9 @@ macro_rules! finding {
|
||||
};
|
||||
}
|
||||
pub(crate) use finding;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) use test::{assert_finding, assert_no_finding, test_finding, test_no_finding};
|
||||
|
||||
/// Check the provided Program for any Findings.
|
||||
pub fn lint<'a, RuleT>(prog: &'a Program, rule: RuleT) -> Result<Vec<Discovered>>
|
||||
where
|
||||
RuleT: Rule<'a>,
|
||||
{
|
||||
let v = Arc::new(Mutex::new(vec![]));
|
||||
walk(prog, &|node: Node<'a>| {
|
||||
let mut findings = v.lock().map_err(|_| anyhow::anyhow!("mutex"))?;
|
||||
findings.append(&mut rule.check(node)?);
|
||||
Ok(true)
|
||||
})?;
|
||||
let x = v.lock().unwrap();
|
||||
Ok(x.clone())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
@ -132,7 +165,7 @@ mod test {
|
||||
let tokens = $crate::token::lexer($kcl).unwrap();
|
||||
let parser = $crate::parser::Parser::new(tokens);
|
||||
let prog = parser.ast().unwrap();
|
||||
for discovered_finding in $crate::lint::lint(&prog, $check).unwrap() {
|
||||
for discovered_finding in prog.lint($check).unwrap() {
|
||||
if discovered_finding.finding == $finding {
|
||||
assert!(false, "Finding {:?} was emitted", $finding.code);
|
||||
}
|
||||
@ -146,7 +179,7 @@ mod test {
|
||||
let parser = $crate::parser::Parser::new(tokens);
|
||||
let prog = parser.ast().unwrap();
|
||||
|
||||
for discovered_finding in $crate::lint::lint(&prog, $check).unwrap() {
|
||||
for discovered_finding in prog.lint($check).unwrap() {
|
||||
if discovered_finding.finding == $finding {
|
||||
return;
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ use super::backend::{InnerHandle, UpdateHandle};
|
||||
use crate::{
|
||||
ast::types::VariableKind,
|
||||
executor::SourceRange,
|
||||
lint::{checks, lint},
|
||||
lint::checks,
|
||||
lsp::{backend::Backend as _, safemap::SafeMap, util::IntoDiagnostic},
|
||||
parser::PIPE_OPERATOR,
|
||||
};
|
||||
@ -257,7 +257,7 @@ impl crate::lsp::backend::Backend for Backend {
|
||||
return;
|
||||
}
|
||||
|
||||
for discovered_finding in lint(&ast, checks::lint_variables).into_iter().flatten() {
|
||||
for discovered_finding in ast.lint(checks::lint_variables).into_iter().flatten() {
|
||||
self.add_to_diagnostics(¶ms, discovered_finding).await;
|
||||
}
|
||||
}
|
||||
@ -370,6 +370,11 @@ impl Backend {
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
self.client.publish_diagnostics(uri.clone(), vec![], None).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn add_to_diagnostics<DiagT: IntoDiagnostic + std::fmt::Debug>(
|
||||
|
@ -2907,7 +2907,10 @@ let myBox = box([0,0], -3, -16, -10)
|
||||
let tokens = crate::token::lexer(some_program_string).unwrap();
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let err = parser.ast().unwrap_err();
|
||||
println!("{err}")
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([30, 36])], message: "All expressions in a pipeline must use the % (substitution operator)" }"#
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -405,7 +405,10 @@ impl From<bool> for DefaultTrue {
|
||||
}
|
||||
|
||||
/// The valid types of length units.
|
||||
#[derive(Debug, Default, Eq, PartialEq, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, Display, FromStr)]
|
||||
#[derive(
|
||||
Debug, Default, Eq, PartialEq, Copy, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, Display, FromStr,
|
||||
)]
|
||||
#[cfg_attr(feature = "pyo3", pyo3::pyclass)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[display(style = "lowercase")]
|
||||
|
@ -19,8 +19,8 @@ pub(crate) const DEFAULT_TOLERANCE: f64 = 0.0000001;
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChamferData {
|
||||
/// The radius of the chamfer.
|
||||
pub radius: f64,
|
||||
/// The length of the chamfer.
|
||||
pub length: f64,
|
||||
/// The tags of the paths you want to chamfer.
|
||||
pub tags: Vec<EdgeReference>,
|
||||
}
|
||||
@ -50,7 +50,7 @@ pub async fn chamfer(args: Args) -> Result<MemoryItem, KclError> {
|
||||
/// const width = 20
|
||||
/// const length = 10
|
||||
/// const thickness = 1
|
||||
/// const chamferRadius = 2
|
||||
/// const chamferLength = 2
|
||||
///
|
||||
/// const mountingPlateSketch = startSketchOn("XY")
|
||||
/// |> startProfileAt([-width/2, -length/2], %)
|
||||
@ -61,7 +61,7 @@ pub async fn chamfer(args: Args) -> Result<MemoryItem, KclError> {
|
||||
///
|
||||
/// const mountingPlate = extrude(thickness, mountingPlateSketch)
|
||||
/// |> chamfer({
|
||||
/// radius: chamferRadius,
|
||||
/// length: chamferLength,
|
||||
/// tags: [
|
||||
/// getNextAdjacentEdge('edge1', %),
|
||||
/// getNextAdjacentEdge('edge2', %),
|
||||
@ -109,12 +109,12 @@ async fn inner_chamfer(
|
||||
}
|
||||
};
|
||||
|
||||
args.send_modeling_cmd(
|
||||
args.batch_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
ModelingCmd::Solid3DFilletEdge {
|
||||
edge_id,
|
||||
object_id: extrude_group.id,
|
||||
radius: data.radius,
|
||||
radius: data.length,
|
||||
tolerance: DEFAULT_TOLERANCE, // We can let the user set this in the future.
|
||||
cut_type: Some(kittycad::types::CutType::Chamfer),
|
||||
},
|
||||
|
@ -111,13 +111,13 @@ pub(crate) async fn do_post_extrude(
|
||||
// We need to do this after extrude for sketch on face.
|
||||
if let SketchSurface::Face(_) = sketch_group.on {
|
||||
// Disable the sketch mode.
|
||||
args.send_modeling_cmd(uuid::Uuid::new_v4(), kittycad::types::ModelingCmd::SketchModeDisable {})
|
||||
args.batch_modeling_cmd(uuid::Uuid::new_v4(), kittycad::types::ModelingCmd::SketchModeDisable {})
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Bring the object to the front of the scene.
|
||||
// See: https://github.com/KittyCAD/modeling-app/issues/806
|
||||
args.send_modeling_cmd(
|
||||
args.batch_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
kittycad::types::ModelingCmd::ObjectBringToFront {
|
||||
object_id: sketch_group.id,
|
||||
|
@ -110,7 +110,7 @@ async fn inner_fillet(
|
||||
}
|
||||
};
|
||||
|
||||
args.send_modeling_cmd(
|
||||
args.batch_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
ModelingCmd::Solid3DFilletEdge {
|
||||
edge_id,
|
||||
|
@ -59,7 +59,7 @@ async fn inner_helix(
|
||||
args: Args,
|
||||
) -> Result<Box<ExtrudeGroup>, KclError> {
|
||||
let id = uuid::Uuid::new_v4();
|
||||
args.send_modeling_cmd(
|
||||
args.batch_modeling_cmd(
|
||||
id,
|
||||
ModelingCmd::EntityMakeHelix {
|
||||
cylinder_id: extrude_group.id,
|
||||
|
@ -1,6 +1,7 @@
|
||||
use std::path::Path;
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
|
||||
use crate::{
|
||||
ast::types::{BodyItem, FunctionExpression, Program, Value},
|
||||
|
@ -215,6 +215,16 @@ impl Args {
|
||||
}
|
||||
}
|
||||
|
||||
// Add a modeling command to the batch but don't fire it right away.
|
||||
pub async fn batch_modeling_cmd(
|
||||
&self,
|
||||
id: uuid::Uuid,
|
||||
cmd: kittycad::types::ModelingCmd,
|
||||
) -> Result<(), crate::errors::KclError> {
|
||||
self.ctx.engine.batch_modeling_cmd(id, self.source_range, &cmd).await
|
||||
}
|
||||
|
||||
/// Send the modeling cmd and wait for the response.
|
||||
pub async fn send_modeling_cmd(
|
||||
&self,
|
||||
id: uuid::Uuid,
|
||||
|
@ -242,7 +242,7 @@ async fn inner_revolve(
|
||||
match data.axis {
|
||||
RevolveAxis::Axis(axis) => {
|
||||
let (axis, origin) = axis.axis_and_origin()?;
|
||||
args.send_modeling_cmd(
|
||||
args.batch_modeling_cmd(
|
||||
id,
|
||||
ModelingCmd::Revolve {
|
||||
angle,
|
||||
@ -274,7 +274,7 @@ async fn inner_revolve(
|
||||
.id
|
||||
}
|
||||
};
|
||||
args.send_modeling_cmd(
|
||||
args.batch_modeling_cmd(
|
||||
id,
|
||||
ModelingCmd::RevolveAboutEdge {
|
||||
angle,
|
||||
|
@ -4,11 +4,10 @@ use anyhow::Result;
|
||||
use derive_docs::stdlib;
|
||||
use schemars::JsonSchema;
|
||||
|
||||
use super::utils::between;
|
||||
use crate::{
|
||||
errors::{KclError, KclErrorDetails},
|
||||
executor::{MemoryItem, SketchGroup},
|
||||
std::Args,
|
||||
std::{utils::between, Args},
|
||||
};
|
||||
|
||||
/// Returns the segment end of x.
|
||||
|
@ -48,7 +48,6 @@ pub async fn circle(args: Args) -> Result<MemoryItem, KclError> {
|
||||
/// |> hole(circle([0, 15], 5, %), %)
|
||||
///
|
||||
/// const example = extrude(5, exampleSketch)
|
||||
///
|
||||
#[stdlib {
|
||||
name = "circle",
|
||||
}]
|
||||
|
@ -126,7 +126,7 @@ async fn inner_shell(
|
||||
}));
|
||||
}
|
||||
|
||||
args.send_modeling_cmd(
|
||||
args.batch_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
ModelingCmd::Solid3DShellFace {
|
||||
face_ids,
|
||||
|
@ -55,7 +55,7 @@ async fn inner_line_to(
|
||||
let from = sketch_group.current_pen_position()?;
|
||||
let id = uuid::Uuid::new_v4();
|
||||
|
||||
args.send_modeling_cmd(
|
||||
args.batch_modeling_cmd(
|
||||
id,
|
||||
ModelingCmd::ExtendPath {
|
||||
path: sketch_group.id,
|
||||
@ -217,7 +217,7 @@ async fn inner_line(
|
||||
|
||||
let id = uuid::Uuid::new_v4();
|
||||
|
||||
args.send_modeling_cmd(
|
||||
args.batch_modeling_cmd(
|
||||
id,
|
||||
ModelingCmd::ExtendPath {
|
||||
path: sketch_group.id,
|
||||
@ -409,7 +409,7 @@ async fn inner_angled_line(
|
||||
},
|
||||
};
|
||||
|
||||
args.send_modeling_cmd(
|
||||
args.batch_modeling_cmd(
|
||||
id,
|
||||
ModelingCmd::ExtendPath {
|
||||
path: sketch_group.id,
|
||||
@ -1071,7 +1071,7 @@ async fn start_sketch_on_face(
|
||||
|
||||
// Enter sketch mode on the face.
|
||||
let id = uuid::Uuid::new_v4();
|
||||
args.send_modeling_cmd(
|
||||
args.batch_modeling_cmd(
|
||||
id,
|
||||
ModelingCmd::EnableSketchMode {
|
||||
animated: false,
|
||||
@ -1117,7 +1117,7 @@ async fn start_sketch_on_plane(data: PlaneData, args: Args) -> Result<Box<Plane>
|
||||
} => {
|
||||
// Create the custom plane on the fly.
|
||||
let id = uuid::Uuid::new_v4();
|
||||
args.send_modeling_cmd(
|
||||
args.batch_modeling_cmd(
|
||||
id,
|
||||
ModelingCmd::MakePlane {
|
||||
clobber: false,
|
||||
@ -1135,7 +1135,7 @@ async fn start_sketch_on_plane(data: PlaneData, args: Args) -> Result<Box<Plane>
|
||||
};
|
||||
|
||||
// Enter sketch mode on the plane.
|
||||
args.send_modeling_cmd(
|
||||
args.batch_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
ModelingCmd::EnableSketchMode {
|
||||
animated: false,
|
||||
@ -1205,8 +1205,8 @@ pub(crate) async fn inner_start_profile_at(
|
||||
let id = uuid::Uuid::new_v4();
|
||||
let path_id = uuid::Uuid::new_v4();
|
||||
|
||||
args.send_modeling_cmd(path_id, ModelingCmd::StartPath {}).await?;
|
||||
args.send_modeling_cmd(
|
||||
args.batch_modeling_cmd(path_id, ModelingCmd::StartPath {}).await?;
|
||||
args.batch_modeling_cmd(
|
||||
id,
|
||||
ModelingCmd::MovePathPen {
|
||||
path: path_id,
|
||||
@ -1359,7 +1359,7 @@ pub(crate) async fn inner_close(
|
||||
|
||||
let id = uuid::Uuid::new_v4();
|
||||
|
||||
args.send_modeling_cmd(
|
||||
args.batch_modeling_cmd(
|
||||
id,
|
||||
ModelingCmd::ClosePath {
|
||||
path_id: sketch_group.id,
|
||||
@ -1370,7 +1370,7 @@ pub(crate) async fn inner_close(
|
||||
// If we are sketching on a plane we can close the sketch group now.
|
||||
if let SketchSurface::Plane(_) = sketch_group.on {
|
||||
// We were on a plane, disable the sketch mode.
|
||||
args.send_modeling_cmd(uuid::Uuid::new_v4(), kittycad::types::ModelingCmd::SketchModeDisable {})
|
||||
args.batch_modeling_cmd(uuid::Uuid::new_v4(), kittycad::types::ModelingCmd::SketchModeDisable {})
|
||||
.await?;
|
||||
}
|
||||
|
||||
@ -1436,7 +1436,6 @@ pub async fn arc(args: Args) -> Result<MemoryItem, KclError> {
|
||||
/// radius: 16
|
||||
/// }, %)
|
||||
/// |> close(%)
|
||||
///
|
||||
// const example = extrude(10, exampleSketch)
|
||||
/// ```
|
||||
#[stdlib {
|
||||
@ -1469,7 +1468,7 @@ pub(crate) async fn inner_arc(
|
||||
|
||||
let id = uuid::Uuid::new_v4();
|
||||
|
||||
args.send_modeling_cmd(
|
||||
args.batch_modeling_cmd(
|
||||
id,
|
||||
ModelingCmd::ExtendPath {
|
||||
path: sketch_group.id,
|
||||
@ -1565,7 +1564,7 @@ async fn inner_tangential_arc(
|
||||
let start_angle = Angle::from_degrees(0.0);
|
||||
let (_, to) = arc_center_and_end(from, start_angle, end_angle, *radius);
|
||||
|
||||
args.send_modeling_cmd(
|
||||
args.batch_modeling_cmd(
|
||||
id,
|
||||
ModelingCmd::ExtendPath {
|
||||
path: sketch_group.id,
|
||||
@ -1582,7 +1581,7 @@ async fn inner_tangential_arc(
|
||||
to.into()
|
||||
}
|
||||
TangentialArcData::Point(to) => {
|
||||
args.send_modeling_cmd(id, tan_arc_to(&sketch_group, to)).await?;
|
||||
args.batch_modeling_cmd(id, tan_arc_to(&sketch_group, to)).await?;
|
||||
|
||||
*to
|
||||
}
|
||||
@ -1692,7 +1691,7 @@ async fn inner_tangential_arc_to(
|
||||
|
||||
let delta = [to_x - from.x, to_y - from.y];
|
||||
let id = uuid::Uuid::new_v4();
|
||||
args.send_modeling_cmd(id, tan_arc_to(&sketch_group, &delta)).await?;
|
||||
args.batch_modeling_cmd(id, tan_arc_to(&sketch_group, &delta)).await?;
|
||||
|
||||
let current_path = Path::TangentialArcTo {
|
||||
base: BasePath {
|
||||
@ -1769,7 +1768,7 @@ async fn inner_bezier_curve(
|
||||
|
||||
let id = uuid::Uuid::new_v4();
|
||||
|
||||
args.send_modeling_cmd(
|
||||
args.batch_modeling_cmd(
|
||||
id,
|
||||
ModelingCmd::ExtendPath {
|
||||
path: sketch_group.id,
|
||||
@ -1864,7 +1863,7 @@ async fn inner_hole(
|
||||
|
||||
match hole_sketch_group {
|
||||
SketchGroupSet::SketchGroup(hole_sketch_group) => {
|
||||
args.send_modeling_cmd(
|
||||
args.batch_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
ModelingCmd::Solid2DAddHole {
|
||||
object_id: sketch_group.id,
|
||||
@ -1874,7 +1873,7 @@ async fn inner_hole(
|
||||
.await?;
|
||||
// suggestion (mike)
|
||||
// we also hide the source hole since its essentially "consumed" by this operation
|
||||
args.send_modeling_cmd(
|
||||
args.batch_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
ModelingCmd::ObjectVisible {
|
||||
object_id: hole_sketch_group.id,
|
||||
@ -1885,7 +1884,7 @@ async fn inner_hole(
|
||||
}
|
||||
SketchGroupSet::SketchGroups(hole_sketch_groups) => {
|
||||
for hole_sketch_group in hole_sketch_groups {
|
||||
args.send_modeling_cmd(
|
||||
args.batch_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
ModelingCmd::Solid2DAddHole {
|
||||
object_id: sketch_group.id,
|
||||
@ -1895,7 +1894,7 @@ async fn inner_hole(
|
||||
.await?;
|
||||
// suggestion (mike)
|
||||
// we also hide the source hole since its essentially "consumed" by this operation
|
||||
args.send_modeling_cmd(
|
||||
args.batch_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
ModelingCmd::ObjectVisible {
|
||||
object_id: hole_sketch_group.id,
|
||||
|
8
src/wasm-lib/kcl/src/test_server.rs
Normal file
@ -0,0 +1,8 @@
|
||||
//! Types used to send data to the test server.
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
pub struct RequestBody {
|
||||
pub kcl_program: String,
|
||||
#[serde(default)]
|
||||
pub test_name: String,
|
||||
}
|
@ -13,6 +13,7 @@ mod tokeniser;
|
||||
|
||||
/// The types of tokens.
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone, Deserialize, Serialize, ts_rs::TS, JsonSchema, FromStr, Display)]
|
||||
#[cfg_attr(feature = "pyo3", pyo3::pyclass)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[display(style = "camelCase")]
|
||||
@ -142,6 +143,7 @@ impl TokenType {
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone, ts_rs::TS)]
|
||||
#[cfg_attr(feature = "pyo3", pyo3::pyclass)]
|
||||
#[ts(export)]
|
||||
pub struct Token {
|
||||
#[serde(rename = "type")]
|
||||
|
@ -28,12 +28,22 @@ pub async fn execute_wasm(
|
||||
let memory: kcl_lib::executor::ProgramMemory = serde_json::from_str(memory_str).map_err(|e| e.to_string())?;
|
||||
let units = kcl_lib::settings::types::UnitLength::from_str(units).map_err(|e| e.to_string())?;
|
||||
|
||||
let engine = kcl_lib::engine::conn_wasm::EngineConnection::new(engine_manager)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))?;
|
||||
let engine: std::sync::Arc<Box<dyn kcl_lib::engine::EngineManager>> = if is_mock {
|
||||
Arc::new(Box::new(
|
||||
kcl_lib::engine::conn_mock::EngineConnection::new()
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))?,
|
||||
))
|
||||
} else {
|
||||
Arc::new(Box::new(
|
||||
kcl_lib::engine::conn_wasm::EngineConnection::new(engine_manager)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))?,
|
||||
))
|
||||
};
|
||||
let fs = Arc::new(kcl_lib::fs::FileManager::new(fs_manager));
|
||||
let ctx = kcl_lib::executor::ExecutorContext {
|
||||
engine: Arc::new(Box::new(engine)),
|
||||
engine,
|
||||
fs,
|
||||
stdlib: std::sync::Arc::new(kcl_lib::std::StdLib::new()),
|
||||
settings: ExecutorSettings {
|
||||
|
316
src/wasm-lib/tests/cordump/inputs/coredump.fixture.json
Normal file
@ -0,0 +1,316 @@
|
||||
{
|
||||
"version": "0.20.1",
|
||||
"git_rev": "3a05211d306ca045ace2e7bf10b7f8138e1daad5",
|
||||
"timestamp": "2024-05-07T20:06:34.655Z",
|
||||
"tauri": false,
|
||||
"os": {
|
||||
"platform": "Mac OS",
|
||||
"version": "10.15.7",
|
||||
"browser": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
|
||||
},
|
||||
"webrtc_stats": {
|
||||
"packets_lost": 0,
|
||||
"frames_received": 672,
|
||||
"frame_width": 1440.0,
|
||||
"frame_height": 712.0,
|
||||
"frame_rate": 58.0,
|
||||
"key_frames_decoded": 7,
|
||||
"frames_dropped": 77,
|
||||
"pause_count": 0,
|
||||
"total_pauses_duration": 0.0,
|
||||
"freeze_count": 12,
|
||||
"total_freezes_duration": 3.057,
|
||||
"pli_count": 6,
|
||||
"jitter": 0.011
|
||||
},
|
||||
"pool": "",
|
||||
"client_state": {
|
||||
"engine_command_manager": {
|
||||
"artifact_map": {
|
||||
"ac7a8c52-7437-42e6-ae2a-25c54d0b4a16": {
|
||||
"type": "result",
|
||||
"range": [0, 0],
|
||||
"pathToNode": [],
|
||||
"commandType": "make_plane",
|
||||
"data": { "type": "empty" },
|
||||
"raw": {
|
||||
"success": true,
|
||||
"request_id": "79cc6dfc-7205-41b2-922f-0d0279f5a30d",
|
||||
"resp": {
|
||||
"type": "modeling",
|
||||
"data": { "modeling_response": { "type": "empty" } }
|
||||
}
|
||||
}
|
||||
},
|
||||
"83a2e866-d8e1-47d9-afae-d55a64cd5b40": {
|
||||
"type": "result",
|
||||
"range": [0, 0],
|
||||
"pathToNode": [],
|
||||
"commandType": "plane_set_color",
|
||||
"data": { "type": "empty" },
|
||||
"raw": {
|
||||
"success": true,
|
||||
"request_id": "79cc6dfc-7205-41b2-922f-0d0279f5a30d",
|
||||
"resp": {
|
||||
"type": "modeling",
|
||||
"data": { "modeling_response": { "type": "empty" } }
|
||||
}
|
||||
}
|
||||
},
|
||||
"c92d7e84-a03e-456e-aad0-3e302ae0ffb6": {
|
||||
"type": "result",
|
||||
"range": [0, 0],
|
||||
"pathToNode": [],
|
||||
"commandType": "make_plane",
|
||||
"data": { "type": "empty" },
|
||||
"raw": {
|
||||
"success": true,
|
||||
"request_id": "79cc6dfc-7205-41b2-922f-0d0279f5a30d",
|
||||
"resp": {
|
||||
"type": "modeling",
|
||||
"data": { "modeling_response": { "type": "empty" } }
|
||||
}
|
||||
}
|
||||
},
|
||||
"a9abb9b8-54b1-4042-8ab3-e67c7ddb4cb3": {
|
||||
"type": "result",
|
||||
"range": [0, 0],
|
||||
"pathToNode": [],
|
||||
"commandType": "plane_set_color",
|
||||
"data": { "type": "empty" },
|
||||
"raw": {
|
||||
"success": true,
|
||||
"request_id": "79cc6dfc-7205-41b2-922f-0d0279f5a30d",
|
||||
"resp": {
|
||||
"type": "modeling",
|
||||
"data": { "modeling_response": { "type": "empty" } }
|
||||
}
|
||||
}
|
||||
},
|
||||
"bafb884d-ee93-48f5-a667-91d1bdb8178a": {
|
||||
"type": "result",
|
||||
"range": [0, 0],
|
||||
"pathToNode": [],
|
||||
"commandType": "make_plane",
|
||||
"data": { "type": "empty" },
|
||||
"raw": {
|
||||
"success": true,
|
||||
"request_id": "79cc6dfc-7205-41b2-922f-0d0279f5a30d",
|
||||
"resp": {
|
||||
"type": "modeling",
|
||||
"data": { "modeling_response": { "type": "empty" } }
|
||||
}
|
||||
}
|
||||
},
|
||||
"6b7f539b-c4ca-4e62-a971-147e1abaca7b": {
|
||||
"type": "result",
|
||||
"range": [0, 0],
|
||||
"pathToNode": [],
|
||||
"commandType": "plane_set_color",
|
||||
"data": { "type": "empty" },
|
||||
"raw": {
|
||||
"success": true,
|
||||
"request_id": "79cc6dfc-7205-41b2-922f-0d0279f5a30d",
|
||||
"resp": {
|
||||
"type": "modeling",
|
||||
"data": { "modeling_response": { "type": "empty" } }
|
||||
}
|
||||
}
|
||||
},
|
||||
"6ddaaaa3-b080-4cb3-b08e-8fe277312ccc": {
|
||||
"type": "result",
|
||||
"range": [0, 0],
|
||||
"pathToNode": [],
|
||||
"commandType": "make_plane",
|
||||
"data": { "type": "empty" },
|
||||
"raw": {
|
||||
"success": true,
|
||||
"request_id": "79cc6dfc-7205-41b2-922f-0d0279f5a30d",
|
||||
"resp": {
|
||||
"type": "modeling",
|
||||
"data": { "modeling_response": { "type": "empty" } }
|
||||
}
|
||||
}
|
||||
},
|
||||
"f1ef28b0-49b3-45f8-852d-772648149785": {
|
||||
"type": "result",
|
||||
"range": [0, 0],
|
||||
"pathToNode": [],
|
||||
"commandType": "make_plane",
|
||||
"data": { "type": "empty" },
|
||||
"raw": {
|
||||
"success": true,
|
||||
"request_id": "79cc6dfc-7205-41b2-922f-0d0279f5a30d",
|
||||
"resp": {
|
||||
"type": "modeling",
|
||||
"data": { "modeling_response": { "type": "empty" } }
|
||||
}
|
||||
}
|
||||
},
|
||||
"7c532be8-f07d-456b-8643-53808df86823": {
|
||||
"type": "result",
|
||||
"range": [0, 0],
|
||||
"pathToNode": [],
|
||||
"commandType": "make_plane",
|
||||
"data": { "type": "empty" },
|
||||
"raw": {
|
||||
"success": true,
|
||||
"request_id": "79cc6dfc-7205-41b2-922f-0d0279f5a30d",
|
||||
"resp": {
|
||||
"type": "modeling",
|
||||
"data": { "modeling_response": { "type": "empty" } }
|
||||
}
|
||||
}
|
||||
},
|
||||
"cf38f826-6d15-45f4-9c64-a6e9e4909e75": {
|
||||
"type": "result",
|
||||
"range": [0, 0],
|
||||
"pathToNode": [],
|
||||
"commandType": "make_plane",
|
||||
"data": { "type": "empty" },
|
||||
"raw": {
|
||||
"success": true,
|
||||
"request_id": "91f1be72-4267-40a4-9923-9c5b5a0ec29c",
|
||||
"resp": {
|
||||
"type": "modeling",
|
||||
"data": { "modeling_response": { "type": "empty" } }
|
||||
}
|
||||
}
|
||||
},
|
||||
"d69e6248-9e0f-4bc8-bc6e-d995931e8886": {
|
||||
"type": "result",
|
||||
"range": [0, 0],
|
||||
"pathToNode": [],
|
||||
"commandType": "plane_set_color",
|
||||
"data": { "type": "empty" },
|
||||
"raw": {
|
||||
"success": true,
|
||||
"request_id": "91f1be72-4267-40a4-9923-9c5b5a0ec29c",
|
||||
"resp": {
|
||||
"type": "modeling",
|
||||
"data": { "modeling_response": { "type": "empty" } }
|
||||
}
|
||||
}
|
||||
},
|
||||
"cdb44075-ac6d-4626-b321-26d022f5f7f0": {
|
||||
"type": "result",
|
||||
"range": [0, 0],
|
||||
"pathToNode": [],
|
||||
"commandType": "make_plane",
|
||||
"data": { "type": "empty" },
|
||||
"raw": {
|
||||
"success": true,
|
||||
"request_id": "91f1be72-4267-40a4-9923-9c5b5a0ec29c",
|
||||
"resp": {
|
||||
"type": "modeling",
|
||||
"data": { "modeling_response": { "type": "empty" } }
|
||||
}
|
||||
}
|
||||
},
|
||||
"44f81391-0f2d-49f3-92b9-7dfc8446d571": {
|
||||
"type": "result",
|
||||
"range": [0, 0],
|
||||
"pathToNode": [],
|
||||
"commandType": "plane_set_color",
|
||||
"data": { "type": "empty" },
|
||||
"raw": {
|
||||
"success": true,
|
||||
"request_id": "91f1be72-4267-40a4-9923-9c5b5a0ec29c",
|
||||
"resp": {
|
||||
"type": "modeling",
|
||||
"data": { "modeling_response": { "type": "empty" } }
|
||||
}
|
||||
}
|
||||
},
|
||||
"7b893ecf-8887-49c3-b978-392813d5f625": {
|
||||
"type": "result",
|
||||
"range": [0, 0],
|
||||
"pathToNode": [],
|
||||
"commandType": "make_plane",
|
||||
"data": { "type": "empty" },
|
||||
"raw": {
|
||||
"success": true,
|
||||
"request_id": "91f1be72-4267-40a4-9923-9c5b5a0ec29c",
|
||||
"resp": {
|
||||
"type": "modeling",
|
||||
"data": { "modeling_response": { "type": "empty" } }
|
||||
}
|
||||
}
|
||||
},
|
||||
"61247d28-04ba-4768-85b0-fbc582151dbd": {
|
||||
"type": "result",
|
||||
"range": [0, 0],
|
||||
"pathToNode": [],
|
||||
"commandType": "plane_set_color",
|
||||
"data": { "type": "empty" },
|
||||
"raw": {
|
||||
"success": true,
|
||||
"request_id": "91f1be72-4267-40a4-9923-9c5b5a0ec29c",
|
||||
"resp": {
|
||||
"type": "modeling",
|
||||
"data": { "modeling_response": { "type": "empty" } }
|
||||
}
|
||||
}
|
||||
},
|
||||
"1318f5af-76a9-4161-9f6a-d410831fd098": {
|
||||
"type": "result",
|
||||
"range": [0, 0],
|
||||
"pathToNode": [],
|
||||
"commandType": "make_plane",
|
||||
"data": { "type": "empty" },
|
||||
"raw": {
|
||||
"success": true,
|
||||
"request_id": "91f1be72-4267-40a4-9923-9c5b5a0ec29c",
|
||||
"resp": {
|
||||
"type": "modeling",
|
||||
"data": { "modeling_response": { "type": "empty" } }
|
||||
}
|
||||
}
|
||||
},
|
||||
"908d264c-5989-4030-b2f4-5dfb52fda71a": {
|
||||
"type": "result",
|
||||
"range": [0, 0],
|
||||
"pathToNode": [],
|
||||
"commandType": "make_plane",
|
||||
"data": { "type": "empty" },
|
||||
"raw": {
|
||||
"success": true,
|
||||
"request_id": "91f1be72-4267-40a4-9923-9c5b5a0ec29c",
|
||||
"resp": {
|
||||
"type": "modeling",
|
||||
"data": { "modeling_response": { "type": "empty" } }
|
||||
}
|
||||
}
|
||||
},
|
||||
"0de4c253-ee21-4385-93bd-b93264f7eb60": {
|
||||
"type": "result",
|
||||
"range": [0, 0],
|
||||
"pathToNode": [],
|
||||
"commandType": "make_plane",
|
||||
"data": { "type": "empty" },
|
||||
"raw": {
|
||||
"success": true,
|
||||
"request_id": "91f1be72-4267-40a4-9923-9c5b5a0ec29c",
|
||||
"resp": {
|
||||
"type": "modeling",
|
||||
"data": { "modeling_response": { "type": "empty" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"engine_connection": {
|
||||
"state": {
|
||||
"type": "connection-established"
|
||||
}
|
||||
}
|
||||
},
|
||||
"kcl_manager": {},
|
||||
"scene_infra": {},
|
||||
"auth_machine": {},
|
||||
"command_bar_machine": {},
|
||||
"file_machine": {},
|
||||
"home_machine": {},
|
||||
"modeling_machine": {},
|
||||
"settings_machine": {}
|
||||
}
|
||||
}
|
@ -459,7 +459,7 @@ async fn serial_test_execute_engine_error_return() {
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.err().unwrap().to_string(),
|
||||
r#"engine: KclErrorDetails { source_ranges: [SourceRange([222, 235])], message: "Modeling command failed: Some([ApiError { error_code: BadRequest, message: \"The path is not closed. Solid2D construction requires a closed path!\" }])" }"#,
|
||||
r#"engine: KclErrorDetails { source_ranges: [SourceRange([222, 235])], message: "Modeling command failed: [ApiError { error_code: BadRequest, message: \"The path is not closed. Solid2D construction requires a closed path!\" }]" }"#,
|
||||
);
|
||||
}
|
||||
|
||||
@ -1614,7 +1614,7 @@ const sketch001 = startSketchOn(box, "revolveAxis")
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.err().unwrap().to_string(),
|
||||
r#"engine: KclErrorDetails { source_ranges: [SourceRange([349, 409])], message: "Modeling command failed: Some([ApiError { error_code: InternalEngine, message: \"Solid3D revolve failed: sketch profile must lie entirely on one side of the revolution axis\" }])" }"#
|
||||
r#"engine: KclErrorDetails { source_ranges: [SourceRange([349, 409])], message: "Modeling command failed: [ApiError { error_code: InternalEngine, message: \"Solid3D revolve failed: sketch profile must lie entirely on one side of the revolution axis\" }]" }"#
|
||||
);
|
||||
}
|
||||
|
||||
@ -1873,7 +1873,7 @@ const bracket = startSketchOn('XY')
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.err().unwrap().to_string(),
|
||||
r#"engine: KclErrorDetails { source_ranges: [SourceRange([1443, 1443])], message: "Modeling command failed: Some([ApiError { error_code: BadRequest, message: \"Fillet failed\" }])" }"#
|
||||
r#"engine: KclErrorDetails { source_ranges: [SourceRange([1443, 1443])], message: "Modeling command failed: [ApiError { error_code: BadRequest, message: \"Fillet failed\" }]" }"#
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -26,7 +26,7 @@ const config = defineConfig({
|
||||
coverage: {
|
||||
provider: 'istanbul', // or 'v8'
|
||||
},
|
||||
exclude: [...configDefaults.exclude, '**/e2e/playwright/**/*'],
|
||||
exclude: [...configDefaults.exclude, '**/e2e/**/*'],
|
||||
deps: {
|
||||
optimizer: {
|
||||
web: {
|
||||
|