Compare commits
36 Commits
v0.22.2
...
achalmers/
Author | SHA1 | Date | |
---|---|---|---|
eda736a85e | |||
abbfdae7d2 | |||
ddbdd9094c | |||
7954b6da96 | |||
bdb84ab3c1 | |||
54e160e8d2 | |||
2c5a8d439f | |||
47a5e1f6d3 | |||
d85211c5a4 | |||
1beb6b5186 | |||
17978ab1d7 | |||
a1bcad9dfb | |||
2e7bdf02cf | |||
6f76196b72 | |||
e7af064518 | |||
674d49e2ae | |||
4cb48674c6 | |||
82daec2aff | |||
f1ef9d5200 | |||
dc226d3270 | |||
7bf50d8fe0 | |||
b26764bc9a | |||
1b0c6298d7 | |||
fe9a483726 | |||
bd42ea037b | |||
fdb1b21af3 | |||
630ef316b8 | |||
e322926be9 | |||
a9e61da8b5 | |||
e2a835a437 | |||
c61273085f | |||
a79e365c0f | |||
2386ba24e5 | |||
e42a891df8 | |||
98200565bf | |||
570fd827ed |
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
|
||||
|
8
.github/workflows/playwright.yml
vendored
@ -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
|
||||
|
1
.gitignore
vendored
@ -17,6 +17,7 @@
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.direnv
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
|
@ -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
|
||||
@ -38,9 +42,9 @@ document.addEventListener('mousemove', (e) =>
|
||||
const deg = (Math.PI * 2) / 360
|
||||
|
||||
const commonPoints = {
|
||||
startAt: '[9.06, -12.22]',
|
||||
num1: 9.14,
|
||||
num2: 18.2,
|
||||
startAt: '[7.19, -9.7]',
|
||||
num1: 7.25,
|
||||
num2: 14.44,
|
||||
// num1: 9.64,
|
||||
// num2: 19.19,
|
||||
}
|
||||
@ -99,7 +103,7 @@ test('Basic sketch', async ({ page }) => {
|
||||
)
|
||||
await u.closeDebugPanel()
|
||||
|
||||
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
|
||||
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
||||
|
||||
const startXPx = 600
|
||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||
@ -118,13 +122,13 @@ test('Basic sketch', async ({ page }) => {
|
||||
await expect(u.codeLocator).toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
|> line([${commonPoints.num1}, 0], %)
|
||||
|> line([0, ${commonPoints.num1}], %)`)
|
||||
|> line([0, ${commonPoints.num1 + 0.01}], %)`)
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||
await expect(u.codeLocator).toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
|> line([${commonPoints.num1}, 0], %)
|
||||
|> line([0, ${commonPoints.num1}], %)
|
||||
|> line([0, ${commonPoints.num1 + 0.01}], %)
|
||||
|> line([-${commonPoints.num2}, 0], %)`)
|
||||
|
||||
// deselect line tool
|
||||
@ -154,10 +158,11 @@ test('Basic sketch', async ({ page }) => {
|
||||
await expect(u.codeLocator).toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
|> line([${commonPoints.num1}, 0], %, 'seg01')
|
||||
|> line([0, ${commonPoints.num1}], %)
|
||||
|> line([0, ${commonPoints.num1 + 0.01}], %)
|
||||
|> angledLine([180, segLen('seg01', %)], %)`)
|
||||
})
|
||||
|
||||
test.describe('Testing Camera Movement', () => {
|
||||
test('Can moving camera', async ({ page, context }) => {
|
||||
test.skip(process.platform === 'darwin', 'Can moving camera')
|
||||
const u = await getUtils(page)
|
||||
@ -320,6 +325,147 @@ test('Can moving camera', async ({ page, context }) => {
|
||||
}, [1, -68, -68])
|
||||
})
|
||||
|
||||
test('Zoom should be consistent when exiting or entering sketches', async ({
|
||||
page,
|
||||
}) => {
|
||||
// start new sketch pan and zoom before exiting, when exiting the sketch should stay in the same place
|
||||
// than zoom and pan outside of sketch mode and enter again and it should not change from where it is
|
||||
// than again for sketching
|
||||
|
||||
test.skip(process.platform !== 'darwin', 'Zoom should be consistent')
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await page.goto('/')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await u.openDebugPanel()
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).not.toBeDisabled()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).toBeVisible()
|
||||
|
||||
// click on "Start Sketch" button
|
||||
await u.clearCommandLogs()
|
||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
// select a plane
|
||||
await page.mouse.click(700, 325)
|
||||
|
||||
let code = `const sketch001 = startSketchOn('XY')`
|
||||
await expect(u.codeLocator).toHaveText(code)
|
||||
await u.closeDebugPanel()
|
||||
|
||||
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
||||
|
||||
// move the camera slightly
|
||||
await page.keyboard.down('Shift')
|
||||
await page.mouse.move(700, 300)
|
||||
await page.mouse.down({ button: 'right' })
|
||||
await page.mouse.move(800, 200)
|
||||
await page.mouse.up({ button: 'right' })
|
||||
await page.keyboard.up('Shift')
|
||||
|
||||
let y = 350,
|
||||
x = 948
|
||||
|
||||
await u.canvasLocator.click({ position: { x: 783, y } })
|
||||
code += `\n |> startProfileAt([8.12, -12.98], %)`
|
||||
// await expect(u.codeLocator).toHaveText(code)
|
||||
await u.canvasLocator.click({ position: { x, y } })
|
||||
code += `\n |> line([11.18, 0], %)`
|
||||
// await expect(u.codeLocator).toHaveText(code)
|
||||
await u.canvasLocator.click({ position: { x, y: 275 } })
|
||||
code += `\n |> line([0, 6.99], %)`
|
||||
// await expect(u.codeLocator).toHaveText(code)
|
||||
|
||||
// click the line button
|
||||
await page.getByRole('button', { name: 'Line' }).click()
|
||||
|
||||
const hoverOverNothing = async () => {
|
||||
// await u.canvasLocator.hover({position: {x: 700, y: 325}})
|
||||
await page.mouse.move(700, 325)
|
||||
await page.waitForTimeout(100)
|
||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||
}
|
||||
|
||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||
|
||||
await page.waitForTimeout(100)
|
||||
// hover over horizontal line
|
||||
await u.canvasLocator.hover({ position: { x: 800, y } })
|
||||
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
||||
|
||||
await hoverOverNothing()
|
||||
// hover over vertical line
|
||||
await u.canvasLocator.hover({ position: { x, y: 325 } })
|
||||
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
||||
|
||||
await hoverOverNothing()
|
||||
|
||||
// click exit sketch
|
||||
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||
await page.waitForTimeout(400)
|
||||
|
||||
await hoverOverNothing()
|
||||
await page.waitForTimeout(100)
|
||||
// hover over horizontal line
|
||||
await page.mouse.move(858, y, { steps: 5 })
|
||||
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
||||
|
||||
await hoverOverNothing()
|
||||
|
||||
// hover over vertical line
|
||||
await page.mouse.move(x, 325)
|
||||
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
||||
|
||||
await hoverOverNothing()
|
||||
|
||||
// hover over vertical line
|
||||
await page.mouse.move(857, y)
|
||||
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
||||
// now click it
|
||||
await page.mouse.click(857, y)
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Edit Sketch' })
|
||||
).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||
|
||||
await page.waitForTimeout(400)
|
||||
|
||||
await hoverOverNothing()
|
||||
x = 975
|
||||
y = 468
|
||||
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.move(x, 419, { steps: 5 })
|
||||
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
||||
|
||||
await hoverOverNothing()
|
||||
|
||||
await page.mouse.move(855, y)
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
|
||||
|
||||
await hoverOverNothing()
|
||||
|
||||
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||
await page.waitForTimeout(400)
|
||||
|
||||
await hoverOverNothing()
|
||||
|
||||
await page.mouse.move(x, 419)
|
||||
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
||||
|
||||
await hoverOverNothing()
|
||||
|
||||
await page.mouse.move(855, y)
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test('if you click the format button it formats your code', async ({
|
||||
page,
|
||||
}) => {
|
||||
@ -452,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 })
|
||||
@ -744,7 +925,7 @@ const sketchOnPlaneAndBackSideTest = async (
|
||||
}
|
||||
|
||||
const code = `const sketch001 = startSketchOn('${plane}')
|
||||
|> startProfileAt([1.14, -1.54], %)`
|
||||
|> startProfileAt([0.9, -1.22], %)`
|
||||
|
||||
await u.openDebugPanel()
|
||||
|
||||
@ -1110,6 +1291,31 @@ test('Keyboard shortcuts can be viewed through the help menu', async ({
|
||||
).toBeAttached()
|
||||
})
|
||||
|
||||
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 }) => {
|
||||
// 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)
|
||||
|
||||
@ -1193,6 +1399,108 @@ test('Onboarding redirects and code updating', async ({ page }) => {
|
||||
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', () => {
|
||||
test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
// tests mapping works on fresh sketch and edited sketch
|
||||
@ -1245,28 +1553,25 @@ test.describe('Testing selections', () => {
|
||||
.toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
|> line([${commonPoints.num1}, 0], %)
|
||||
|> line([0, ${commonPoints.num1}], %)`)
|
||||
|> line([0, ${commonPoints.num1 + 0.01}], %)`)
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
|> line([${commonPoints.num1}, 0], %)
|
||||
|> line([0, ${commonPoints.num1}], %)
|
||||
|> line([0, ${commonPoints.num1 + 0.01}], %)
|
||||
|> line([-${commonPoints.num2}, 0], %)`)
|
||||
|
||||
// deselect line tool
|
||||
await page.getByRole('button', { name: 'Line' }).click()
|
||||
|
||||
await u.closeDebugPanel()
|
||||
const selectionSequence = async (isSecondTime = false) => {
|
||||
const selectionSequence = async () => {
|
||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.move(
|
||||
startXPx + PUR * 15,
|
||||
isSecondTime ? 430 : 500 - PUR * 10
|
||||
)
|
||||
await page.mouse.move(startXPx + PUR * 15, 500 - PUR * 10)
|
||||
|
||||
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
||||
// bg-yellow-200 is more brittle than hover-highlight, but is closer to the user experience
|
||||
@ -1276,10 +1581,7 @@ test.describe('Testing selections', () => {
|
||||
// check mousing off, than mousing onto another line
|
||||
await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 15) // mouse off
|
||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||
await page.mouse.move(
|
||||
startXPx + PUR * 10,
|
||||
isSecondTime ? 295 : 500 - PUR * 20
|
||||
) // mouse onto another line
|
||||
await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 20) // mouse onto another line
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
|
||||
|
||||
// now check clicking works including axis
|
||||
@ -1376,8 +1678,33 @@ test.describe('Testing selections', () => {
|
||||
|
||||
await page.waitForTimeout(300) // wait for animation
|
||||
|
||||
await u.openAndClearDebugPanel()
|
||||
await u.sendCustomCmd({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_look_at',
|
||||
center: { x: 0, y: 0, z: 0 },
|
||||
vantage: { x: 0, y: -1378.01, z: 0 },
|
||||
up: { x: 0, y: 0, z: 1 },
|
||||
},
|
||||
})
|
||||
await page.waitForTimeout(100)
|
||||
await u.sendCustomCmd({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_get_settings',
|
||||
},
|
||||
})
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await emptySpaceClick()
|
||||
|
||||
await u.closeDebugPanel()
|
||||
|
||||
// hover again and check it works
|
||||
await selectionSequence(true)
|
||||
await selectionSequence()
|
||||
})
|
||||
|
||||
test('Hovering over 3d features highlights code', async ({ page }) => {
|
||||
@ -1471,6 +1798,74 @@ test.describe('Testing selections', () => {
|
||||
await page.waitForTimeout(100)
|
||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||
})
|
||||
test("Extrude button should be disabled if there's no extrudable geometry when nothing is selected", 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], %)
|
||||
|> line([3.75, 0.46], %)
|
||||
|> line([4.99, -0.46], %, 'seg01')
|
||||
|> line([3.3, -2.12], %)
|
||||
|> line([2.16, -3.33], %)
|
||||
|> line([0.85, -3.08], %)
|
||||
|> line([-0.18, -3.36], %)
|
||||
|> line([-3.86, -2.73], %)
|
||||
|> line([-17.67, 0.85], %)
|
||||
|> close(%)
|
||||
const extrude001 = extrude(10, sketch001)
|
||||
`
|
||||
)
|
||||
})
|
||||
await page.setViewportSize({ width: 1000, height: 500 })
|
||||
await page.goto('/')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
// wait for execution done
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.closeDebugPanel()
|
||||
|
||||
const selectUnExtrudable = () =>
|
||||
page.getByText(`line([4.99, -0.46], %, 'seg01')`).click()
|
||||
const clickEmpty = () => page.mouse.click(700, 460)
|
||||
await selectUnExtrudable()
|
||||
// expect extrude button to be disabled
|
||||
await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled()
|
||||
|
||||
await clickEmpty()
|
||||
|
||||
// expect active line to contain nothing
|
||||
await expect(page.locator('.cm-activeLine')).toHaveText('')
|
||||
// and extrude to still be disabled
|
||||
await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled()
|
||||
|
||||
const codeToAdd = `${await u.codeLocator.allInnerTexts()}
|
||||
const sketch002 = startSketchOn(extrude001, 'seg01')
|
||||
|> startProfileAt([-12.94, 6.6], %)
|
||||
|> line([2.45, -0.2], %)
|
||||
|> line([-2, -1.25], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
`
|
||||
await u.codeLocator.fill(codeToAdd)
|
||||
|
||||
await selectUnExtrudable()
|
||||
// expect extrude button to be disabled
|
||||
await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled()
|
||||
|
||||
await clickEmpty()
|
||||
await expect(page.locator('.cm-activeLine')).toHaveText('')
|
||||
// there's not extrudable geometry, so button should be enabled
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Extrude' })
|
||||
).not.toBeDisabled()
|
||||
})
|
||||
|
||||
test('Testing selections (and hovers) work on sketches when NOT in sketch mode', async ({
|
||||
page,
|
||||
@ -1761,6 +2156,7 @@ test('Can add multiple sketches', async ({ page }) => {
|
||||
await expect(u.codeLocator).toHaveText(codeStr)
|
||||
|
||||
await click00r(50, 0)
|
||||
await page.waitForTimeout(100)
|
||||
codeStr += ` |> line(${toSU([50, 0])}, %)`
|
||||
await expect(u.codeLocator).toHaveText(codeStr)
|
||||
|
||||
@ -1785,26 +2181,26 @@ test('Can add multiple sketches', async ({ page }) => {
|
||||
|
||||
// when exiting the sketch above the camera is still looking down at XY,
|
||||
// so selecting the plane again is a bit easier.
|
||||
await page.mouse.click(center.x + 30, center.y)
|
||||
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
||||
await page.mouse.click(center.x + 200, center.y + 100)
|
||||
await page.waitForTimeout(600) // TODO detect animation ending, or disable animation
|
||||
codeStr += "const sketch002 = startSketchOn('XY')"
|
||||
await expect(u.codeLocator).toHaveText(codeStr)
|
||||
await u.closeDebugPanel()
|
||||
|
||||
await click00r(30, 0)
|
||||
codeStr += ` |> startProfileAt(${toSU([30, 0])}, %)`
|
||||
codeStr += ` |> startProfileAt([1.53, 0], %)`
|
||||
await expect(u.codeLocator).toHaveText(codeStr)
|
||||
|
||||
await click00r(30, 0)
|
||||
codeStr += ` |> line(${toSU([30 + 0.1 /* imprecision */, 0])}, %)`
|
||||
codeStr += ` |> line([1.53, 0], %)`
|
||||
await expect(u.codeLocator).toHaveText(codeStr)
|
||||
|
||||
await click00r(0, 30)
|
||||
codeStr += ` |> line(${toSU([0, 30])}, %)`
|
||||
codeStr += ` |> line([0, -1.53], %)`
|
||||
await expect(u.codeLocator).toHaveText(codeStr)
|
||||
|
||||
await click00r(-30, 0)
|
||||
codeStr += ` |> line(${toSU([-30 - 0.1, 0])}, %)`
|
||||
codeStr += ` |> line([-1.53, 0], %)`
|
||||
await expect(u.codeLocator).toHaveText(codeStr)
|
||||
|
||||
click00r(undefined, undefined)
|
||||
@ -1812,7 +2208,6 @@ test('Can add multiple sketches', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.updateCamPosition([100, 100, 100])
|
||||
await page.waitForTimeout(250)
|
||||
await u.clearCommandLogs()
|
||||
})
|
||||
|
||||
@ -2217,9 +2612,9 @@ const doSnapAtDifferentScales = async (
|
||||
await u.openDebugPanel()
|
||||
|
||||
const code = `const sketch001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([${roundOff(scale * 87.68)}, ${roundOff(scale * 43.84)}], %)
|
||||
|> line([${roundOff(scale * 175.36)}, 0], %)
|
||||
|> line([0, -${roundOff(scale * 175.36) + fudge}], %)
|
||||
|> startProfileAt([${roundOff(scale * 69.6)}, ${roundOff(scale * 34.8)}], %)
|
||||
|> line([${roundOff(scale * 139.19)}, 0], %)
|
||||
|> line([0, -${roundOff(scale * 139.2)}], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)`
|
||||
|
||||
@ -2249,6 +2644,7 @@ const doSnapAtDifferentScales = async (
|
||||
const pointC = [900, 400]
|
||||
|
||||
// draw three lines
|
||||
await page.waitForTimeout(500)
|
||||
await page.mouse.click(pointA[0], pointA[1])
|
||||
await page.waitForTimeout(100)
|
||||
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
||||
@ -2384,11 +2780,8 @@ test('Sketch on face', async ({ page }) => {
|
||||
|
||||
await page.getByText('startProfileAt([-12.94, 6.6], %)').click()
|
||||
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible()
|
||||
await u.doAndWaitForCmd(
|
||||
() => page.getByRole('button', { name: 'Edit Sketch' }).click(),
|
||||
'default_camera_get_settings',
|
||||
true
|
||||
)
|
||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||
await page.waitForTimeout(400)
|
||||
await page.waitForTimeout(150)
|
||||
await page.setViewportSize({ width: 1200, height: 1200 })
|
||||
await u.openAndClearDebugPanel()
|
||||
@ -4658,13 +5051,15 @@ test('Basic default modeling and sketch hotkeys work', async ({ page }) => {
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Exit Sketch' })
|
||||
).not.toBeVisible()
|
||||
await page.waitForTimeout(400)
|
||||
|
||||
// Extrude
|
||||
await page.mouse.click(750, 150)
|
||||
await expect(extrudeButton).not.toBeDisabled()
|
||||
await page.keyboard.press('e')
|
||||
await page.mouse.move(730, 230, { steps: 5 })
|
||||
await page.mouse.click(730, 230)
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.move(900, 200, { steps: 5 })
|
||||
await page.mouse.click(900, 200)
|
||||
await page.waitForTimeout(100)
|
||||
await page.getByRole('button', { name: 'Continue' }).click()
|
||||
await page.getByRole('button', { name: 'Submit command' }).click()
|
||||
@ -4768,7 +5163,7 @@ test('Engine disconnect & reconnect in sketch mode', async ({ page }) => {
|
||||
)
|
||||
await u.closeDebugPanel()
|
||||
|
||||
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
|
||||
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
||||
|
||||
const startXPx = 600
|
||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||
@ -4847,15 +5242,15 @@ test('Engine disconnect & reconnect in sketch mode', async ({ page }) => {
|
||||
.toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
|> line([${commonPoints.num1}, 0], %)
|
||||
|> line([-11.64, 11.11], %)`)
|
||||
|> line([-9.16, 8.81], %)`)
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
|> line([${commonPoints.num1}, 0], %)
|
||||
|> line([-11.64, 11.11], %)
|
||||
|> line([-6.56, 0], %)`)
|
||||
|> line([-9.16, 8.81], %)
|
||||
|> line([-5.28, 0], %)`)
|
||||
|
||||
// Unequip line tool
|
||||
await page.keyboard.press('Escape')
|
||||
|
@ -405,17 +405,16 @@ test('Draft segments should look right', async ({ page, context }) => {
|
||||
// select a plane
|
||||
await page.mouse.click(700, 200)
|
||||
|
||||
await expect(page.locator('.cm-content')).toHaveText(
|
||||
`const sketch001 = startSketchOn('XZ')`
|
||||
)
|
||||
let code = `const sketch001 = startSketchOn('XZ')`
|
||||
await expect(page.locator('.cm-content')).toHaveText(code)
|
||||
|
||||
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
|
||||
await page.waitForTimeout(700) // TODO detect animation ending, or disable animation
|
||||
|
||||
const startXPx = 600
|
||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([9.06, -12.22], %)`)
|
||||
code += `
|
||||
|> startProfileAt([7.19, -9.7], %)`
|
||||
await expect(page.locator('.cm-content')).toHaveText(code)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await u.closeDebugPanel()
|
||||
@ -427,10 +426,9 @@ test('Draft segments should look right', async ({ page, context }) => {
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([9.06, -12.22], %)
|
||||
|> line([9.14, 0], %)`)
|
||||
code += `
|
||||
|> line([7.25, 0], %)`
|
||||
await expect(page.locator('.cm-content')).toHaveText(code)
|
||||
|
||||
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
||||
|
||||
@ -513,17 +511,16 @@ test.describe('Client side scene scale should match engine scale', () => {
|
||||
// select a plane
|
||||
await page.mouse.click(700, 200)
|
||||
|
||||
await expect(page.locator('.cm-content')).toHaveText(
|
||||
`const sketch001 = startSketchOn('XZ')`
|
||||
)
|
||||
let code = `const sketch001 = startSketchOn('XZ')`
|
||||
await expect(page.locator('.cm-content')).toHaveText(code)
|
||||
|
||||
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
|
||||
await page.waitForTimeout(600) // TODO detect animation ending, or disable animation
|
||||
|
||||
const startXPx = 600
|
||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([9.06, -12.22], %)`)
|
||||
code += `
|
||||
|> startProfileAt([7.19, -9.7], %)`
|
||||
await expect(u.codeLocator).toHaveText(code)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await u.closeDebugPanel()
|
||||
@ -531,21 +528,18 @@ test.describe('Client side scene scale should match engine scale', () => {
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([9.06, -12.22], %)
|
||||
|> line([9.14, 0], %)`)
|
||||
code += `
|
||||
|> line([7.25, 0], %)`
|
||||
await expect(u.codeLocator).toHaveText(code)
|
||||
|
||||
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([9.06, -12.22], %)
|
||||
|> line([9.14, 0], %)
|
||||
|> tangentialArcTo([27.34, -3.08], %)`)
|
||||
code += `
|
||||
|> tangentialArcTo([21.7, -2.44], %)`
|
||||
await expect(u.codeLocator).toHaveText(code)
|
||||
|
||||
// click tangential arc tool again to unequip it
|
||||
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
||||
@ -616,17 +610,16 @@ test.describe('Client side scene scale should match engine scale', () => {
|
||||
// select a plane
|
||||
await page.mouse.click(700, 200)
|
||||
|
||||
await expect(page.locator('.cm-content')).toHaveText(
|
||||
`const sketch001 = startSketchOn('XZ')`
|
||||
)
|
||||
let code = `const sketch001 = startSketchOn('XZ')`
|
||||
await expect(u.codeLocator).toHaveText(code)
|
||||
|
||||
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
|
||||
await page.waitForTimeout(600) // TODO detect animation ending, or disable animation
|
||||
|
||||
const startXPx = 600
|
||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([230.03, -310.32], %)`)
|
||||
code += `
|
||||
|> startProfileAt([182.59, -246.32], %)`
|
||||
await expect(u.codeLocator).toHaveText(code)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await u.closeDebugPanel()
|
||||
@ -634,21 +627,18 @@ test.describe('Client side scene scale should match engine scale', () => {
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([230.03, -310.32], %)
|
||||
|> line([232.2, 0], %)`)
|
||||
code += `
|
||||
|> line([184.3, 0], %)`
|
||||
await expect(u.codeLocator).toHaveText(code)
|
||||
|
||||
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([230.03, -310.32], %)
|
||||
|> line([232.2, 0], %)
|
||||
|> tangentialArcTo([694.43, -78.12], %)`)
|
||||
code += `
|
||||
|> tangentialArcTo([551.2, -62.01], %)`
|
||||
await expect(u.codeLocator).toHaveText(code)
|
||||
|
||||
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
||||
await page.waitForTimeout(100)
|
||||
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 36 KiB |
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 = {
|
||||
|
@ -132,8 +132,8 @@ export const getMovementUtils = (opts: any) => {
|
||||
// NOTE: these pretty much can't be perfect because of screen scaling.
|
||||
// Handle on a case-by-case.
|
||||
const toU = (x: number, y: number) => [
|
||||
kcRound(x * 0.0854),
|
||||
kcRound(-y * 0.0854), // Y is inverted in our coordinate system
|
||||
kcRound(x * 0.0678),
|
||||
kcRound(-y * 0.0678), // Y is inverted in our coordinate system
|
||||
]
|
||||
|
||||
// Turn the array into a string with specific formatting
|
||||
@ -226,6 +226,7 @@ export async function getUtils(page: Page) {
|
||||
.boundingBox()
|
||||
.then((box) => ({ ...box, x: box?.x || 0, y: box?.y || 0 })),
|
||||
codeLocator: page.locator('.cm-content'),
|
||||
canvasLocator: page.getByTestId('client-side-scene'),
|
||||
doAndWaitForCmd: async (
|
||||
fn: () => Promise<void>,
|
||||
commandType: string,
|
||||
|
@ -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)
|
||||
}
|
62
flake.lock
generated
Normal file
@ -0,0 +1,62 @@
|
||||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1718470082,
|
||||
"narHash": "sha256-u2F0MMYE+Efc+ocruTbtU/wWHuYHWcJafp5zJ++n/YE=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "3027ba73dfef68eb555fc2fa97aed4e999e74f97",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1718428119,
|
||||
"narHash": "sha256-WdWDpNaq6u1IPtxtYHHWpl5BmabtpmLnMAx0RdJ/vo8=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e6cea36f83499eb4e9cd184c8a8e823296b50ad5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1718681902,
|
||||
"narHash": "sha256-E/T7Ge6ayEQe7FVKMJqDBoHyLhRhjc6u9CmU8MyYfy0=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "16c8ad83297c278eebe740dea5491c1708960dd1",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
70
flake.nix
Normal file
@ -0,0 +1,70 @@
|
||||
{
|
||||
description = "modeling-app development environment";
|
||||
|
||||
# Flake inputs
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
rust-overlay.url = "github:oxalica/rust-overlay"; # A helper for Rust + Nix
|
||||
};
|
||||
|
||||
# Flake outputs
|
||||
outputs = { self, nixpkgs, rust-overlay }:
|
||||
let
|
||||
# Overlays enable you to customize the Nixpkgs attribute set
|
||||
overlays = [
|
||||
# Makes a `rust-bin` attribute available in Nixpkgs
|
||||
(import rust-overlay)
|
||||
# Provides a `rustToolchain` attribute for Nixpkgs that we can use to
|
||||
# create a Rust environment
|
||||
(self: super: {
|
||||
rustToolchain = super. rust-bin.stable.latest.default.override {
|
||||
targets = [ "wasm32-unknown-unknown" ];
|
||||
extensions = [ "rustfmt" "llvm-tools-preview" ];
|
||||
};
|
||||
})
|
||||
];
|
||||
|
||||
# Systems supported
|
||||
allSystems = [
|
||||
"x86_64-linux" # 64-bit Intel/AMD Linux
|
||||
"aarch64-linux" # 64-bit ARM Linux
|
||||
"x86_64-darwin" # 64-bit Intel macOS
|
||||
"aarch64-darwin" # 64-bit ARM macOS
|
||||
];
|
||||
|
||||
# Helper to provide system-specific attributes
|
||||
forAllSystems = f: nixpkgs.lib.genAttrs allSystems (system: f {
|
||||
pkgs = import nixpkgs { inherit overlays system; };
|
||||
});
|
||||
|
||||
in
|
||||
{
|
||||
# Development environment output
|
||||
devShells = forAllSystems ({ pkgs }: {
|
||||
default = pkgs.mkShell {
|
||||
# The Nix packages provided in the environment
|
||||
packages = (with pkgs; [
|
||||
# The package provided by our custom overlay. Includes cargo, Clippy, cargo-fmt,
|
||||
# rustdoc, rustfmt, and other tools.
|
||||
rustToolchain
|
||||
|
||||
cargo-llvm-cov
|
||||
cargo-nextest
|
||||
|
||||
just
|
||||
postgresql.lib
|
||||
openssl
|
||||
pkg-config
|
||||
|
||||
nodejs_22
|
||||
]) ++ pkgs.lib.optionals pkgs.stdenv.isDarwin (with pkgs; [
|
||||
libiconv
|
||||
darwin.apple_sdk.frameworks.Security
|
||||
]);
|
||||
|
||||
TARGET_CC = "${pkgs.stdenv.cc}/bin/${pkgs.stdenv.cc.targetPrefix}cc";
|
||||
LIBCLANG_PATH = "${pkgs.libclang.lib}/lib";
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "untitled-app",
|
||||
"version": "0.22.2",
|
||||
"version": "0.22.3",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.16.0",
|
||||
|
258
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"
|
||||
@ -2748,9 +2618,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kittycad"
|
||||
version = "0.3.5"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df75feef10313fa1cb15b7cecd0f579877312ba3d42bb5b8b4c1d4b1d0fcabf0"
|
||||
checksum = "af3de9bb4b1441f198689a9f64a8163a518377e30b348a784680e738985b95eb"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -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"
|
||||
}
|
||||
|
@ -127,7 +127,7 @@ export function App() {
|
||||
/>
|
||||
<ModalContainer />
|
||||
<ModelingSidebar paneOpacity={paneOpacity} />
|
||||
<Stream className="absolute inset-0 z-0" />
|
||||
<Stream />
|
||||
{/* <CamToggle /> */}
|
||||
<LowerRightControls>
|
||||
<Gizmo />
|
||||
|
@ -174,41 +174,6 @@ export class CameraControls {
|
||||
}
|
||||
}
|
||||
|
||||
throttledUpdateEngineFov = throttle(
|
||||
(vals: {
|
||||
position: Vector3
|
||||
quaternion: Quaternion
|
||||
zoom: number
|
||||
fov: number
|
||||
target: Vector3
|
||||
}) => {
|
||||
const cmd: EngineCommand = {
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_perspective_settings',
|
||||
...convertThreeCamValuesToEngineCam({
|
||||
...vals,
|
||||
isPerspective: true,
|
||||
}),
|
||||
fov_y: vals.fov,
|
||||
...calculateNearFarFromFOV(vals.fov),
|
||||
},
|
||||
}
|
||||
this.engineCommandManager.sendSceneCommand(cmd)
|
||||
this.lastPerspectiveCmd = cmd
|
||||
this.lastPerspectiveCmdTime = Date.now()
|
||||
if (this.lastPerspectiveCmdTimeoutId !== null) {
|
||||
clearTimeout(this.lastPerspectiveCmdTimeoutId)
|
||||
}
|
||||
this.lastPerspectiveCmdTimeoutId = setTimeout(
|
||||
this.sendLastPerspectiveReliableChannel,
|
||||
lastCmdDelay
|
||||
) as any as number
|
||||
},
|
||||
1000 / 30
|
||||
)
|
||||
|
||||
constructor(
|
||||
isOrtho = false,
|
||||
domElement: HTMLCanvasElement,
|
||||
@ -534,9 +499,10 @@ export class CameraControls {
|
||||
direction.normalize()
|
||||
this.camera.position.copy(this.target).addScaledVector(direction, distance)
|
||||
}
|
||||
usePerspectiveCamera = () => {
|
||||
usePerspectiveCamera = async () => {
|
||||
this._usePerspectiveCamera()
|
||||
this.engineCommandManager.sendSceneCommand({
|
||||
if (this.syncDirection === 'clientToEngine') {
|
||||
await this.engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
@ -548,12 +514,13 @@ export class CameraControls {
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
this.onCameraChange()
|
||||
this.update()
|
||||
return this.camera
|
||||
}
|
||||
|
||||
dollyZoom = (newFov: number) => {
|
||||
dollyZoom = async (newFov: number, splitEngineCalls = false) => {
|
||||
if (!(this.camera instanceof PerspectiveCamera)) {
|
||||
console.warn('Dolly zoom is only applicable to perspective cameras.')
|
||||
return
|
||||
@ -604,13 +571,52 @@ export class CameraControls {
|
||||
this.camera.near = z_near
|
||||
this.camera.far = z_far
|
||||
|
||||
this.throttledUpdateEngineFov({
|
||||
fov: newFov,
|
||||
if (splitEngineCalls) {
|
||||
await this.engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_look_at',
|
||||
...convertThreeCamValuesToEngineCam({
|
||||
isPerspective: true,
|
||||
position: newPosition,
|
||||
quaternion: this.camera.quaternion,
|
||||
zoom: this.camera.zoom,
|
||||
target: this.target,
|
||||
}),
|
||||
},
|
||||
})
|
||||
await this.engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_set_perspective',
|
||||
parameters: {
|
||||
fov_y: newFov,
|
||||
z_near: 0.01,
|
||||
z_far: 1000,
|
||||
},
|
||||
},
|
||||
})
|
||||
} else {
|
||||
await this.engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_perspective_settings',
|
||||
...convertThreeCamValuesToEngineCam({
|
||||
isPerspective: true,
|
||||
position: newPosition,
|
||||
quaternion: this.camera.quaternion,
|
||||
zoom: this.camera.zoom,
|
||||
target: this.target,
|
||||
}),
|
||||
fov_y: newFov,
|
||||
z_near: 0.01,
|
||||
z_far: 1000,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
update = (forceUpdate = false) => {
|
||||
@ -1015,6 +1021,29 @@ export class CameraControls {
|
||||
.onComplete(onComplete)
|
||||
.start()
|
||||
})
|
||||
snapToPerspectiveBeforeHandingBackControlToEngine = async (
|
||||
targetCamUp = new Vector3(0, 0, 1)
|
||||
) => {
|
||||
if (this.syncDirection === 'engineToClient') {
|
||||
console.warn(
|
||||
'animate To Perspective not design to work with engineToClient syncDirection.'
|
||||
)
|
||||
}
|
||||
this.isFovAnimationInProgress = true
|
||||
const targetFov = this.fovBeforeOrtho // Target FOV for perspective
|
||||
this.lastPerspectiveFov = 4
|
||||
let currentFov = 4
|
||||
const initialCameraUp = this.camera.up.clone()
|
||||
this.usePerspectiveCamera()
|
||||
const tempVec = new Vector3()
|
||||
|
||||
currentFov = this.lastPerspectiveFov + (targetFov - this.lastPerspectiveFov)
|
||||
const currentUp = tempVec.lerpVectors(initialCameraUp, targetCamUp, 1)
|
||||
this.camera.up.copy(currentUp)
|
||||
await this.dollyZoom(currentFov, true)
|
||||
|
||||
this.isFovAnimationInProgress = false
|
||||
}
|
||||
|
||||
get reactCameraProperties(): ReactCameraProperties {
|
||||
return {
|
||||
@ -1087,7 +1116,7 @@ function calculateNearFarFromFOV(fov: number) {
|
||||
// const nearFarRatio = (fov - 3) / (45 - 3)
|
||||
// const z_near = 0.1 + nearFarRatio * (5 - 0.1)
|
||||
// const z_far = 1000 + nearFarRatio * (100000 - 1000)
|
||||
return { z_near: 0.1, z_far: 1000 }
|
||||
return { z_near: 0.01, z_far: 1000 }
|
||||
}
|
||||
|
||||
function convertThreeCamValuesToEngineCam({
|
||||
@ -1106,11 +1135,6 @@ function convertThreeCamValuesToEngineCam({
|
||||
// leaving for now since it's working but maybe revisit later
|
||||
const euler = new Euler().setFromQuaternion(quaternion, 'XYZ')
|
||||
|
||||
const lookAtVector = new Vector3(0, 0, -1)
|
||||
.applyEuler(euler)
|
||||
.normalize()
|
||||
.add(position)
|
||||
|
||||
const upVector = new Vector3(0, 1, 0).applyEuler(euler).normalize()
|
||||
if (isPerspective) {
|
||||
return {
|
||||
@ -1119,6 +1143,10 @@ function convertThreeCamValuesToEngineCam({
|
||||
vantage: position,
|
||||
}
|
||||
}
|
||||
const lookAtVector = new Vector3(0, 0, -1)
|
||||
.applyEuler(euler)
|
||||
.normalize()
|
||||
.add(position)
|
||||
const fudgeFactor2 = zoom * 0.9979224466814468 - 0.03473692325839295
|
||||
const zoomFactor = (-ZOOM_MAGIC_NUMBER + fudgeFactor2) / zoom
|
||||
const direction = lookAtVector.clone().sub(position).normalize()
|
||||
|
@ -136,6 +136,7 @@ export const ClientSideScene = ({
|
||||
<div
|
||||
ref={canvasRef}
|
||||
style={{ cursor: cursor }}
|
||||
data-testid="client-side-scene"
|
||||
className={`absolute inset-0 h-full w-full transition-all duration-300 ${
|
||||
hideClient ? 'opacity-0' : 'opacity-100'
|
||||
} ${hideServer ? 'bg-chalkboard-10 dark:bg-chalkboard-100' : ''} ${
|
||||
|
@ -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 {
|
||||
@ -1329,13 +1327,6 @@ export class SceneEntities {
|
||||
to,
|
||||
})
|
||||
}
|
||||
async animateAfterSketch() {
|
||||
// if (isReducedMotion()) {
|
||||
// sceneInfra.camControls.usePerspectiveCamera()
|
||||
// return
|
||||
// }
|
||||
await sceneInfra.camControls.animateToPerspective()
|
||||
}
|
||||
removeSketchGrid() {
|
||||
if (this.axisGroup) this.scene.remove(this.axisGroup)
|
||||
}
|
||||
@ -1399,31 +1390,88 @@ export class SceneEntities {
|
||||
selected.material.color = defaultPlaneColor(type)
|
||||
},
|
||||
onClick: async (args) => {
|
||||
const checkExtrudeFaceClick = async (): Promise<
|
||||
['face' | 'plane' | 'other', string]
|
||||
> => {
|
||||
const { streamDimensions } = useStore.getState()
|
||||
const { entity_id } = await sendSelectEventToEngine(
|
||||
const { entity_id, ...rest } = await sendSelectEventToEngine(
|
||||
args?.mouseEvent,
|
||||
document.getElementById('video-stream') as HTMLVideoElement,
|
||||
streamDimensions
|
||||
)
|
||||
if (!entity_id) return ['other', '']
|
||||
let _entity_id = entity_id
|
||||
console.log('things', _entity_id, rest)
|
||||
if (!_entity_id) return
|
||||
if (
|
||||
engineCommandManager.defaultPlanes?.xy === entity_id ||
|
||||
engineCommandManager.defaultPlanes?.xz === entity_id ||
|
||||
engineCommandManager.defaultPlanes?.yz === entity_id
|
||||
engineCommandManager.defaultPlanes?.xy === _entity_id ||
|
||||
engineCommandManager.defaultPlanes?.xz === _entity_id ||
|
||||
engineCommandManager.defaultPlanes?.yz === _entity_id ||
|
||||
engineCommandManager.defaultPlanes?.negXy === _entity_id ||
|
||||
engineCommandManager.defaultPlanes?.negXz === _entity_id ||
|
||||
engineCommandManager.defaultPlanes?.negYz === _entity_id
|
||||
) {
|
||||
return ['plane', entity_id]
|
||||
const defaultPlaneStrMap: Record<string, DefaultPlaneStr> = {
|
||||
[engineCommandManager.defaultPlanes.xy]: 'XY',
|
||||
[engineCommandManager.defaultPlanes.xz]: 'XZ',
|
||||
[engineCommandManager.defaultPlanes.yz]: 'YZ',
|
||||
[engineCommandManager.defaultPlanes.negXy]: '-XY',
|
||||
[engineCommandManager.defaultPlanes.negXz]: '-XZ',
|
||||
[engineCommandManager.defaultPlanes.negYz]: '-YZ',
|
||||
}
|
||||
const artifact = this.engineCommandManager.artifactMap[entity_id]
|
||||
// TODO can we get this information from rust land when it creates the default planes?
|
||||
// maybe returned from make_default_planes (src/wasm-lib/src/wasm.rs)
|
||||
let zAxis: [number, number, number] = [0, 0, 1]
|
||||
let yAxis: [number, number, number] = [0, 1, 0]
|
||||
|
||||
// get unit vector from camera position to target
|
||||
const camVector = sceneInfra.camControls.camera.position
|
||||
.clone()
|
||||
.sub(sceneInfra.camControls.target)
|
||||
|
||||
if (engineCommandManager.defaultPlanes?.xy === _entity_id) {
|
||||
console.log('XY')
|
||||
zAxis = [0, 0, 1]
|
||||
yAxis = [0, 1, 0]
|
||||
if (camVector.z < 0) {
|
||||
zAxis = [0, 0, -1]
|
||||
_entity_id = engineCommandManager.defaultPlanes?.negXy || ''
|
||||
}
|
||||
} else if (engineCommandManager.defaultPlanes?.yz === _entity_id) {
|
||||
console.log('YZ')
|
||||
zAxis = [1, 0, 0]
|
||||
yAxis = [0, 0, 1]
|
||||
if (camVector.x < 0) {
|
||||
zAxis = [-1, 0, 0]
|
||||
_entity_id = engineCommandManager.defaultPlanes?.negYz || ''
|
||||
}
|
||||
} else if (engineCommandManager.defaultPlanes?.xz === _entity_id) {
|
||||
console.log('XZ')
|
||||
zAxis = [0, 1, 0]
|
||||
yAxis = [0, 0, 1]
|
||||
_entity_id = engineCommandManager.defaultPlanes?.negXz || ''
|
||||
if (camVector.y < 0) {
|
||||
zAxis = [0, -1, 0]
|
||||
_entity_id = engineCommandManager.defaultPlanes?.xz || ''
|
||||
}
|
||||
}
|
||||
|
||||
sceneInfra.modelingSend({
|
||||
type: 'Select default plane',
|
||||
data: {
|
||||
type: 'defaultPlane',
|
||||
planeId: _entity_id,
|
||||
plane: defaultPlaneStrMap[_entity_id],
|
||||
zAxis,
|
||||
yAxis,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
const artifact = this.engineCommandManager.artifactMap[_entity_id]
|
||||
// If we clicked on an extrude wall, we climb up the parent Id
|
||||
// to get the sketch profile's face ID. If we clicked on an endcap,
|
||||
// we already have it.
|
||||
const targetId =
|
||||
'additionalData' in artifact &&
|
||||
artifact.additionalData?.type === 'cap'
|
||||
? entity_id
|
||||
? _entity_id
|
||||
: artifact.parentId
|
||||
|
||||
// tsc cannot infer that target can have extrusions
|
||||
@ -1437,16 +1485,12 @@ export class SceneEntities {
|
||||
// but we need to more robustly handle resolving to the correct extrusion
|
||||
// if there are multiple.
|
||||
const extrusions =
|
||||
this.engineCommandManager.artifactMap?.[
|
||||
target?.extrusions?.[0] || ''
|
||||
]
|
||||
this.engineCommandManager.artifactMap?.[target?.extrusions?.[0] || '']
|
||||
|
||||
if (artifact?.commandType !== 'solid3d_get_extrusion_face_info')
|
||||
return ['other', entity_id]
|
||||
if (artifact?.commandType !== 'solid3d_get_extrusion_face_info') return
|
||||
|
||||
const faceInfo = await getFaceDetails(entity_id)
|
||||
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
|
||||
return ['other', entity_id]
|
||||
const faceInfo = await getFaceDetails(_entity_id)
|
||||
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis) return
|
||||
const { z_axis, y_axis, origin } = faceInfo
|
||||
const sketchPathToNode = getNodePathFromSourceRange(
|
||||
kclManager.ast,
|
||||
@ -1471,42 +1515,10 @@ export class SceneEntities {
|
||||
artifact?.additionalData?.type === 'cap'
|
||||
? artifact.additionalData.info
|
||||
: 'none',
|
||||
faceId: entity_id,
|
||||
},
|
||||
})
|
||||
return ['face', entity_id]
|
||||
}
|
||||
|
||||
const faceResult = await checkExtrudeFaceClick()
|
||||
if (faceResult[0] === 'face') return
|
||||
|
||||
if (!args || !args.intersects?.[0]) return
|
||||
if (args.mouseEvent.which !== 1) return
|
||||
const { intersects } = args
|
||||
const type = intersects?.[0].object.name || ''
|
||||
const posNorm = Number(intersects?.[0]?.normal?.z) > 0
|
||||
let planeString: DefaultPlaneStr = posNorm ? 'XY' : '-XY'
|
||||
let zAxis: [number, number, number] = posNorm ? [0, 0, 1] : [0, 0, -1]
|
||||
let yAxis: [number, number, number] = [0, 1, 0]
|
||||
if (type === YZ_PLANE) {
|
||||
planeString = posNorm ? 'YZ' : '-YZ'
|
||||
zAxis = posNorm ? [1, 0, 0] : [-1, 0, 0]
|
||||
yAxis = [0, 0, 1]
|
||||
} else if (type === XZ_PLANE) {
|
||||
planeString = posNorm ? '-XZ' : 'XZ'
|
||||
zAxis = posNorm ? [0, 1, 0] : [0, -1, 0]
|
||||
yAxis = [0, 0, 1]
|
||||
}
|
||||
sceneInfra.modelingSend({
|
||||
type: 'Select default plane',
|
||||
data: {
|
||||
type: 'defaultPlane',
|
||||
plane: planeString,
|
||||
zAxis,
|
||||
yAxis,
|
||||
planeId: faceResult[1],
|
||||
faceId: _entity_id,
|
||||
},
|
||||
})
|
||||
return
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -6,6 +6,8 @@ import CommandComboBox from '../CommandComboBox'
|
||||
import CommandBarReview from './CommandBarReview'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import useHotkeyWrapper from 'lib/hotkeyWrapper'
|
||||
import { CustomIcon } from 'components/CustomIcon'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
|
||||
export const CommandBar = () => {
|
||||
const { pathname } = useLocation()
|
||||
@ -103,7 +105,7 @@ export const CommandBar = () => {
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<WrapperComponent.Panel
|
||||
className="relative z-50 pointer-events-auto w-full max-w-xl py-2 mx-auto border rounded shadow-lg bg-chalkboard-10 dark:bg-chalkboard-100 dark:border-chalkboard-70"
|
||||
className="relative z-50 pointer-events-auto w-full max-w-xl py-2 mx-auto border rounded rounded-tl-none shadow-lg bg-chalkboard-10 dark:bg-chalkboard-100 dark:border-chalkboard-70"
|
||||
as="div"
|
||||
data-testid="command-bar"
|
||||
>
|
||||
@ -116,6 +118,19 @@ export const CommandBar = () => {
|
||||
<CommandBarReview stepBack={stepBack} />
|
||||
)
|
||||
)}
|
||||
<button
|
||||
onClick={() => commandBarSend({ type: 'Close' })}
|
||||
className="group block !absolute left-auto right-full top-[-3px] m-2.5 p-0 border-none bg-transparent hover:bg-transparent"
|
||||
>
|
||||
<CustomIcon
|
||||
name="close"
|
||||
className="w-5 h-5 rounded-sm bg-destroy-10 text-destroy-80 dark:bg-destroy-80 dark:text-destroy-10 group-hover:brightness-110"
|
||||
/>
|
||||
<Tooltip position="bottom" delay={500}>
|
||||
Cancel{' '}
|
||||
<kbd className="hotkey ml-4 dark:!bg-chalkboard-80">esc</kbd>
|
||||
</Tooltip>
|
||||
</button>
|
||||
</WrapperComponent.Panel>
|
||||
</Transition.Child>
|
||||
</WrapperComponent>
|
||||
|
@ -7,10 +7,8 @@ import {
|
||||
getSelectionType,
|
||||
getSelectionTypeDisplayText,
|
||||
} from 'lib/selections'
|
||||
import { kclManager } from 'lib/singletons'
|
||||
import { modelingMachine } from 'machines/modelingMachine'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { StateFrom } from 'xstate'
|
||||
|
||||
const selectionSelector = (snapshot: StateFrom<typeof modelingMachine>) =>
|
||||
@ -41,12 +39,6 @@ function CommandBarSelectionInput({
|
||||
canSubmitSelectionArg(selectionsByType, arg)
|
||||
)
|
||||
|
||||
useHotkeys('tab', () => onSubmit(selection), {
|
||||
enableOnFormTags: true,
|
||||
enableOnContentEditable: true,
|
||||
keyup: true,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
inputRef.current?.focus()
|
||||
}, [selection, inputRef])
|
||||
|
@ -74,8 +74,8 @@ const CustomIconMap = {
|
||||
bug: (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10.8209 5.99884C10.6403 5.73962 10.3399 5.57001 10 5.57001C9.65984 5.57001 9.35936 5.73984 9.17871 5.99935C9.43724 5.95129 9.71142 5.92578 10.0012 5.92578C10.29 5.92578 10.5633 5.95111 10.8209 5.99884ZM10 4.57001C8.9459 4.57001 8.08227 5.38548 8.00554 6.41997C7.58916 6.65398 7.23724 6.95989 6.95014 7.31304L5.85355 6.21645L5.14645 6.92356L6.40931 8.18642C6.20774 8.62503 6.08043 9.09624 6.0278 9.57001H5V10.57H6.01946C6.06396 11.1581 6.1867 11.8173 6.4071 12.4558L5.14645 13.7165L5.85355 14.4236L6.8408 13.4363C7.46354 14.555 8.47307 15.4258 10.0012 15.4258C11.529 15.4258 12.5378 14.5554 13.16 13.4371L14.1464 14.4236L14.8536 13.7165L13.5934 12.4563C13.8136 11.8177 13.9362 11.1583 13.9806 10.57H15V9.57001H13.9722C13.9197 9.0961 13.7925 8.62474 13.5911 8.18602L14.8536 6.92356L14.1464 6.21645L13.0505 7.31239C12.7633 6.95894 12.4112 6.65285 11.9944 6.41883C11.9171 5.38488 11.0537 4.57001 10 4.57001ZM10.5 14.3801V8.57001H9.5V14.3796C8.72105 14.2298 8.15885 13.7245 7.7428 12.9999C7.22316 12.095 7 10.937 7 10.07C7 8.46381 8.04281 6.92578 10.0012 6.92578C11.9589 6.92578 13 8.4629 13 10.07C13 10.9373 12.7773 12.0954 12.2582 13.0003C11.8422 13.7254 11.2799 14.2309 10.5 14.3801Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
|
@ -47,7 +47,6 @@ import {
|
||||
TANGENTIAL_ARC_TO_SEGMENT,
|
||||
getParentGroup,
|
||||
getSketchOrientationDetails,
|
||||
getSketchQuaternion,
|
||||
} from 'clientSideScene/sceneEntities'
|
||||
import {
|
||||
moveValueIntoNewVariablePath,
|
||||
@ -64,6 +63,7 @@ import {
|
||||
import {
|
||||
getNodeFromPath,
|
||||
getNodePathFromSourceRange,
|
||||
hasExtrudableGeometry,
|
||||
isSingleCursorInPipe,
|
||||
} from 'lang/queryAst'
|
||||
import { TEST } from 'env'
|
||||
@ -76,6 +76,7 @@ import { useSearchParams } from 'react-router-dom'
|
||||
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
|
||||
import { getVarNameModal } from 'hooks/useToolbarGuards'
|
||||
import useHotkeyWrapper from 'lib/hotkeyWrapper'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
@ -121,7 +122,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(
|
||||
@ -141,7 +159,41 @@ export const ModelingMachineProvider = ({
|
||||
{
|
||||
actions: {
|
||||
'sketch exit execute': () => {
|
||||
;(async () => {
|
||||
await sceneInfra.camControls.snapToPerspectiveBeforeHandingBackControlToEngine()
|
||||
|
||||
sceneInfra.camControls.syncDirection = 'engineToClient'
|
||||
|
||||
const settings: Models['CameraSettings_type'] = (
|
||||
await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_get_settings',
|
||||
},
|
||||
})
|
||||
)?.data?.data?.settings
|
||||
if (settings.up.z !== 1) {
|
||||
// workaround for gimbal lock situation
|
||||
await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_look_at',
|
||||
center: settings.center,
|
||||
vantage: {
|
||||
...settings.pos,
|
||||
y:
|
||||
settings.pos.y +
|
||||
(settings.center.z - settings.pos.z > 0 ? 2 : -2),
|
||||
},
|
||||
up: { x: 0, y: 0, z: 1 },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
kclManager.executeCode(true)
|
||||
})()
|
||||
},
|
||||
'Set mouse state': assign({
|
||||
mouseState: (_, event) => event.data,
|
||||
@ -396,8 +448,13 @@ export const ModelingMachineProvider = ({
|
||||
if (
|
||||
selectionRanges.codeBasedSelections.length === 0 ||
|
||||
isSelectionLastLine(selectionRanges, codeManager.code)
|
||||
)
|
||||
return true
|
||||
) {
|
||||
// they have no selection, we should enable the button
|
||||
// so they can select the face through the cmdbar
|
||||
// BUT only if there's extrudable geometry
|
||||
if (hasExtrudableGeometry(kclManager.ast)) return true
|
||||
return false
|
||||
}
|
||||
if (!isPipe) return false
|
||||
|
||||
return canExtrudeSelection(selectionRanges)
|
||||
@ -464,7 +521,7 @@ export const ModelingMachineProvider = ({
|
||||
engineCommandManager,
|
||||
data.faceId
|
||||
)
|
||||
|
||||
sceneInfra.camControls.syncDirection = 'clientToEngine'
|
||||
return {
|
||||
sketchPathToNode: pathToNewSketchNode,
|
||||
zAxis: data.zAxis,
|
||||
@ -478,8 +535,10 @@ export const ModelingMachineProvider = ({
|
||||
)
|
||||
await kclManager.updateAst(modifiedAst, false)
|
||||
sceneInfra.camControls.syncDirection = 'clientToEngine'
|
||||
const quat = await getSketchQuaternion(pathToNode, data.zAxis)
|
||||
await sceneInfra.camControls.tweenCameraToQuaternion(quat)
|
||||
await letEngineAnimateAndSyncCamAfter(
|
||||
engineCommandManager,
|
||||
data.planeId
|
||||
)
|
||||
return {
|
||||
sketchPathToNode: pathToNode,
|
||||
zAxis: data.zAxis,
|
||||
|
@ -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,6 +123,7 @@ function ModelingSidebarSection({
|
||||
}, [showDebugPanel.current, togglePane, openPanes])
|
||||
|
||||
return (
|
||||
<div className="group contents">
|
||||
<Tab.Group
|
||||
vertical
|
||||
selectedIndex={
|
||||
@ -142,7 +143,8 @@ function ModelingSidebarSection({
|
||||
(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 ' +
|
||||
' 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' : '')
|
||||
}
|
||||
>
|
||||
@ -187,6 +189,7 @@ function ModelingSidebarSection({
|
||||
))}
|
||||
</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'
|
||||
|
||||
|
@ -126,8 +126,8 @@ export const Stream = ({ className = '' }: { className?: string }) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
id="stream"
|
||||
className={className}
|
||||
className="absolute inset-0 z-0"
|
||||
data-testid="stream"
|
||||
onMouseUp={handleMouseUp}
|
||||
onMouseDown={handleMouseDown}
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
@ -142,7 +142,6 @@ export const Stream = ({ className = '' }: { className?: string }) => {
|
||||
onMouseMoveCapture={handleMouseMove}
|
||||
className="w-full cursor-pointer h-full"
|
||||
disablePictureInPicture
|
||||
style={{ transitionDuration: '200ms', transitionProperty: 'filter' }}
|
||||
id="video-stream"
|
||||
/>
|
||||
<ClientSideScene
|
||||
|
@ -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 {
|
||||
@ -249,3 +253,10 @@ code {
|
||||
.cm-ghostText * {
|
||||
color: rgb(120, 120, 120, 0.8) !important;
|
||||
}
|
||||
|
||||
@layer components {
|
||||
kbd.hotkey {
|
||||
@apply font-mono text-xs inline-block px-1 py-0.5 rounded-sm;
|
||||
@apply bg-chalkboard-20 dark:bg-chalkboard-90;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
@ -364,18 +384,55 @@ export class KclManager {
|
||||
return this?.engineCommandManager?.defaultPlanes
|
||||
}
|
||||
|
||||
showPlanes() {
|
||||
if (!this.defaultPlanes) return
|
||||
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xy, false)
|
||||
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, false)
|
||||
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xz, false)
|
||||
showPlanes(all = false) {
|
||||
if (!this.defaultPlanes) return Promise.all([])
|
||||
const thePromises = [
|
||||
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xy, false),
|
||||
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, false),
|
||||
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xz, false),
|
||||
]
|
||||
if (all) {
|
||||
thePromises.push(
|
||||
this.engineCommandManager.setPlaneHidden(
|
||||
this.defaultPlanes.negXy,
|
||||
false
|
||||
)
|
||||
)
|
||||
thePromises.push(
|
||||
this.engineCommandManager.setPlaneHidden(
|
||||
this.defaultPlanes.negYz,
|
||||
false
|
||||
)
|
||||
)
|
||||
thePromises.push(
|
||||
this.engineCommandManager.setPlaneHidden(
|
||||
this.defaultPlanes.negXz,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
return Promise.all(thePromises)
|
||||
}
|
||||
|
||||
hidePlanes() {
|
||||
if (!this.defaultPlanes) return
|
||||
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xy, true)
|
||||
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, true)
|
||||
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xz, true)
|
||||
hidePlanes(all = false) {
|
||||
if (!this.defaultPlanes) return Promise.all([])
|
||||
const thePromises = [
|
||||
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xy, true),
|
||||
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, true),
|
||||
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xz, true),
|
||||
]
|
||||
if (all) {
|
||||
thePromises.push(
|
||||
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.negXy, true)
|
||||
)
|
||||
thePromises.push(
|
||||
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.negYz, true)
|
||||
)
|
||||
thePromises.push(
|
||||
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.negXz, true)
|
||||
)
|
||||
}
|
||||
return Promise.all(thePromises)
|
||||
}
|
||||
defaultSelectionFilter() {
|
||||
defaultSelectionFilter(this.programMemory, this.engineCommandManager)
|
||||
|
@ -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
|
||||
|
@ -7,6 +7,8 @@ import {
|
||||
doesPipeHaveCallExp,
|
||||
hasExtrudeSketchGroup,
|
||||
findUsesOfTagInPipe,
|
||||
hasSketchPipeBeenExtruded,
|
||||
hasExtrudableGeometry,
|
||||
} from './queryAst'
|
||||
import { enginelessExecutor } from '../lib/testHelpers'
|
||||
import {
|
||||
@ -396,3 +398,90 @@ describe('Testing findUsesOfTagInPipe', () => {
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Testing hasSketchPipeBeenExtruded', () => {
|
||||
const exampleCode = `const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([3.29, 7.86], %)
|
||||
|> line([2.48, 2.44], %)
|
||||
|> line([2.66, 1.17], %)
|
||||
|> line([3.75, 0.46], %)
|
||||
|> line([4.99, -0.46], %, 'seg01')
|
||||
|> line([3.3, -2.12], %)
|
||||
|> line([2.16, -3.33], %)
|
||||
|> line([0.85, -3.08], %)
|
||||
|> line([-0.18, -3.36], %)
|
||||
|> line([-3.86, -2.73], %)
|
||||
|> line([-17.67, 0.85], %)
|
||||
|> close(%)
|
||||
const extrude001 = extrude(10, sketch001)
|
||||
const sketch002 = startSketchOn(extrude001, 'seg01')
|
||||
|> startProfileAt([-12.94, 6.6], %)
|
||||
|> line([2.45, -0.2], %)
|
||||
|> line([-2, -1.25], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
`
|
||||
it('finds sketch001 pipe to be extruded', async () => {
|
||||
const ast = parse(exampleCode)
|
||||
const lineOfInterest = `line([4.99, -0.46], %, 'seg01')`
|
||||
const characterIndex =
|
||||
exampleCode.indexOf(lineOfInterest) + lineOfInterest.length
|
||||
const extruded = hasSketchPipeBeenExtruded(
|
||||
{
|
||||
range: [characterIndex, characterIndex],
|
||||
type: 'default',
|
||||
},
|
||||
ast
|
||||
)
|
||||
expect(extruded).toBeTruthy()
|
||||
})
|
||||
it('find sketch002 NOT pipe to be extruded', async () => {
|
||||
const ast = parse(exampleCode)
|
||||
const lineOfInterest = `line([2.45, -0.2], %)`
|
||||
const characterIndex =
|
||||
exampleCode.indexOf(lineOfInterest) + lineOfInterest.length
|
||||
const extruded = hasSketchPipeBeenExtruded(
|
||||
{
|
||||
range: [characterIndex, characterIndex],
|
||||
type: 'default',
|
||||
},
|
||||
ast
|
||||
)
|
||||
expect(extruded).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Testing hasExtrudableGeometry', () => {
|
||||
it('finds sketch001 pipe to be extruded', async () => {
|
||||
const exampleCode = `const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([3.29, 7.86], %)
|
||||
|> line([2.48, 2.44], %)
|
||||
|> line([-3.86, -2.73], %)
|
||||
|> line([-17.67, 0.85], %)
|
||||
|> close(%)
|
||||
const extrude001 = extrude(10, sketch001)
|
||||
const sketch002 = startSketchOn(extrude001, 'seg01')
|
||||
|> startProfileAt([-12.94, 6.6], %)
|
||||
|> line([2.45, -0.2], %)
|
||||
|> line([-2, -1.25], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
`
|
||||
const ast = parse(exampleCode)
|
||||
const extrudable = hasExtrudableGeometry(ast)
|
||||
expect(extrudable).toBeTruthy()
|
||||
})
|
||||
it('find sketch002 NOT pipe to be extruded', async () => {
|
||||
const exampleCode = `const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([3.29, 7.86], %)
|
||||
|> line([2.48, 2.44], %)
|
||||
|> line([-3.86, -2.73], %)
|
||||
|> line([-17.67, 0.85], %)
|
||||
|> close(%)
|
||||
const extrude001 = extrude(10, sketch001)
|
||||
`
|
||||
const ast = parse(exampleCode)
|
||||
const extrudable = hasExtrudableGeometry(ast)
|
||||
expect(extrudable).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
@ -720,3 +720,78 @@ export function findUsesOfTagInPipe(
|
||||
})
|
||||
return dependentRanges
|
||||
}
|
||||
|
||||
export function hasSketchPipeBeenExtruded(selection: Selection, ast: Program) {
|
||||
const path = getNodePathFromSourceRange(ast, selection.range)
|
||||
const { node: pipeExpression } = getNodeFromPath<PipeExpression>(
|
||||
ast,
|
||||
path,
|
||||
'PipeExpression'
|
||||
)
|
||||
if (pipeExpression.type !== 'PipeExpression') return false
|
||||
const varDec = getNodeFromPath<VariableDeclarator>(
|
||||
ast,
|
||||
path,
|
||||
'VariableDeclarator'
|
||||
).node
|
||||
let extruded = false
|
||||
traverse(ast as any, {
|
||||
enter(node) {
|
||||
if (
|
||||
node.type === 'CallExpression' &&
|
||||
node.callee.type === 'Identifier' &&
|
||||
node.callee.name === 'extrude' &&
|
||||
node.arguments?.[1]?.type === 'Identifier' &&
|
||||
node.arguments[1].name === varDec.id.name
|
||||
) {
|
||||
extruded = true
|
||||
}
|
||||
},
|
||||
})
|
||||
return extruded
|
||||
}
|
||||
|
||||
/** File must contain at least one sketch that has not been extruded already */
|
||||
export function hasExtrudableGeometry(ast: Program) {
|
||||
const theMap: any = {}
|
||||
traverse(ast as any, {
|
||||
enter(node) {
|
||||
if (
|
||||
node.type === 'VariableDeclarator' &&
|
||||
node.init?.type === 'PipeExpression'
|
||||
) {
|
||||
let hasStartProfileAt = false
|
||||
let hasStartSketchOn = false
|
||||
let hasClose = false
|
||||
for (const pipe of node.init.body) {
|
||||
if (
|
||||
pipe.type === 'CallExpression' &&
|
||||
pipe.callee.name === 'startProfileAt'
|
||||
) {
|
||||
hasStartProfileAt = true
|
||||
}
|
||||
if (
|
||||
pipe.type === 'CallExpression' &&
|
||||
pipe.callee.name === 'startSketchOn'
|
||||
) {
|
||||
hasStartSketchOn = true
|
||||
}
|
||||
if (pipe.type === 'CallExpression' && pipe.callee.name === 'close') {
|
||||
hasClose = true
|
||||
}
|
||||
}
|
||||
if (hasStartProfileAt && hasStartSketchOn && hasClose) {
|
||||
theMap[node.id.name] = true
|
||||
}
|
||||
} else if (
|
||||
node.type === 'CallExpression' &&
|
||||
node.callee.name === 'extrude' &&
|
||||
node.arguments[1]?.type === 'Identifier' &&
|
||||
theMap?.[node?.arguments?.[1]?.name]
|
||||
) {
|
||||
delete theMap[node.arguments[1].name]
|
||||
}
|
||||
},
|
||||
})
|
||||
return Object.keys(theMap).length > 0
|
||||
}
|
||||
|
@ -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'
|
||||
) {
|
||||
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) => {
|
||||
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) => {
|
||||
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)
|
||||
|
@ -16,6 +16,7 @@ import { Program } from 'lang/wasm'
|
||||
import {
|
||||
doesPipeHaveCallExp,
|
||||
getNodeFromPath,
|
||||
hasSketchPipeBeenExtruded,
|
||||
isSingleCursorInPipe,
|
||||
} from 'lang/queryAst'
|
||||
import { CommandArgument } from './commandTypes'
|
||||
@ -387,6 +388,7 @@ export function canExtrudeSelection(selection: Selections) {
|
||||
)
|
||||
return (
|
||||
!!isSketchPipe(selection) &&
|
||||
commonNodes.every((n) => !hasSketchPipeBeenExtruded(n.selection, n.ast)) &&
|
||||
commonNodes.every((n) => nodeHasClose(n)) &&
|
||||
commonNodes.every((n) => !nodeHasExtrude(n))
|
||||
)
|
||||
@ -540,7 +542,7 @@ function codeToIdSelections(
|
||||
.filter(Boolean) as any
|
||||
}
|
||||
|
||||
export function sendSelectEventToEngine(
|
||||
export async function sendSelectEventToEngine(
|
||||
e: MouseEvent | React.MouseEvent<HTMLDivElement, MouseEvent>,
|
||||
el: HTMLVideoElement,
|
||||
streamDimensions: { streamWidth: number; streamHeight: number }
|
||||
@ -551,7 +553,7 @@ export function sendSelectEventToEngine(
|
||||
el,
|
||||
...streamDimensions,
|
||||
})
|
||||
const result: Promise<Models['SelectWithPoint_type']> = engineCommandManager
|
||||
const result: Models['SelectWithPoint_type'] = await engineCommandManager
|
||||
.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
|
@ -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 {
|
||||
|
147
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,11 +1441,24 @@ 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"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df75feef10313fa1cb15b7cecd0f579877312ba3d42bb5b8b4c1d4b1d0fcabf0"
|
||||
checksum = "af3de9bb4b1441f198689a9f64a8163a518377e30b348a784680e738985b95eb"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -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"
|
||||
@ -1936,13 +1977,76 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.85"
|
||||
version = "1.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23"
|
||||
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
|
||||
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,10 +65,11 @@ members = [
|
||||
"derive-docs",
|
||||
"kcl",
|
||||
"kcl-macros",
|
||||
"kcl-test-server",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
kittycad = { version = "0.3.5", default-features = false, features = ["js", "requests"] }
|
||||
kittycad = { version = "0.3.6", default-features = false, features = ["js", "requests"] }
|
||||
kittycad-modeling-session = "0.1.4"
|
||||
|
||||
[[test]]
|
||||
|
@ -96,11 +96,17 @@ fn do_stdlib_inner(
|
||||
}
|
||||
|
||||
if !ast.sig.generics.params.is_empty() {
|
||||
if ast.sig.generics.params.iter().any(|generic_type| match generic_type {
|
||||
syn::GenericParam::Lifetime(_) => false,
|
||||
syn::GenericParam::Type(_) => true,
|
||||
syn::GenericParam::Const(_) => true,
|
||||
}) {
|
||||
errors.push(Error::new_spanned(
|
||||
&ast.sig.generics,
|
||||
"generics are not permitted for stdlib functions",
|
||||
"Stdlib functions may not be generic over types or constants, only lifetimes.",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if ast.sig.variadic.is_some() {
|
||||
errors.push(Error::new_spanned(&ast.sig.variadic, "no language C here"));
|
||||
@ -650,7 +656,12 @@ impl Parse for ItemFnForSignature {
|
||||
}
|
||||
|
||||
fn clean_ty_string(t: &str) -> (String, proc_macro2::TokenStream) {
|
||||
let mut ty_string = t.replace('&', "").replace("mut", "").replace(' ', "");
|
||||
let mut ty_string = t
|
||||
.replace("& 'a", "")
|
||||
.replace('&', "")
|
||||
.replace("mut", "")
|
||||
.replace("< 'a >", "")
|
||||
.replace(' ', "");
|
||||
if ty_string.starts_with("Args") {
|
||||
ty_string = "Args".to_string();
|
||||
}
|
||||
|
@ -35,6 +35,56 @@ fn test_get_inner_array_type() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_args_with_refs() {
|
||||
let (item, mut errors) = do_stdlib(
|
||||
quote! {
|
||||
name = "someFn",
|
||||
},
|
||||
quote! {
|
||||
/// Docs
|
||||
/// ```
|
||||
/// someFn()
|
||||
/// ```
|
||||
fn someFn(
|
||||
data: &'a str,
|
||||
) -> i32 {
|
||||
3
|
||||
}
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
if let Some(e) = errors.pop() {
|
||||
panic!("{e}");
|
||||
}
|
||||
expectorate::assert_contents("tests/args_with_refs.gen", &get_text_fmt(&item).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_args_with_lifetime() {
|
||||
let (item, mut errors) = do_stdlib(
|
||||
quote! {
|
||||
name = "someFn",
|
||||
},
|
||||
quote! {
|
||||
/// Docs
|
||||
/// ```
|
||||
/// someFn()
|
||||
/// ```
|
||||
fn someFn<'a>(
|
||||
data: Foo<'a>,
|
||||
) -> i32 {
|
||||
3
|
||||
}
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
if let Some(e) = errors.pop() {
|
||||
panic!("{e}");
|
||||
}
|
||||
expectorate::assert_contents("tests/args_with_lifetime.gen", &get_text_fmt(&item).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stdlib_line_to() {
|
||||
let (item, errors) = do_stdlib(
|
||||
@ -64,7 +114,6 @@ fn test_stdlib_line_to() {
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let _expected = quote! {};
|
||||
|
||||
assert!(errors.is_empty());
|
||||
expectorate::assert_contents("tests/lineTo.gen", &get_text_fmt(&item).unwrap());
|
||||
|
194
src/wasm-lib/derive-docs/tests/args_with_lifetime.gen
Normal file
@ -0,0 +1,194 @@
|
||||
#[cfg(test)]
|
||||
mod test_examples_someFn {
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_mock_example_someFn0() {
|
||||
let tokens = crate::token::lexer("someFn()").unwrap();
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
let ctx = crate::executor::ExecutorContext {
|
||||
engine: std::sync::Arc::new(Box::new(
|
||||
crate::engine::conn_mock::EngineConnection::new()
|
||||
.await
|
||||
.unwrap(),
|
||||
)),
|
||||
fs: std::sync::Arc::new(crate::fs::FileManager::new()),
|
||||
stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
|
||||
settings: Default::default(),
|
||||
is_mock: true,
|
||||
};
|
||||
ctx.run(program, None).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||
async fn serial_test_example_someFn0() {
|
||||
let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"),);
|
||||
let http_client = reqwest::Client::builder()
|
||||
.user_agent(user_agent)
|
||||
.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)
|
||||
.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");
|
||||
let mut client = kittycad::Client::new_from_reqwest(token, http_client, ws_client);
|
||||
if let Ok(addr) = std::env::var("LOCAL_ENGINE_ADDR") {
|
||||
client.set_base_url(addr);
|
||||
}
|
||||
|
||||
let tokens = crate::token::lexer("someFn()").unwrap();
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
let ctx = crate::executor::ExecutorContext::new(&client, Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
ctx.run(program, None).await.unwrap();
|
||||
ctx.engine
|
||||
.send_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
crate::executor::SourceRange::default(),
|
||||
kittycad::types::ModelingCmd::ZoomToFit {
|
||||
object_ids: Default::default(),
|
||||
padding: 0.1,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let resp = ctx
|
||||
.engine
|
||||
.send_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
crate::executor::SourceRange::default(),
|
||||
kittycad::types::ModelingCmd::TakeSnapshot {
|
||||
format: kittycad::types::ImageFormat::Png,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let output_file =
|
||||
std::env::temp_dir().join(format!("kcl_output_{}.png", uuid::Uuid::new_v4()));
|
||||
if let kittycad::types::OkWebSocketResponseData::Modeling {
|
||||
modeling_response: kittycad::types::OkModelingCmdResponse::TakeSnapshot { data },
|
||||
} = &resp
|
||||
{
|
||||
std::fs::write(&output_file, &data.contents.0).unwrap();
|
||||
} else {
|
||||
panic!("Unexpected response from engine: {:?}", resp);
|
||||
}
|
||||
|
||||
let actual = image::io::Reader::open(output_file)
|
||||
.unwrap()
|
||||
.decode()
|
||||
.unwrap();
|
||||
twenty_twenty::assert_image(
|
||||
&format!("tests/outputs/{}.png", "serial_test_example_someFn0"),
|
||||
&actual,
|
||||
1.0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types, missing_docs)]
|
||||
#[doc = "Std lib function: someFn\nDocs"]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, schemars :: JsonSchema, ts_rs :: TS)]
|
||||
#[ts(export)]
|
||||
pub(crate) struct SomeFn {}
|
||||
|
||||
#[allow(non_upper_case_globals, missing_docs)]
|
||||
#[doc = "Std lib function: someFn\nDocs"]
|
||||
pub(crate) const SomeFn: SomeFn = SomeFn {};
|
||||
fn boxed_someFn(
|
||||
args: crate::std::Args,
|
||||
) -> std::pin::Pin<
|
||||
Box<
|
||||
dyn std::future::Future<
|
||||
Output = anyhow::Result<crate::executor::MemoryItem, crate::errors::KclError>,
|
||||
> + Send,
|
||||
>,
|
||||
> {
|
||||
Box::pin(someFn(args))
|
||||
}
|
||||
|
||||
impl crate::docs::StdLibFn for SomeFn {
|
||||
fn name(&self) -> String {
|
||||
"someFn".to_string()
|
||||
}
|
||||
|
||||
fn summary(&self) -> String {
|
||||
"Docs".to_string()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"".to_string()
|
||||
}
|
||||
|
||||
fn tags(&self) -> Vec<String> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn args(&self) -> Vec<crate::docs::StdLibFnArg> {
|
||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||
settings.inline_subschemas = true;
|
||||
let mut generator = schemars::gen::SchemaGenerator::new(settings);
|
||||
vec![crate::docs::StdLibFnArg {
|
||||
name: "data".to_string(),
|
||||
type_: "Foo".to_string(),
|
||||
schema: Foo::json_schema(&mut generator),
|
||||
required: true,
|
||||
}]
|
||||
}
|
||||
|
||||
fn return_value(&self) -> Option<crate::docs::StdLibFnArg> {
|
||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||
settings.inline_subschemas = true;
|
||||
let mut generator = schemars::gen::SchemaGenerator::new(settings);
|
||||
Some(crate::docs::StdLibFnArg {
|
||||
name: "".to_string(),
|
||||
type_: "i32".to_string(),
|
||||
schema: <i32>::json_schema(&mut generator),
|
||||
required: true,
|
||||
})
|
||||
}
|
||||
|
||||
fn unpublished(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn deprecated(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<String> {
|
||||
let code_blocks = vec!["someFn()"];
|
||||
code_blocks
|
||||
.iter()
|
||||
.map(|cb| {
|
||||
let tokens = crate::token::lexer(cb).unwrap();
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
let mut options: crate::ast::types::FormatOptions = Default::default();
|
||||
options.insert_final_newline = false;
|
||||
program.recast(&options, 0)
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
}
|
||||
|
||||
fn std_lib_fn(&self) -> crate::std::StdFn {
|
||||
boxed_someFn
|
||||
}
|
||||
|
||||
fn clone_box(&self) -> Box<dyn crate::docs::StdLibFn> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[doc = r" Docs"]
|
||||
#[doc = r" ```"]
|
||||
#[doc = r" someFn()"]
|
||||
#[doc = r" ```"]
|
||||
fn someFn<'a>(data: Foo<'a>) -> i32 {
|
||||
3
|
||||
}
|
194
src/wasm-lib/derive-docs/tests/args_with_refs.gen
Normal file
@ -0,0 +1,194 @@
|
||||
#[cfg(test)]
|
||||
mod test_examples_someFn {
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_mock_example_someFn0() {
|
||||
let tokens = crate::token::lexer("someFn()").unwrap();
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
let ctx = crate::executor::ExecutorContext {
|
||||
engine: std::sync::Arc::new(Box::new(
|
||||
crate::engine::conn_mock::EngineConnection::new()
|
||||
.await
|
||||
.unwrap(),
|
||||
)),
|
||||
fs: std::sync::Arc::new(crate::fs::FileManager::new()),
|
||||
stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
|
||||
settings: Default::default(),
|
||||
is_mock: true,
|
||||
};
|
||||
ctx.run(program, None).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||
async fn serial_test_example_someFn0() {
|
||||
let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"),);
|
||||
let http_client = reqwest::Client::builder()
|
||||
.user_agent(user_agent)
|
||||
.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)
|
||||
.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");
|
||||
let mut client = kittycad::Client::new_from_reqwest(token, http_client, ws_client);
|
||||
if let Ok(addr) = std::env::var("LOCAL_ENGINE_ADDR") {
|
||||
client.set_base_url(addr);
|
||||
}
|
||||
|
||||
let tokens = crate::token::lexer("someFn()").unwrap();
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
let ctx = crate::executor::ExecutorContext::new(&client, Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
ctx.run(program, None).await.unwrap();
|
||||
ctx.engine
|
||||
.send_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
crate::executor::SourceRange::default(),
|
||||
kittycad::types::ModelingCmd::ZoomToFit {
|
||||
object_ids: Default::default(),
|
||||
padding: 0.1,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let resp = ctx
|
||||
.engine
|
||||
.send_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
crate::executor::SourceRange::default(),
|
||||
kittycad::types::ModelingCmd::TakeSnapshot {
|
||||
format: kittycad::types::ImageFormat::Png,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let output_file =
|
||||
std::env::temp_dir().join(format!("kcl_output_{}.png", uuid::Uuid::new_v4()));
|
||||
if let kittycad::types::OkWebSocketResponseData::Modeling {
|
||||
modeling_response: kittycad::types::OkModelingCmdResponse::TakeSnapshot { data },
|
||||
} = &resp
|
||||
{
|
||||
std::fs::write(&output_file, &data.contents.0).unwrap();
|
||||
} else {
|
||||
panic!("Unexpected response from engine: {:?}", resp);
|
||||
}
|
||||
|
||||
let actual = image::io::Reader::open(output_file)
|
||||
.unwrap()
|
||||
.decode()
|
||||
.unwrap();
|
||||
twenty_twenty::assert_image(
|
||||
&format!("tests/outputs/{}.png", "serial_test_example_someFn0"),
|
||||
&actual,
|
||||
1.0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types, missing_docs)]
|
||||
#[doc = "Std lib function: someFn\nDocs"]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, schemars :: JsonSchema, ts_rs :: TS)]
|
||||
#[ts(export)]
|
||||
pub(crate) struct SomeFn {}
|
||||
|
||||
#[allow(non_upper_case_globals, missing_docs)]
|
||||
#[doc = "Std lib function: someFn\nDocs"]
|
||||
pub(crate) const SomeFn: SomeFn = SomeFn {};
|
||||
fn boxed_someFn(
|
||||
args: crate::std::Args,
|
||||
) -> std::pin::Pin<
|
||||
Box<
|
||||
dyn std::future::Future<
|
||||
Output = anyhow::Result<crate::executor::MemoryItem, crate::errors::KclError>,
|
||||
> + Send,
|
||||
>,
|
||||
> {
|
||||
Box::pin(someFn(args))
|
||||
}
|
||||
|
||||
impl crate::docs::StdLibFn for SomeFn {
|
||||
fn name(&self) -> String {
|
||||
"someFn".to_string()
|
||||
}
|
||||
|
||||
fn summary(&self) -> String {
|
||||
"Docs".to_string()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"".to_string()
|
||||
}
|
||||
|
||||
fn tags(&self) -> Vec<String> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn args(&self) -> Vec<crate::docs::StdLibFnArg> {
|
||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||
settings.inline_subschemas = true;
|
||||
let mut generator = schemars::gen::SchemaGenerator::new(settings);
|
||||
vec![crate::docs::StdLibFnArg {
|
||||
name: "data".to_string(),
|
||||
type_: "string".to_string(),
|
||||
schema: str::json_schema(&mut generator),
|
||||
required: true,
|
||||
}]
|
||||
}
|
||||
|
||||
fn return_value(&self) -> Option<crate::docs::StdLibFnArg> {
|
||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||
settings.inline_subschemas = true;
|
||||
let mut generator = schemars::gen::SchemaGenerator::new(settings);
|
||||
Some(crate::docs::StdLibFnArg {
|
||||
name: "".to_string(),
|
||||
type_: "i32".to_string(),
|
||||
schema: <i32>::json_schema(&mut generator),
|
||||
required: true,
|
||||
})
|
||||
}
|
||||
|
||||
fn unpublished(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn deprecated(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<String> {
|
||||
let code_blocks = vec!["someFn()"];
|
||||
code_blocks
|
||||
.iter()
|
||||
.map(|cb| {
|
||||
let tokens = crate::token::lexer(cb).unwrap();
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
let mut options: crate::ast::types::FormatOptions = Default::default();
|
||||
options.insert_final_newline = false;
|
||||
program.recast(&options, 0)
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
}
|
||||
|
||||
fn std_lib_fn(&self) -> crate::std::StdFn {
|
||||
boxed_someFn
|
||||
}
|
||||
|
||||
fn clone_box(&self) -> Box<dyn crate::docs::StdLibFn> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[doc = r" Docs"]
|
||||
#[doc = r" ```"]
|
||||
#[doc = r" someFn()"]
|
||||
#[doc = r" ```"]
|
||||
fn someFn(data: &'a str) -> i32 {
|
||||
3
|
||||
}
|
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()
|
||||
@ -1110,7 +1135,7 @@ impl CallExpression {
|
||||
match ctx.stdlib.get_either(&self.callee.name) {
|
||||
FunctionKind::Core(func) => {
|
||||
// Attempt to call the function.
|
||||
let args = crate::std::Args::new(fn_args, self.into(), ctx.clone());
|
||||
let args = crate::std::Args::new(fn_args, self.into(), ctx.clone(), memory.clone());
|
||||
let result = func.std_lib_fn()(args).await?;
|
||||
Ok(result)
|
||||
}
|
||||
@ -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,17 +82,18 @@ 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);
|
||||
|
||||
// Return a fake modeling_request empty response.
|
||||
if !is_sending {
|
||||
return Ok(OkWebSocketResponseData::Modeling {
|
||||
modeling_response: kittycad::types::OkModelingCmdResponse::Empty {},
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
} => {
|
||||
// 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)?;
|
||||
|
||||
// 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],
|
||||
}));
|
||||
})),
|
||||
}
|
||||
};
|
||||
|
||||
self.inner_send_modeling_cmd(id_final, source_range, final_req, id_to_source_range)
|
||||
.await
|
||||
}
|
||||
|
||||
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],
|
||||
})
|
||||
}
|
||||
|
||||
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],
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
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_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]],
|
||||
})
|
||||
})?;
|
||||
|
||||
true
|
||||
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},
|
||||
};
|
||||
|
||||
@ -188,6 +189,15 @@ pub enum SketchGroupSet {
|
||||
SketchGroups(Vec<Box<SketchGroup>>),
|
||||
}
|
||||
|
||||
impl SketchGroupSet {
|
||||
pub fn ids(&self) -> Vec<uuid::Uuid> {
|
||||
match self {
|
||||
SketchGroupSet::SketchGroup(s) => vec![s.id],
|
||||
SketchGroupSet::SketchGroups(s) => s.iter().map(|s| s.id).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A extrude group or a group of extrude groups.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
@ -197,6 +207,15 @@ pub enum ExtrudeGroupSet {
|
||||
ExtrudeGroups(Vec<Box<ExtrudeGroup>>),
|
||||
}
|
||||
|
||||
impl ExtrudeGroupSet {
|
||||
pub fn ids(&self) -> Vec<uuid::Uuid> {
|
||||
match self {
|
||||
ExtrudeGroupSet::ExtrudeGroup(s) => vec![s.id],
|
||||
ExtrudeGroupSet::ExtrudeGroups(s) => s.iter().map(|s| s.id).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Data for an imported geometry.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
@ -297,6 +316,39 @@ pub struct UserVal {
|
||||
pub meta: Vec<Metadata>,
|
||||
}
|
||||
|
||||
/// A function being used as a parameter into a stdlib function.
|
||||
pub struct FunctionParam<'a> {
|
||||
pub inner: &'a MemoryFunction,
|
||||
pub memory: ProgramMemory,
|
||||
pub fn_expr: Box<FunctionExpression>,
|
||||
pub meta: Vec<Metadata>,
|
||||
pub ctx: ExecutorContext,
|
||||
}
|
||||
|
||||
impl<'a> FunctionParam<'a> {
|
||||
pub async fn call(&self, args: Vec<MemoryItem>) -> Result<Option<ProgramReturn>, KclError> {
|
||||
(self.inner)(
|
||||
args,
|
||||
self.memory.clone(),
|
||||
self.fn_expr.clone(),
|
||||
self.meta.clone(),
|
||||
self.ctx.clone(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> JsonSchema for FunctionParam<'a> {
|
||||
fn schema_name() -> String {
|
||||
"FunctionParam".to_owned()
|
||||
}
|
||||
|
||||
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
|
||||
// TODO: Actually generate a reasonable schema.
|
||||
gen.subschema_for::<()>()
|
||||
}
|
||||
}
|
||||
|
||||
pub type MemoryFunction =
|
||||
fn(
|
||||
s: Vec<MemoryItem>,
|
||||
@ -412,6 +464,88 @@ impl MemoryItem {
|
||||
};
|
||||
func(args, memory, expression.clone(), meta.clone(), ctx).await
|
||||
}
|
||||
|
||||
fn as_user_val(&self) -> Option<&UserVal> {
|
||||
if let MemoryItem::UserVal(x) = self {
|
||||
Some(x)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// If this value is of type function, return it.
|
||||
pub fn get_function(
|
||||
&self,
|
||||
source_ranges: Vec<SourceRange>,
|
||||
) -> Result<(&MemoryFunction, Box<FunctionExpression>), KclError> {
|
||||
let MemoryItem::Function {
|
||||
func,
|
||||
expression,
|
||||
meta: _,
|
||||
} = &self
|
||||
else {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: "not an in-memory function".to_string(),
|
||||
source_ranges,
|
||||
}));
|
||||
};
|
||||
let func = func.as_ref().ok_or_else(|| {
|
||||
KclError::Semantic(KclErrorDetails {
|
||||
message: format!("Not an in-memory function: {:?}", expression),
|
||||
source_ranges,
|
||||
})
|
||||
})?;
|
||||
Ok((func, expression.to_owned()))
|
||||
}
|
||||
|
||||
/// If this value is of type u32, return it.
|
||||
pub fn get_u32(&self, source_ranges: Vec<SourceRange>) -> Result<u32, KclError> {
|
||||
let err = KclError::Semantic(KclErrorDetails {
|
||||
message: "Expected an integer >= 0".to_owned(),
|
||||
source_ranges,
|
||||
});
|
||||
self.as_user_val()
|
||||
.and_then(|uv| uv.value.as_number())
|
||||
.and_then(|n| n.as_u64())
|
||||
.and_then(|n| u32::try_from(n).ok())
|
||||
.ok_or(err)
|
||||
}
|
||||
|
||||
/// If this contains a sketch group set, return it.
|
||||
pub(crate) fn as_sketch_group_set(&self, sr: SourceRange) -> Result<SketchGroupSet, KclError> {
|
||||
let sketch_set = if let MemoryItem::SketchGroup(sg) = self {
|
||||
SketchGroupSet::SketchGroup(sg.clone())
|
||||
} else if let MemoryItem::SketchGroups { value } = self {
|
||||
SketchGroupSet::SketchGroups(value.clone())
|
||||
} else {
|
||||
return Err(KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected a SketchGroup or Vector of SketchGroups as this argument, found {:?}",
|
||||
self,
|
||||
),
|
||||
source_ranges: vec![sr],
|
||||
}));
|
||||
};
|
||||
Ok(sketch_set)
|
||||
}
|
||||
|
||||
/// If this contains an extrude group set, return it.
|
||||
pub(crate) fn as_extrude_group_set(&self, sr: SourceRange) -> Result<ExtrudeGroupSet, KclError> {
|
||||
let sketch_set = if let MemoryItem::ExtrudeGroup(sg) = self {
|
||||
ExtrudeGroupSet::ExtrudeGroup(sg.clone())
|
||||
} else if let MemoryItem::ExtrudeGroups { value } = self {
|
||||
ExtrudeGroupSet::ExtrudeGroups(value.clone())
|
||||
} else {
|
||||
return Err(KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected an ExtrudeGroup or Vector of ExtrudeGroups as this argument, found {:?}",
|
||||
self,
|
||||
),
|
||||
source_ranges: vec![sr],
|
||||
}));
|
||||
};
|
||||
Ok(sketch_set)
|
||||
}
|
||||
}
|
||||
|
||||
/// A sketch group is a collection of paths.
|
||||
@ -616,6 +750,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 +1127,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 +1200,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 +1218,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 +1279,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?;
|
||||
@ -1140,7 +1326,7 @@ impl ExecutorContext {
|
||||
}
|
||||
match self.stdlib.get_either(&call_expr.callee.name) {
|
||||
FunctionKind::Core(func) => {
|
||||
let args = crate::std::Args::new(args, call_expr.into(), self.clone());
|
||||
let args = crate::std::Args::new(args, call_expr.into(), self.clone(), memory.clone());
|
||||
let result = func.std_lib_fn()(args).await?;
|
||||
memory.return_ = Some(ProgramReturn::Value(result));
|
||||
}
|
||||
@ -1309,7 +1495,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,9 +1,12 @@
|
||||
use super::Node;
|
||||
use crate::ast::types::{
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::{
|
||||
ast::types::{
|
||||
BinaryPart, BodyItem, LiteralIdentifier, MemberExpression, MemberObject, ObjectExpression, ObjectProperty,
|
||||
Parameter, Program, UnaryExpression, Value, VariableDeclarator,
|
||||
},
|
||||
lint::Node,
|
||||
};
|
||||
use anyhow::Result;
|
||||
|
||||
/// 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
|
||||
|
@ -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},
|
||||
|
@ -25,14 +25,15 @@ use lazy_static::lazy_static;
|
||||
use parse_display::{Display, FromStr};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
ast::types::parse_json_number_as_f64,
|
||||
ast::types::{parse_json_number_as_f64, FunctionExpression},
|
||||
docs::StdLibFn,
|
||||
errors::{KclError, KclErrorDetails},
|
||||
executor::{
|
||||
ExecutorContext, ExtrudeGroup, ExtrudeGroupSet, MemoryItem, Metadata, SketchGroup, SketchGroupSet,
|
||||
SketchSurface, SourceRange,
|
||||
ExecutorContext, ExtrudeGroup, ExtrudeGroupSet, MemoryFunction, MemoryItem, Metadata, ProgramMemory,
|
||||
SketchGroup, SketchGroupSet, SketchSurface, SourceRange,
|
||||
},
|
||||
std::{kcl_stdlib::KclStdLibFn, sketch::SketchOnFaceTag},
|
||||
};
|
||||
@ -84,6 +85,7 @@ lazy_static! {
|
||||
Box::new(crate::std::patterns::PatternLinear3D),
|
||||
Box::new(crate::std::patterns::PatternCircular2D),
|
||||
Box::new(crate::std::patterns::PatternCircular3D),
|
||||
Box::new(crate::std::patterns::Pattern),
|
||||
Box::new(crate::std::chamfer::Chamfer),
|
||||
Box::new(crate::std::fillet::Fillet),
|
||||
Box::new(crate::std::fillet::GetOppositeEdge),
|
||||
@ -204,17 +206,30 @@ pub struct Args {
|
||||
pub args: Vec<MemoryItem>,
|
||||
pub source_range: SourceRange,
|
||||
pub ctx: ExecutorContext,
|
||||
// TODO: This should be reference, not clone.
|
||||
pub memory: ProgramMemory,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
pub fn new(args: Vec<MemoryItem>, source_range: SourceRange, ctx: ExecutorContext) -> Self {
|
||||
pub fn new(args: Vec<MemoryItem>, source_range: SourceRange, ctx: ExecutorContext, memory: ProgramMemory) -> Self {
|
||||
Self {
|
||||
args,
|
||||
source_range,
|
||||
ctx,
|
||||
memory,
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
@ -377,6 +392,41 @@ impl Args {
|
||||
}
|
||||
}
|
||||
|
||||
/// Works with either 2D or 3D solids.
|
||||
fn get_pattern_args(&self) -> std::result::Result<(u32, FnAsArg<'_>, Vec<Uuid>), KclError> {
|
||||
let sr = vec![self.source_range];
|
||||
let mut args = self.args.iter();
|
||||
let num_repetitions = args.next().ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: "Missing first argument (should be the number of repetitions)".to_owned(),
|
||||
source_ranges: sr.clone(),
|
||||
})
|
||||
})?;
|
||||
let num_repetitions = num_repetitions.get_u32(sr.clone())?;
|
||||
let transform = args.next().ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: "Missing second argument (should be the transform function)".to_owned(),
|
||||
source_ranges: sr.clone(),
|
||||
})
|
||||
})?;
|
||||
let (transform, expr) = transform.get_function(sr.clone())?;
|
||||
let sg = args.next().ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: "Missing third argument (should be a Sketch/ExtrudeGroup or an array of Sketch/ExtrudeGroups)"
|
||||
.to_owned(),
|
||||
source_ranges: sr.clone(),
|
||||
})
|
||||
})?;
|
||||
let sketch_ids = sg.as_sketch_group_set(self.source_range);
|
||||
let extrude_ids = sg.as_extrude_group_set(self.source_range);
|
||||
let entity_ids = match (sketch_ids, extrude_ids) {
|
||||
(Ok(group), _) => group.ids(),
|
||||
(_, Ok(group)) => group.ids(),
|
||||
(Err(e), _) => return Err(e),
|
||||
};
|
||||
Ok((num_repetitions, FnAsArg { func: transform, expr }, entity_ids))
|
||||
}
|
||||
|
||||
fn get_segment_name_sketch_group(&self) -> Result<(String, Box<SketchGroup>), KclError> {
|
||||
// Iterate over our args, the first argument should be a UserVal with a string value.
|
||||
// The second argument should be a SketchGroup.
|
||||
@ -427,19 +477,7 @@ impl Args {
|
||||
})
|
||||
})?;
|
||||
|
||||
let sketch_set = if let MemoryItem::SketchGroup(sg) = first_value {
|
||||
SketchGroupSet::SketchGroup(sg.clone())
|
||||
} else if let MemoryItem::SketchGroups { value } = first_value {
|
||||
SketchGroupSet::SketchGroups(value.clone())
|
||||
} else {
|
||||
return Err(KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected a SketchGroup or Vector of SketchGroups as the first argument, found `{:?}`",
|
||||
self.args
|
||||
),
|
||||
source_ranges: vec![self.source_range],
|
||||
}));
|
||||
};
|
||||
let sketch_set = first_value.as_sketch_group_set(self.source_range)?;
|
||||
|
||||
let second_value = self.args.get(1).ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
@ -662,19 +700,7 @@ impl Args {
|
||||
})
|
||||
})?;
|
||||
|
||||
let sketch_set = if let MemoryItem::SketchGroup(sg) = second_value {
|
||||
SketchGroupSet::SketchGroup(sg.clone())
|
||||
} else if let MemoryItem::SketchGroups { value } = second_value {
|
||||
SketchGroupSet::SketchGroups(value.clone())
|
||||
} else {
|
||||
return Err(KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected a SketchGroup or Vector of SketchGroups as the second argument, found `{:?}`",
|
||||
self.args
|
||||
),
|
||||
source_ranges: vec![self.source_range],
|
||||
}));
|
||||
};
|
||||
let sketch_set = second_value.as_sketch_group_set(self.source_range)?;
|
||||
|
||||
Ok((data, sketch_set))
|
||||
}
|
||||
@ -943,19 +969,7 @@ impl Args {
|
||||
})
|
||||
})?;
|
||||
|
||||
let sketch_set = if let MemoryItem::SketchGroup(sg) = second_value {
|
||||
SketchGroupSet::SketchGroup(sg.clone())
|
||||
} else if let MemoryItem::SketchGroups { value } = second_value {
|
||||
SketchGroupSet::SketchGroups(value.clone())
|
||||
} else {
|
||||
return Err(KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected a SketchGroup or Vector of SketchGroups as the second argument, found `{:?}`",
|
||||
self.args
|
||||
),
|
||||
source_ranges: vec![self.source_range],
|
||||
}));
|
||||
};
|
||||
let sketch_set = second_value.as_sketch_group_set(self.source_range)?;
|
||||
|
||||
Ok((number, sketch_set))
|
||||
}
|
||||
@ -1036,6 +1050,11 @@ pub enum Primitive {
|
||||
Uuid,
|
||||
}
|
||||
|
||||
struct FnAsArg<'a> {
|
||||
pub func: &'a MemoryFunction,
|
||||
pub expr: Box<FunctionExpression>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use base64::Engine;
|
||||
|