Compare commits

..

64 Commits

Author SHA1 Message Date
2a4ff386e0 Merge branch 'main' into coredump-clientstate 2024-06-10 18:21:14 -07:00
2be00f8f52 Merge branch 'main' into coredump-clientstate 2024-06-03 11:14:29 -07:00
5d66fc496f Merge branch 'main' into coredump-clientstate 2024-05-31 10:31:46 -07:00
764675d411 Merge branch 'main' into coredump-clientstate 2024-05-30 14:22:03 -07:00
4482b0fee4 Hoist upload_coredump to resolve scoping issues 2024-05-30 14:21:37 -07:00
a604660feb Revert to original state
I added a bunch of structs to be able to implement clientstate, but then was able to resolve these by upgrading ts-rs. The import statement had a lot of entries in the intermediate state. This reverts to the prior state in the codebase. No need for a reformatting change.
2024-05-30 14:11:52 -07:00
0465aa06d3 Merge branch 'main' into coredump-clientstate 2024-05-29 22:44:36 -07:00
62302ccfda WIP coredump json upload
I need @paultag or someone to help guide me through passing the coredump around properly.
2024-05-29 22:42:11 -07:00
313e586510 Remove window.Zoo for now
We're going to come back and do the xstate state capture after the update to xstate 5 which will provide a native api for both accessing xstate state and restoring state, which will be useful for debugging.
2024-05-29 22:40:28 -07:00
0b9f707063 WIP Upload json 2024-05-29 17:32:45 -07:00
fb7b0aadfb prettier fmt coredump.ts 2024-05-29 17:29:09 -07:00
cf54905539 prettier fmt 2024-05-29 17:28:30 -07:00
2fb318b154 Attach Zoo to window 2024-05-29 12:30:23 -07:00
9366f68a88 window.Zoo 2024-05-29 12:24:46 -07:00
6985678e25 Editor Manager 2024-05-29 12:24:34 -07:00
c56319fb74 Engine Command Manage console.log
Make coredump console log output more readable and traceable.
2024-05-29 09:33:39 -07:00
ec20e9752f Rust gimme JSON
This brings the code back to my original intent and strategy when I started building out this solution: gather all the JSON we may need under `client_state` and pass it back. 🎉
2024-05-29 09:17:04 -07:00
88b94c985f Finish KCL Manager coredump 2024-05-28 20:29:00 -07:00
d455189cb1 Simplify kcl_manager in Rust
MOAR JSON
2024-05-28 20:28:04 -07:00
27974dee04 Simplify XState representation in Rust 2024-05-28 19:54:37 -07:00
51acd5d6bf Simplify ModelingMachine expectations in Rust 2024-05-28 18:04:40 -07:00
48774ba8bf Capturing Scene Infra in CoreDump
️ Using JavaScript 
2024-05-28 13:31:43 -07:00
5a4834f37c More serde_json::Value less predefined struct 2024-05-28 12:16:17 -07:00
e30b634974 Add kclManager AST and kclErrors 2024-05-28 11:44:41 -07:00
6c9d0378f0 Update Cargo.lock 2024-05-28 09:24:54 -07:00
717838c702 Merge branch 'main' into coredump-clientstate 2024-05-28 09:24:46 -07:00
a92acc68a8 TODO? more singletons
Should we log anything from these?
2024-05-27 23:45:04 -07:00
0ca7095777 Gather KCL Manager info for CoreDump 2024-05-27 23:44:00 -07:00
8ce3b32f75 Update Cargo.lock 2024-05-27 23:42:01 -07:00
c0826690a1 Merge branch 'main' into coredump-clientstate 2024-05-27 15:47:25 -07:00
1fa03a8d58 Output clientState as JSON 2024-05-24 00:39:48 -07:00
9ea92d5a6d coredump engine command manager
Complete?!?
2024-05-24 00:22:13 -07:00
81bf96d52f Update Cargo.lock 2024-05-23 22:51:56 -07:00
3bbd63eb5d Merge branch 'main' into coredump-clientstate 2024-05-23 22:44:46 -07:00
2b0af61b09 deepClone types 2024-05-23 22:38:45 -07:00
a671fcb128 fmt 2024-05-23 19:07:21 -07:00
cbb5f67364 Remove token debug
Not sure why I committed this anyway
2024-05-23 19:07:21 -07:00
3a64f29ca6 Add sample artifact map 2024-05-23 19:07:21 -07:00
4cbf2d19b5 Not using HashMap...yet
serde_json::Value to the rescue. Thanks @paultag
2024-05-23 19:07:21 -07:00
64457cd2c2 serde_json::Value 💖 save us 2024-05-23 19:07:21 -07:00
4571ea4e08 WIP ArtifactMap dump 2024-05-23 19:07:21 -07:00
b586b38ad9 coredump typescript implementation
Connection state
2024-05-23 19:07:21 -07:00
8766861d07 Enable connection type dump 2024-05-23 19:07:21 -07:00
abe70f4406 Update coredump.fixture.json 2024-05-23 19:07:21 -07:00
a4452f775b Add serde_json with new support for ts-rs 2024-05-23 19:07:21 -07:00
b06158ba4e Patch ts-rs to 8.1.0 with paultag's fixes 2024-05-23 19:06:16 -07:00
7a42e9e868 Pull in more types 2024-05-23 19:04:33 -07:00
4e856abbcb Types for Engine Manager and Connection state 2024-05-23 19:04:33 -07:00
fef8139a3e Parse out XState Services 2024-05-23 19:01:48 -07:00
b578a88808 Need to upgrade ts-rs to 8.1 for serde json types 2024-05-23 19:01:48 -07:00
a33c15b667 Change _unused to meta 2024-05-23 19:01:48 -07:00
bdf1eef0d5 Properly import ClientState 2024-05-23 19:01:48 -07:00
41d900ad93 migrate over a few more app infos 2024-05-23 19:01:48 -07:00
5796ce02c3 Remove dummy response 2024-05-23 19:01:48 -07:00
229a89fb1d Convert AppInfo to CoreDumpInfo 2024-05-23 19:01:48 -07:00
6343545496 WIP Filling out state data 2024-05-23 19:01:48 -07:00
779b7038dd Include all the state objects 2024-05-23 19:01:48 -07:00
10cbfddc41 Add singleton base structure definitiions 2024-05-23 19:01:48 -07:00
958b68f06f Add XState base structure definitions 2024-05-23 19:01:48 -07:00
522dfdcbcc Removed unused reference 2024-05-23 19:01:48 -07:00
0ac1cbfcf9 Start to implement the ClienState struct
Signed-off-by: Paul R. Tagliamonte <paul@kittycad.io>
2024-05-23 19:01:01 -07:00
950f27ce2a Add client_state to coredump fixture 2024-05-23 19:01:01 -07:00
62e6559177 Create coredump.fixture.json
With data from #2316
2024-05-23 19:01:01 -07:00
8fbacd0363 WIP client state core dump plumbing 2024-05-23 19:01:01 -07:00
139 changed files with 4206 additions and 14487 deletions

View File

@ -1,3 +1,3 @@
[codespell]
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey,atleast,ue
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey,atleast
skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md,./src-tauri/gen/schemas

1
.envrc
View File

@ -1 +0,0 @@
use flake .

View File

@ -1,40 +0,0 @@
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

View File

@ -9,12 +9,6 @@ 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
@ -60,8 +54,3 @@ jobs:
run: |
cd "${{ matrix.dir }}"
cargo clippy --all --tests --benches -- -D warnings
# If this fails, run "cargo check" to update Cargo.lock,
# then add Cargo.lock to the PR.
- name: Check Cargo.lock doesn't need updating
run: |
cargo check --locked || echo "Pls run cargo check and commit the changed Cargo.lock"

View File

@ -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@v6
uses: dawidd6/action-download-artifact@v5
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-ubuntu
name: playwright-report
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@v6
uses: dawidd6/action-download-artifact@v5
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-macos
name: playwright-report
path: playwright-report/
retention-days: 30

1
.gitignore vendored
View File

@ -17,7 +17,6 @@
.env.development.local
.env.test.local
.env.production.local
.direnv
npm-debug.log*
yarn-debug.log*

View File

@ -319,7 +319,7 @@ PS: for the debug panel, the following JSON is useful for snapping the camera
```
yarn install
yarn build:wasm-dev
yarn build:wasm
cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
yarn vite build --mode development
yarn tauri build --debug -b

File diff suppressed because one or more lines are too long

View File

@ -23,7 +23,6 @@ layout: manual
* [`atan`](kcl/atan)
* [`bezierCurve`](kcl/bezierCurve)
* [`ceil`](kcl/ceil)
* [`chamfer`](kcl/chamfer)
* [`circle`](kcl/circle)
* [`close`](kcl/close)
* [`cos`](kcl/cos)
@ -65,7 +64,6 @@ layout: manual
* [`segEndX`](kcl/segEndX)
* [`segEndY`](kcl/segEndY)
* [`segLen`](kcl/segLen)
* [`shell`](kcl/shell)
* [`sin`](kcl/sin)
* [`sqrt`](kcl/sqrt)
* [`startProfileAt`](kcl/startProfileAt)

View File

@ -9,7 +9,7 @@ A circular pattern on a 2D sketch.
```js
patternCircular2d(data: CircularPattern2dData, sketch_group_set: SketchGroupSet) -> [SketchGroup]
patternCircular2d(data: CircularPattern2dData, sketch_group: SketchGroup) -> [SketchGroup]
```
### Examples
@ -48,7 +48,7 @@ const example = extrude(1, exampleSketch)
rotateDuplicates: string,
}
```
* `sketch_group_set`: `SketchGroupSet` - A sketch group or a group of sketch groups. (REQUIRED)
* `sketch_group`: `SketchGroup` - A sketch group is a collection of paths. (REQUIRED)
```js
{
// The plane id or face id of the sketch group.
@ -129,7 +129,6 @@ const example = extrude(1, exampleSketch)
// The to point.
to: [number, number],
},
type: "sketchGroup",
// The paths in the sketch group.
value: [{
// The from point.
@ -213,9 +212,6 @@ const example = extrude(1, exampleSketch)
y: number,
z: number,
},
} |
{
type: "sketchGroups",
}
```

View File

@ -9,7 +9,7 @@ A circular pattern on a 3D model.
```js
patternCircular3d(data: CircularPattern3dData, extrude_group_set: ExtrudeGroupSet) -> [ExtrudeGroup]
patternCircular3d(data: CircularPattern3dData, extrude_group: ExtrudeGroup) -> [ExtrudeGroup]
```
### Examples
@ -47,7 +47,7 @@ const example = extrude(-5, exampleSketch)
rotateDuplicates: string,
}
```
* `extrude_group_set`: `ExtrudeGroupSet` - A extrude group or a group of extrude groups. (REQUIRED)
* `extrude_group`: `ExtrudeGroup` - An extrude group is a collection of extrude surfaces. (REQUIRED)
```js
{
// The id of the extrusion end cap
@ -127,7 +127,6 @@ const example = extrude(-5, exampleSketch)
}],
// The id of the extrusion start cap
startCapId: uuid,
type: "extrudeGroup",
// The extrude surfaces.
value: [{
// The face id for the extrude plane.
@ -177,9 +176,6 @@ const example = extrude(-5, exampleSketch)
y: number,
z: number,
},
} |
{
type: "extrudeGroups",
}
```

View File

@ -9,7 +9,7 @@ A linear pattern on a 3D model.
```js
patternLinear3d(data: LinearPattern3dData, extrude_group_set: ExtrudeGroupSet) -> [ExtrudeGroup]
patternLinear3d(data: LinearPattern3dData, extrude_group: ExtrudeGroup) -> [ExtrudeGroup]
```
### Examples
@ -45,7 +45,7 @@ const example = extrude(1, exampleSketch)
repetitions: number,
}
```
* `extrude_group_set`: `ExtrudeGroupSet` - A extrude group or a group of extrude groups. (REQUIRED)
* `extrude_group`: `ExtrudeGroup` - An extrude group is a collection of extrude surfaces. (REQUIRED)
```js
{
// The id of the extrusion end cap
@ -125,7 +125,6 @@ const example = extrude(1, exampleSketch)
}],
// The id of the extrusion start cap
startCapId: uuid,
type: "extrudeGroup",
// The extrude surfaces.
value: [{
// The face id for the extrude plane.
@ -175,9 +174,6 @@ const example = extrude(1, exampleSketch)
y: number,
z: number,
},
} |
{
type: "extrudeGroups",
}
```

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -405,16 +405,17 @@ test('Draft segments should look right', async ({ page, context }) => {
// select a plane
await page.mouse.click(700, 200)
let code = `const sketch001 = startSketchOn('XZ')`
await expect(page.locator('.cm-content')).toHaveText(code)
await expect(page.locator('.cm-content')).toHaveText(
`const sketch001 = startSketchOn('XZ')`
)
await page.waitForTimeout(700) // TODO detect animation ending, or disable animation
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
code += `
|> startProfileAt([7.19, -9.7], %)`
await expect(page.locator('.cm-content')).toHaveText(code)
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt([9.06, -12.22], %)`)
await page.waitForTimeout(100)
await u.closeDebugPanel()
@ -426,9 +427,10 @@ test('Draft segments should look right', async ({ page, context }) => {
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100)
code += `
|> line([7.25, 0], %)`
await expect(page.locator('.cm-content')).toHaveText(code)
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt([9.06, -12.22], %)
|> line([9.14, 0], %)`)
await page.getByRole('button', { name: 'Tangential Arc' }).click()
@ -511,16 +513,17 @@ test.describe('Client side scene scale should match engine scale', () => {
// select a plane
await page.mouse.click(700, 200)
let code = `const sketch001 = startSketchOn('XZ')`
await expect(page.locator('.cm-content')).toHaveText(code)
await expect(page.locator('.cm-content')).toHaveText(
`const sketch001 = startSketchOn('XZ')`
)
await page.waitForTimeout(600) // TODO detect animation ending, or disable animation
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
code += `
|> startProfileAt([7.19, -9.7], %)`
await expect(u.codeLocator).toHaveText(code)
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt([9.06, -12.22], %)`)
await page.waitForTimeout(100)
await u.closeDebugPanel()
@ -528,18 +531,21 @@ test.describe('Client side scene scale should match engine scale', () => {
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100)
code += `
|> line([7.25, 0], %)`
await expect(u.codeLocator).toHaveText(code)
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt([9.06, -12.22], %)
|> line([9.14, 0], %)`)
await page.getByRole('button', { name: 'Tangential Arc' }).click()
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
code += `
|> tangentialArcTo([21.7, -2.44], %)`
await expect(u.codeLocator).toHaveText(code)
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], %)`)
// click tangential arc tool again to unequip it
await page.getByRole('button', { name: 'Tangential Arc' }).click()
@ -610,16 +616,17 @@ test.describe('Client side scene scale should match engine scale', () => {
// select a plane
await page.mouse.click(700, 200)
let code = `const sketch001 = startSketchOn('XZ')`
await expect(u.codeLocator).toHaveText(code)
await expect(page.locator('.cm-content')).toHaveText(
`const sketch001 = startSketchOn('XZ')`
)
await page.waitForTimeout(600) // TODO detect animation ending, or disable animation
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
code += `
|> startProfileAt([182.59, -246.32], %)`
await expect(u.codeLocator).toHaveText(code)
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt([230.03, -310.32], %)`)
await page.waitForTimeout(100)
await u.closeDebugPanel()
@ -627,18 +634,21 @@ test.describe('Client side scene scale should match engine scale', () => {
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100)
code += `
|> line([184.3, 0], %)`
await expect(u.codeLocator).toHaveText(code)
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt([230.03, -310.32], %)
|> line([232.2, 0], %)`)
await page.getByRole('button', { name: 'Tangential Arc' }).click()
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
code += `
|> tangentialArcTo([551.2, -62.01], %)`
await expect(u.codeLocator).toHaveText(code)
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], %)`)
await page.getByRole('button', { name: 'Tangential Arc' }).click()
await page.waitForTimeout(100)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View File

@ -1,6 +1,5 @@
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 = {
@ -23,22 +22,9 @@ 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: onboardingPaths.EXPORT },
} satisfies Partial<SaveSettingsPayload>
export const TEST_SETTINGS_ONBOARDING_PARAMETRIC_MODELING = {
...TEST_SETTINGS,
app: {
...TEST_SETTINGS.app,
onboardingStatus: onboardingPaths.PARAMETRIC_MODELING,
},
app: { ...TEST_SETTINGS.app, onboardingStatus: '/export' },
} satisfies Partial<SaveSettingsPayload>
export const TEST_SETTINGS_ONBOARDING_START = {

View File

@ -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.0678),
kcRound(-y * 0.0678), // Y is inverted in our coordinate system
kcRound(x * 0.0854),
kcRound(-y * 0.0854), // Y is inverted in our coordinate system
]
// Turn the array into a string with specific formatting
@ -226,7 +226,6 @@ 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,

View File

@ -2,7 +2,6 @@ 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')
@ -16,8 +15,25 @@ const newProjectDir = path.join(documentsDir, 'a-different-directory')
const tmp = process.env.TEMP || '/tmp'
const userCodeDir = path.join(tmp, 'kittycad_user_code')
describe('ZMA sign in flow', () => {
before(async () => {
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 () => {
// Clean up filesystem from previous tests
await new Promise((resolve) => setTimeout(resolve, 100))
await fs.rm(defaultProjectDir, { force: true, recursive: true })
@ -26,9 +42,7 @@ describe('ZMA sign in flow', () => {
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')
@ -68,10 +82,6 @@ describe('ZMA sign in flow', () => {
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"]')
@ -140,9 +150,7 @@ describe('ZMA authorized user flows', () => {
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"]')

View File

@ -1,18 +0,0 @@
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
View File

@ -1,62 +0,0 @@
{
"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
}

View File

@ -1,70 +0,0 @@
{
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";
};
});
};
}

View File

@ -1,6 +1,6 @@
{
"name": "untitled-app",
"version": "0.22.3",
"version": "0.22.1",
"private": true,
"dependencies": {
"@codemirror/autocomplete": "^6.16.0",
@ -10,7 +10,7 @@
"@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.19",
"@headlessui/tailwindcss": "^0.2.0",
"@kittycad/lib": "^0.0.67",
"@kittycad/lib": "^0.0.64",
"@lezer/javascript": "^1.4.9",
"@open-rpc/client-js": "^1.8.1",
"@react-hook/resize-observer": "^2.0.1",

1441
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -16,11 +16,11 @@ tauri-build = { version = "2.0.0-beta.13", features = [] }
[dependencies]
anyhow = "1"
kcl-lib = { version = "0.1.53", path = "../src/wasm-lib/kcl" }
kittycad = "0.3.5"
kittycad = "0.3.0"
log = "0.4.21"
oauth2 = "4.4.2"
serde_json = "1.0"
tauri = { version = "2.0.0-beta.22", features = [ "devtools", "unstable"] }
tauri = { version = "2.0.0-beta.15", features = [ "devtools", "unstable"] }
tauri-plugin-cli = { version = "2.0.0-beta.3" }
tauri-plugin-deep-link = { version = "2.0.0-beta.3" }
tauri-plugin-dialog = { version = "2.0.0-beta.6" }

View File

@ -63,17 +63,16 @@
"subcommands": {}
},
"deep-link": {
"mobile": [],
"desktop": {
"schemes": [
"app.zoo.dev"
]
}
"domains": [
{
"host": "app.zoo.dev"
}
]
},
"shell": {
"open": true
}
},
"productName": "Zoo Modeling App",
"version": "0.22.3"
"version": "0.22.1"
}

View File

@ -127,7 +127,7 @@ export function App() {
/>
<ModalContainer />
<ModelingSidebar paneOpacity={paneOpacity} />
<Stream />
<Stream className="absolute inset-0 z-0" />
{/* <CamToggle /> */}
<LowerRightControls>
<Gizmo />

View File

@ -174,6 +174,41 @@ 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,
@ -499,28 +534,26 @@ export class CameraControls {
direction.normalize()
this.camera.position.copy(this.target).addScaledVector(direction, distance)
}
usePerspectiveCamera = async () => {
usePerspectiveCamera = () => {
this._usePerspectiveCamera()
if (this.syncDirection === 'clientToEngine') {
await this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_set_perspective',
parameters: {
fov_y:
this.camera instanceof PerspectiveCamera ? this.camera.fov : 45,
...calculateNearFarFromFOV(this.lastPerspectiveFov),
},
this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_set_perspective',
parameters: {
fov_y:
this.camera instanceof PerspectiveCamera ? this.camera.fov : 45,
...calculateNearFarFromFOV(this.lastPerspectiveFov),
},
})
}
},
})
this.onCameraChange()
this.update()
return this.camera
}
dollyZoom = async (newFov: number, splitEngineCalls = false) => {
dollyZoom = (newFov: number) => {
if (!(this.camera instanceof PerspectiveCamera)) {
console.warn('Dolly zoom is only applicable to perspective cameras.')
return
@ -571,52 +604,13 @@ export class CameraControls {
this.camera.near = z_near
this.camera.far = z_far
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,
},
})
}
this.throttledUpdateEngineFov({
fov: newFov,
position: newPosition,
quaternion: this.camera.quaternion,
zoom: this.camera.zoom,
target: this.target,
})
}
update = (forceUpdate = false) => {
@ -1021,29 +1015,6 @@ 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 {
@ -1116,7 +1087,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.01, z_far: 1000 }
return { z_near: 0.1, z_far: 1000 }
}
function convertThreeCamValuesToEngineCam({
@ -1135,6 +1106,11 @@ 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 {
@ -1143,10 +1119,6 @@ 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()

View File

@ -136,7 +136,6 @@ 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' : ''} ${

View File

@ -32,7 +32,9 @@ import {
SKETCH_GROUP_SEGMENTS,
SKETCH_LAYER,
X_AXIS,
XZ_PLANE,
Y_AXIS,
YZ_PLANE,
} from './sceneInfra'
import { isQuaternionVertical, quaternionFromUpNForward } from './helpers'
import {
@ -1327,6 +1329,13 @@ 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)
}
@ -1390,135 +1399,114 @@ export class SceneEntities {
selected.material.color = defaultPlaneColor(type)
},
onClick: async (args) => {
const { streamDimensions } = useStore.getState()
const { entity_id, ...rest } = await sendSelectEventToEngine(
args?.mouseEvent,
document.getElementById('video-stream') as HTMLVideoElement,
streamDimensions
)
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?.negXy === _entity_id ||
engineCommandManager.defaultPlanes?.negXz === _entity_id ||
engineCommandManager.defaultPlanes?.negYz === _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 checkExtrudeFaceClick = async (): Promise<
['face' | 'plane' | 'other', string]
> => {
const { streamDimensions } = useStore.getState()
const { entity_id } = await sendSelectEventToEngine(
args?.mouseEvent,
document.getElementById('video-stream') as HTMLVideoElement,
streamDimensions
)
if (!entity_id) return ['other', '']
if (
engineCommandManager.defaultPlanes?.xy === entity_id ||
engineCommandManager.defaultPlanes?.xz === entity_id ||
engineCommandManager.defaultPlanes?.yz === entity_id
) {
return ['plane', 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]
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
: artifact.parentId
// get unit vector from camera position to target
const camVector = sceneInfra.camControls.camera.position
.clone()
.sub(sceneInfra.camControls.target)
// tsc cannot infer that target can have extrusions
// from the commandType (why?) so we need to cast it
const target = this.engineCommandManager.artifactMap?.[
targetId || ''
] as ArtifactMapCommand & { extrusions?: string[] }
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 || ''
}
}
// TODO: We get the first extrusion command ID,
// which is fine while backend systems only support one extrusion.
// but we need to more robustly handle resolving to the correct extrusion
// if there are multiple.
const extrusions =
this.engineCommandManager.artifactMap?.[
target?.extrusions?.[0] || ''
]
if (artifact?.commandType !== 'solid3d_get_extrusion_face_info')
return ['other', entity_id]
const faceInfo = await getFaceDetails(entity_id)
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
return ['other', entity_id]
const { z_axis, y_axis, origin } = faceInfo
const sketchPathToNode = getNodePathFromSourceRange(
kclManager.ast,
artifact.range
)
const extrudePathToNode = extrusions?.range
? getNodePathFromSourceRange(kclManager.ast, extrusions.range)
: []
sceneInfra.modelingSend({
type: 'Select default plane',
data: {
type: 'defaultPlane',
planeId: _entity_id,
plane: defaultPlaneStrMap[_entity_id],
zAxis,
yAxis,
type: 'extrudeFace',
zAxis: [z_axis.x, z_axis.y, z_axis.z],
yAxis: [y_axis.x, y_axis.y, y_axis.z],
position: [origin.x, origin.y, origin.z].map(
(num) => num / sceneInfra._baseUnitMultiplier
) as [number, number, number],
sketchPathToNode,
extrudePathToNode,
cap:
artifact?.additionalData?.type === 'cap'
? artifact.additionalData.info
: 'none',
faceId: entity_id,
},
})
return
return ['face', entity_id]
}
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
: artifact.parentId
// tsc cannot infer that target can have extrusions
// from the commandType (why?) so we need to cast it
const target = this.engineCommandManager.artifactMap?.[
targetId || ''
] as ArtifactMapCommand & { extrusions?: string[] }
// TODO: We get the first extrusion command ID,
// which is fine while backend systems only support one extrusion.
// but we need to more robustly handle resolving to the correct extrusion
// if there are multiple.
const extrusions =
this.engineCommandManager.artifactMap?.[target?.extrusions?.[0] || '']
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
const { z_axis, y_axis, origin } = faceInfo
const sketchPathToNode = getNodePathFromSourceRange(
kclManager.ast,
artifact.range
)
const extrudePathToNode = extrusions?.range
? getNodePathFromSourceRange(kclManager.ast, extrusions.range)
: []
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: 'extrudeFace',
zAxis: [z_axis.x, z_axis.y, z_axis.z],
yAxis: [y_axis.x, y_axis.y, y_axis.z],
position: [origin.x, origin.y, origin.z].map(
(num) => num / sceneInfra._baseUnitMultiplier
) as [number, number, number],
sketchPathToNode,
extrudePathToNode,
cap:
artifact?.additionalData?.type === 'cap'
? artifact.additionalData.info
: 'none',
faceId: _entity_id,
type: 'defaultPlane',
plane: planeString,
zAxis,
yAxis,
planeId: faceResult[1],
},
})
return
},
})
}

View File

@ -6,8 +6,6 @@ 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()
@ -105,7 +103,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 rounded-tl-none 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 shadow-lg bg-chalkboard-10 dark:bg-chalkboard-100 dark:border-chalkboard-70"
as="div"
data-testid="command-bar"
>
@ -118,19 +116,6 @@ 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>

View File

@ -7,8 +7,10 @@ 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>) =>
@ -39,6 +41,12 @@ function CommandBarSelectionInput({
canSubmitSelectionArg(selectionsByType, arg)
)
useHotkeys('tab', () => onSubmit(selection), {
enableOnFormTags: true,
enableOnContentEditable: true,
keyup: true,
})
useEffect(() => {
inputRef.current?.focus()
}, [selection, inputRef])

View File

@ -71,16 +71,6 @@ const CustomIconMap = {
/>
</svg>
),
bug: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
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"
/>
</svg>
),
checkmark: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path

View File

@ -33,7 +33,7 @@ export function LowerRightControls(props: React.PropsWithChildren) {
rel="noopener noreferrer"
>
<CustomIcon
name="bug"
name="exclamationMark"
className={`w-5 h-5 ${linkOverrideClassName}`}
/>
<Tooltip position="top">Report a bug</Tooltip>

View File

@ -47,6 +47,7 @@ import {
TANGENTIAL_ARC_TO_SEGMENT,
getParentGroup,
getSketchOrientationDetails,
getSketchQuaternion,
} from 'clientSideScene/sceneEntities'
import {
moveValueIntoNewVariablePath,
@ -63,7 +64,6 @@ import {
import {
getNodeFromPath,
getNodePathFromSourceRange,
hasExtrudableGeometry,
isSingleCursorInPipe,
} from 'lang/queryAst'
import { TEST } from 'env'
@ -76,7 +76,6 @@ 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>
@ -122,24 +121,7 @@ export const ModelingMachineProvider = ({
htmlRef,
token
)
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,
},
}
)
})
useHotkeyWrapper(['meta + shift + .'], () => coreDump(coreDumpManager, true))
// Settings machine setup
// const retrievedSettings = useRef(
@ -159,41 +141,7 @@ 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)
})()
kclManager.executeCode(true)
},
'Set mouse state': assign({
mouseState: (_, event) => event.data,
@ -448,13 +396,8 @@ export const ModelingMachineProvider = ({
if (
selectionRanges.codeBasedSelections.length === 0 ||
isSelectionLastLine(selectionRanges, codeManager.code)
) {
// 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
}
)
return true
if (!isPipe) return false
return canExtrudeSelection(selectionRanges)
@ -521,7 +464,7 @@ export const ModelingMachineProvider = ({
engineCommandManager,
data.faceId
)
sceneInfra.camControls.syncDirection = 'clientToEngine'
return {
sketchPathToNode: pathToNewSketchNode,
zAxis: data.zAxis,
@ -535,10 +478,8 @@ export const ModelingMachineProvider = ({
)
await kclManager.updateAst(modifiedAst, false)
sceneInfra.camControls.syncDirection = 'clientToEngine'
await letEngineAnimateAndSyncCamAfter(
engineCommandManager,
data.planeId
)
const quat = await getSketchQuaternion(pathToNode, data.zAxis)
await sceneInfra.camControls.tweenCameraToQuaternion(quat)
return {
sketchPathToNode: pathToNode,
zAxis: data.zAxis,

View File

@ -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 focus-within:bg-chalkboard-10/90 backdrop-blur-sm border border-chalkboard-20;
@apply bg-chalkboard-10/50 backdrop-blur-sm border border-chalkboard-20;
scroll-margin-block-start: 41px;
}
@ -12,7 +12,7 @@
}
:global(.dark) .panel {
@apply bg-chalkboard-100/50 focus-within:bg-chalkboard-100/90 backdrop-blur-[3px] border-chalkboard-80;
@apply bg-chalkboard-100/50 backdrop-blur-[3px] border-chalkboard-80;
}
.header {

View File

@ -46,11 +46,7 @@ export const ModelingPane = ({
data-testid={detailsTestId}
id={id}
className={
'group-focus-within:border-primary dark:group-focus-within:border-chalkboard-50 ' +
pointerEventsCssClass +
styles.panel +
' group ' +
(className || '')
pointerEventsCssClass + styles.panel + ' group ' + (className || '')
}
>
<ModelingPaneHeader title={title} Menu={Menu} />

View File

@ -123,73 +123,70 @@ function ModelingSidebarSection({
}, [showDebugPanel.current, togglePane, openPanes])
return (
<div className="group contents">
<Tab.Group
vertical
selectedIndex={
currentPane === 'none' ? 0 : paneIds.indexOf(currentPane) + 1
<Tab.Group
vertical
selectedIndex={
currentPane === 'none' ? 0 : paneIds.indexOf(currentPane) + 1
}
onChange={(index) => {
const newPane = index === 0 ? 'none' : paneIds[index - 1]
togglePane(newPane)
}}
>
<Tab.List
className={
'pointer-events-auto ' +
(alignButtons === 'start'
? 'justify-start self-start'
: 'justify-end self-end') +
(currentPane === 'none'
? ' rounded-r focus-within:!border-primary/50'
: ' border-r-0') +
' p-2 col-start-1 col-span-1 h-fit w-fit flex flex-col items-start gap-2 bg-chalkboard-10 border border-solid border-chalkboard-20 dark:bg-chalkboard-90 dark:border-chalkboard-80 ' +
(openPanes.length === 1 && currentPane === 'none' ? 'pr-0.5' : '')
}
onChange={(index) => {
const newPane = index === 0 ? 'none' : paneIds[index - 1]
togglePane(newPane)
}}
>
<Tab.List
className={
'pointer-events-auto ' +
(alignButtons === 'start'
? 'justify-start self-start'
: 'justify-end self-end') +
(currentPane === 'none'
? ' rounded-r focus-within:!border-primary/50'
: ' border-r-0') +
' p-2 col-start-1 col-span-1 h-fit w-fit flex flex-col items-start gap-2 ' +
'bg-chalkboard-10 border border-solid border-chalkboard-20 dark:bg-chalkboard-90 dark:border-chalkboard-80 group-focus-within:border-primary dark:group-focus-within:border-chalkboard-50 ' +
(openPanes.length === 1 && currentPane === 'none' ? 'pr-0.5' : '')
}
>
<Tab key="none" className="sr-only">
No panes open
</Tab>
{filteredPanes.map((pane) => (
<ModelingPaneButton
key={pane.id}
paneConfig={pane}
currentPane={currentPane}
togglePane={() => togglePane(pane.id)}
/>
))}
</Tab.List>
<Tab.Panels
as="article"
className={
'col-start-2 col-span-1 ' +
(openPanes.length === 1
? currentPane !== 'none'
? `row-start-1 row-end-3`
: `hidden`
: ``)
}
>
<Tab.Panel key="none" />
{filteredPanes.map((pane) => (
<Tab.Panel key={pane.id} className="h-full">
<ModelingPane
id={`${pane.id}-pane`}
title={pane.title}
Menu={pane.Menu}
>
{pane.Content instanceof Function ? (
<pane.Content />
) : (
pane.Content
)}
</ModelingPane>
</Tab.Panel>
))}
</Tab.Panels>
</Tab.Group>
</div>
<Tab key="none" className="sr-only">
No panes open
</Tab>
{filteredPanes.map((pane) => (
<ModelingPaneButton
key={pane.id}
paneConfig={pane}
currentPane={currentPane}
togglePane={() => togglePane(pane.id)}
/>
))}
</Tab.List>
<Tab.Panels
as="article"
className={
'col-start-2 col-span-1 ' +
(openPanes.length === 1
? currentPane !== 'none'
? `row-start-1 row-end-3`
: `hidden`
: ``)
}
>
<Tab.Panel key="none" />
{filteredPanes.map((pane) => (
<Tab.Panel key={pane.id} className="h-full">
<ModelingPane
id={`${pane.id}-pane`}
title={pane.title}
Menu={pane.Menu}
>
{pane.Content instanceof Function ? (
<pane.Content />
) : (
pane.Content
)}
</ModelingPane>
</Tab.Panel>
))}
</Tab.Panels>
</Tab.Group>
)
}

View File

@ -1,7 +1,8 @@
import { render, screen } from '@testing-library/react'
import { fireEvent, 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'

View File

@ -24,9 +24,9 @@ export function RefreshButton() {
return (
<button
onClick={refresh}
className="p-1 m-0 bg-chalkboard-10/80 dark:bg-chalkboard-100/50 hover:bg-chalkboard-10 dark:hover:bg-chalkboard-100 rounded-full border border-solid border-chalkboard-20 dark:border-chalkboard-90"
className="p-1 m-0 bg-chalkboard-10/80 dark:bg-chalkboard-100/50 hover:bg-chalkboard-10 dark:hover:bg-chalkboard-100 rounded-full border border-solid border-chalkboard-10 dark:border-chalkboard-100"
>
<CustomIcon name="exclamationMark" className="w-5 h-5" />
<CustomIcon name="arrowRotateRight" className="w-5 h-5" />
<Tooltip position="bottom-right">
<span>Refresh and report</span>
<br />

View File

@ -171,9 +171,7 @@ export const SettingsAuthProviderBase = ({
})
},
'Execute AST': () => kclManager.executeCode(true, true),
},
services: {
'Persist settings': (context) =>
persistSettings: (context) =>
saveSettings(context, loadedProject?.project?.path),
},
}

View File

@ -126,8 +126,8 @@ export const Stream = ({ className = '' }: { className?: string }) => {
return (
<div
className="absolute inset-0 z-0"
data-testid="stream"
id="stream"
className={className}
onMouseUp={handleMouseUp}
onMouseDown={handleMouseDown}
onContextMenu={(e) => e.preventDefault()}
@ -142,6 +142,7 @@ 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

View File

@ -11,8 +11,30 @@
--_p-inline: calc(50% + calc(var(--isRTL) * var(--_triangle-width) / 2));
--_p-block: 4px;
--_bg: var(--chalkboard-10);
--_shadow-alpha: 8%;
--_shadow-alpha: 5%;
--_theme-alpha: 0.15;
--_theme-outline: drop-shadow(
0 1px 0
oklch(
var(--primary-lightness) var(--primary-chroma) var(--primary-hue) /
var(--_theme-alpha)
)
)
drop-shadow(
0 -1px 0 oklch(var(--primary-lightness) var(--primary-chroma)
var(--primary-hue) / var(--_theme-alpha))
)
drop-shadow(
1px 0 0
oklch(
var(--primary-lightness) var(--primary-chroma) var(--primary-hue) /
var(--_theme-alpha)
)
)
drop-shadow(
-1px 0 0 oklch(var(--primary-lightness) var(--primary-chroma)
var(--primary-hue) / var(--_theme-alpha))
);
pointer-events: none;
user-select: none;
@ -39,15 +61,16 @@
background: var(--_bg);
@apply text-chalkboard-110;
will-change: filter;
filter: drop-shadow(0 1px 2px hsl(0 0% 0% / var(--_shadow-alpha)))
drop-shadow(0 4px 6px hsl(0 0% 0% / calc(var(--_shadow-alpha) / 2)));
filter: drop-shadow(0 1px 3px hsl(0 0% 0% / var(--_shadow-alpha)))
drop-shadow(0 4px 8px hsl(0 0% 0% / var(--_shadow-alpha)))
var(--_theme-outline);
}
:global(.dark) .tooltip {
--_bg: var(--chalkboard-90);
--_bg: var(--chalkboard-110);
--_theme-alpha: 40%;
--_shadow-alpha: 16%;
@apply text-chalkboard-10;
filter: var(--_theme-outline);
}
.tooltip:dir(rtl) {
@ -86,7 +109,7 @@
}
/* Sometimes there's no visible label,
* so we'll use the tooltip as the label
* so we'll use the tooltip as the label
*/
.tooltip:only-child::before {
content: 'Tooltip:';

View File

@ -39,7 +39,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
<Popover className="relative">
{user?.image && !imageLoadFailed ? (
<Popover.Button
className="relative border-0 rounded-full w-fit min-w-max p-0 group"
className="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,9 +51,6 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
onError={() => setImageLoadFailed(true)}
/>
</div>
<Tooltip position="bottom-right" delay={1000}>
User menu
</Tooltip>
</Popover.Button>
) : (
<ActionButton
@ -62,7 +59,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
className="border-transparent !px-0"
data-testid="user-sidebar-toggle"
>
<Tooltip position="bottom-right" delay={1000}>
<Tooltip position="left" delay={1000}>
User menu
</Tooltip>
</ActionButton>

View File

@ -7,11 +7,7 @@ import { Selections, processCodeMirrorRanges, Selection } from 'lib/selections'
import { undo, redo } from '@codemirror/commands'
import { CommandBarMachineEvent } from 'machines/commandBarMachine'
import { addLineHighlight } from './highlightextension'
import { forEachDiagnostic, setDiagnostics, Diagnostic } from '@codemirror/lint'
function diagnosticIsEqual(d1: Diagnostic, d2: Diagnostic): boolean {
return d1.from === d2.from && d1.to === d2.to && d1.message === d2.message
}
import { setDiagnostics, Diagnostic } from '@codemirror/lint'
export default class EditorManager {
private _editorView: EditorView | null = null
@ -95,38 +91,11 @@ export default class EditorManager {
}
}
clearDiagnostics(): void {
if (!this.editorView) return
this.editorView.dispatch(setDiagnostics(this.editorView.state, []))
}
setDiagnostics(diagnostics: Diagnostic[]): void {
if (!this.editorView) return
this.editorView.dispatch(setDiagnostics(this.editorView.state, diagnostics))
}
addDiagnostics(diagnostics: Diagnostic[]): void {
if (!this.editorView) return
forEachDiagnostic(this.editorView.state, function (diag) {
diagnostics.push(diag)
})
const uniqueDiagnostics = new Set<Diagnostic>()
diagnostics.forEach((diagnostic) => {
for (const knownDiagnostic of uniqueDiagnostics.values()) {
if (diagnosticIsEqual(diagnostic, knownDiagnostic)) {
return
}
}
uniqueDiagnostics.add(diagnostic)
})
this.editorView.dispatch(
setDiagnostics(this.editorView.state, [...uniqueDiagnostics])
)
}
undo() {
if (this._editorView) {
undo(this._editorView)

View File

@ -382,14 +382,9 @@ export class LanguageServerPlugin implements PluginValue {
try {
switch (notification.method) {
case 'textDocument/publishDiagnostics':
console.log(
'[lsp] [window/publishDiagnostics]',
this.client.getName(),
notification.params
)
const params = notification.params as PublishDiagnosticsParams
//const params = notification.params as PublishDiagnosticsParams
// this is sometimes slower than our actual typing.
this.processDiagnostics(params)
//this.processDiagnostics(params)
break
case 'window/logMessage':
console.log(

View File

@ -147,31 +147,13 @@ code {
#code-mirror-override .cm-activeLine,
#code-mirror-override .cm-activeLineGutter {
@apply bg-primary/5;
@apply bg-primary/10;
}
.dark #code-mirror-override .cm-activeLine,
.dark #code-mirror-override .cm-activeLineGutter {
@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;
mix-blend-mode: lighten;
}
#code-mirror-override .cm-gutters {
@ -189,8 +171,22 @@ code {
@apply caret-chalkboard-10;
}
#code-mirror-override .cm-focused {
outline: none;
#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;
}
@keyframes blink {
@ -253,10 +249,3 @@ 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;
}
}

View File

@ -41,10 +41,7 @@ export class KclManager {
engineCommandManager: EngineCommandManager
private _defferer = deferExecution((code: string) => {
const ast = this.safeParse(code)
if (!ast) {
this.clearAst()
return
}
if (!ast) return
try {
const fmtAndStringify = (ast: Program) =>
JSON.stringify(parse(recast(ast)))
@ -94,7 +91,7 @@ export class KclManager {
set kclErrors(kclErrors) {
this._kclErrors = kclErrors
let diagnostics = kclErrorsToDiagnostics(kclErrors)
editorManager.addDiagnostics(diagnostics)
editorManager.setDiagnostics(diagnostics)
this._kclErrorsCallBack(kclErrors)
}
@ -148,18 +145,6 @@ 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)
@ -200,11 +185,6 @@ export class KclManager {
const currentExecutionId = executionId || Date.now()
this._cancelTokens.set(currentExecutionId, false)
// here we're going to clear diagnostics since we're the first
// one in. We're the only location where diagnostics are cleared;
// everything from here on out should be *appending*.
editorManager.clearDiagnostics()
this.isExecuting = true
await this.ensureWasmInit()
const { logs, errors, programMemory } = await executeAst({
@ -254,7 +234,6 @@ export class KclManager {
} = { updates: 'none' }
) {
await this.ensureWasmInit()
const newCode = recast(ast)
const newAst = this.safeParse(newCode)
if (!newAst) return
@ -264,11 +243,6 @@ export class KclManager {
await this?.engineCommandManager?.waitForReady
this._ast = { ...newAst }
// here we're going to clear diagnostics since we're the first
// one in. We're the only location where diagnostics are cleared;
// everything from here on out should be *appending*.
editorManager.clearDiagnostics()
const { logs, errors, programMemory } = await executeAst({
ast: newAst,
engineCommandManager: this.engineCommandManager,
@ -307,20 +281,14 @@ export class KclManager {
if (!force) return this._defferer(codeManager.code)
const ast = this.safeParse(codeManager.code)
if (!ast) {
this.clearAst()
return
}
if (!ast) return
this.ast = { ...ast }
return this.executeAst(ast, zoomToFit)
}
format() {
const originalCode = codeManager.code
const ast = this.safeParse(originalCode)
if (!ast) {
this.clearAst()
return
}
if (!ast) return
const code = recast(ast)
if (originalCode === code) return
@ -384,55 +352,18 @@ export class KclManager {
return this?.engineCommandManager?.defaultPlanes
}
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)
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)
}
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)
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)
}
defaultSelectionFilter() {
defaultSelectionFilter(this.programMemory, this.engineCommandManager)

View File

@ -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,9 +68,7 @@ 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
@ -78,9 +76,7 @@ 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) {
@ -94,9 +90,7 @@ 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

View File

@ -7,8 +7,6 @@ import {
doesPipeHaveCallExp,
hasExtrudeSketchGroup,
findUsesOfTagInPipe,
hasSketchPipeBeenExtruded,
hasExtrudableGeometry,
} from './queryAst'
import { enginelessExecutor } from '../lib/testHelpers'
import {
@ -398,90 +396,3 @@ 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()
})
})

View File

@ -720,78 +720,3 @@ 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
}

View File

@ -58,9 +58,6 @@ 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'
@ -1319,8 +1316,7 @@ export class EngineCommandManager extends EventTarget {
)
if (
message.success &&
(message.resp.type === 'modeling' ||
message.resp.type === 'modeling_batch') &&
message.resp.type === 'modeling' &&
message.request_id
) {
this.handleModelingCommand(
@ -1384,60 +1380,19 @@ export class EngineCommandManager extends EventTarget {
id: string,
raw: WebSocketResponse
) {
if (!(message.type === 'modeling' || message.type === 'modeling_batch')) {
if (message.type !== 'modeling') {
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)
})
}
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
@ -1446,6 +1401,7 @@ export class EngineCommandManager extends EventTarget {
id,
commandType: command.commandType,
range: command.range,
data: modelingResponse,
raw,
})
return
@ -1777,7 +1733,7 @@ export class EngineCommandManager extends EventTarget {
command: EngineCommand
ast: Program
idToRangeMap?: { [key: string]: SourceRange }
}): Promise<ResolveCommand | void> {
}): Promise<any> {
if (this.engineConnection === undefined) {
return Promise.resolve()
}
@ -1846,13 +1802,11 @@ export class EngineCommandManager extends EventTarget {
command: Models['ModelingCmd_type'],
ast?: Program,
range?: SourceRange
): Promise<ResolveCommand | void> {
) {
let resolve: (val: any) => void = () => {}
const promise: Promise<ResolveCommand | void> = new Promise(
(_resolve, reject) => {
resolve = _resolve
}
)
const promise = 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') {
@ -1913,13 +1867,11 @@ export class EngineCommandManager extends EventTarget {
idToRangeMap?: { [key: string]: SourceRange },
ast?: Program,
range?: SourceRange
): Promise<ResolveCommand | void> {
) {
let resolve: (val: any) => void = () => {}
const promise: Promise<ResolveCommand | void> = new Promise(
(_resolve, reject) => {
resolve = _resolve
}
)
const promise = new Promise((_resolve, reject) => {
resolve = _resolve
})
if (!idToRangeMap) {
throw new Error('idToRangeMap is required for batch commands')
@ -1939,7 +1891,7 @@ export class EngineCommandManager extends EventTarget {
resolve,
}
Promise.all(
await Promise.all(
commands.map((c) =>
this.handlePendingCommand(c.cmd_id, c.cmd, ast, idToRangeMap[c.cmd_id])
)
@ -1951,7 +1903,7 @@ export class EngineCommandManager extends EventTarget {
rangeStr: string,
commandStr: string,
idToRangeStr: string
): Promise<string | void> {
): Promise<any> {
if (this.engineConnection === undefined) {
return Promise.resolve()
}
@ -1980,13 +1932,13 @@ export class EngineCommandManager extends EventTarget {
command,
ast: this.getAst(),
idToRangeMap,
}).then((resp) => {
if (!resp) {
}).then(({ raw }: { raw: WebSocketResponse | undefined | null }) => {
if (raw === undefined || raw === null) {
throw new Error(
'returning modeling cmd response to the rust side is undefined or null'
)
}
return JSON.stringify(resp.raw)
return JSON.stringify(raw)
})
}
commandResult(id: string): Promise<any> {

View File

@ -338,24 +338,11 @@ export async function coreDump(
): Promise<CoreDumpInfo> {
try {
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}`)
}
}

View File

@ -13,15 +13,8 @@ import screenshot from 'lib/screenshot'
import React from 'react'
import { VITE_KC_API_BASE_URL } from 'env'
/**
* 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
// This is a class for getting all the values from the JS world to pass to the Rust world
// for a core dump.
export class CoreDumpManager {
engineCommandManager: EngineCommandManager
htmlRef: React.RefObject<HTMLDivElement> | null
@ -155,8 +148,8 @@ export class CoreDumpManager {
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
* - NOTE: this function thows on parse errors from things like circular references
* - It is also syncronous 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.
@ -170,14 +163,6 @@ export class CoreDumpManager {
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
@ -205,18 +190,18 @@ export class CoreDumpManager {
modeling_machine: {},
settings_machine: {},
}
debugLog('CoreDump: initialized clientState', clientState)
debugLog('CoreDump: globalThis.window', globalThis.window)
console.log('CoreDump: initialized clientState', clientState)
console.info('CoreDump: globalThis.window', globalThis.window)
try {
// Singletons
// engine_command_manager
debugLog('CoreDump: engineCommandManager', this.engineCommandManager)
console.log('CoreDump: engineCommandManager', this.engineCommandManager)
// artifact map - this.engineCommandManager.artifactMap
if (this.engineCommandManager?.artifactMap) {
debugLog(
console.log(
'CoreDump: Engine Command Manager artifact map',
this.engineCommandManager.artifactMap
)
@ -227,7 +212,7 @@ export class CoreDumpManager {
// command logs - this.engineCommandManager.commandLogs
if (this.engineCommandManager?.commandLogs) {
debugLog(
console.log(
'CoreDump: Engine Command Manager command logs',
this.engineCommandManager.commandLogs
)
@ -238,7 +223,7 @@ export class CoreDumpManager {
// default planes - this.engineCommandManager.defaultPlanes
if (this.engineCommandManager?.defaultPlanes) {
debugLog(
console.log(
'CoreDump: Engine Command Manager default planes',
this.engineCommandManager.defaultPlanes
)
@ -249,37 +234,37 @@ export class CoreDumpManager {
// engine connection state
if (this.engineCommandManager?.engineConnection?.state) {
debugLog(
clientState.engine_command_manager.engine_connection.state =
this.engineCommandManager.engineConnection.state
console.log(
'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(
console.log(
'CoreDump: Engine Command Manager in sequence',
this.engineCommandManager.inSequence
)
;(clientState.engine_command_manager as any).in_sequence =
clientState.engine_command_manager.in_sequence =
this.engineCommandManager.inSequence
}
// out sequence - this.engineCommandManager.outSequence
if (this.engineCommandManager?.outSequence) {
debugLog(
if (this.engineCommandManager?.inSequence) {
console.log(
'CoreDump: Engine Command Manager out sequence',
this.engineCommandManager.outSequence
)
;(clientState.engine_command_manager as any).out_sequence =
this.engineCommandManager.outSequence
clientState.engine_command_manager.out_sequence =
this.engineCommandManager.inSequence
}
// scene command artifacts - this.engineCommandManager.sceneCommandArtifacts
if (this.engineCommandManager?.sceneCommandArtifacts) {
debugLog(
console.log(
'CoreDump: Engine Command Manager scene command artifacts',
this.engineCommandManager.sceneCommandArtifacts
)
@ -289,53 +274,52 @@ export class CoreDumpManager {
}
// KCL Manager - globalThis?.window?.kclManager
const kclManager = (globalThis?.window as any)?.kclManager
debugLog('CoreDump: kclManager', kclManager)
const kclManager = globalThis?.window?.kclManager
console.log('CoreDump: kclManager', kclManager)
if (kclManager) {
// KCL Manager AST
debugLog('CoreDump: KCL Manager AST', kclManager?.ast)
console.log('CoreDump: KCL Manager AST', kclManager?.ast)
if (kclManager?.ast) {
clientState.kcl_manager.ast = deepClone(kclManager.ast)
}
// KCL Errors
debugLog('CoreDump: KCL Errors', kclManager?.kclErrors)
console.log('CoreDump: KCL Errors', kclManager?.kclErrors)
if (kclManager?.kclErrors) {
clientState.kcl_manager.kcl_errors = deepClone(kclManager.kclErrors)
}
// KCL isExecuting
debugLog('CoreDump: KCL isExecuting', kclManager?.isExecuting)
console.log('CoreDump: KCL isExecuting', kclManager?.isExecuting)
if (kclManager?.isExecuting) {
;(clientState.kcl_manager as any).isExecuting = kclManager.isExecuting
clientState.kcl_manager.isExecuting = kclManager.isExecuting
}
// KCL logs
debugLog('CoreDump: KCL logs', kclManager?.logs)
console.log('CoreDump: KCL logs', kclManager?.logs)
if (kclManager?.logs) {
;(clientState.kcl_manager as any).logs = deepClone(kclManager.logs)
clientState.kcl_manager.logs = deepClone(kclManager.logs)
}
// KCL programMemory
debugLog('CoreDump: KCL programMemory', kclManager?.programMemory)
console.log('CoreDump: KCL programMemory', kclManager?.programMemory)
if (kclManager?.programMemory) {
;(clientState.kcl_manager as any).programMemory = deepClone(
clientState.kcl_manager.programMemory = deepClone(
kclManager.programMemory
)
}
// KCL wasmInitFailed
debugLog('CoreDump: KCL wasmInitFailed', kclManager?.wasmInitFailed)
console.log('CoreDump: KCL wasmInitFailed', kclManager?.wasmInitFailed)
if (kclManager?.wasmInitFailed) {
;(clientState.kcl_manager as any).wasmInitFailed =
kclManager.wasmInitFailed
clientState.kcl_manager.wasmInitFailed = kclManager.wasmInitFailed
}
}
// Scene Infra - globalThis?.window?.sceneInfra
const sceneInfra = (globalThis?.window as any)?.sceneInfra
debugLog('CoreDump: Scene Infra', sceneInfra)
const sceneInfra = globalThis?.window?.sceneInfra
console.log('CoreDump: Scene Infra', sceneInfra)
if (sceneInfra) {
const sceneInfraSkipKeys = ['camControls']
@ -348,11 +332,11 @@ export class CoreDumpManager {
)
})
debugLog('CoreDump: Scene Infra keys', sceneInfraKeys)
console.log('CoreDump: Scene Infra keys', sceneInfraKeys)
sceneInfraKeys.forEach((key: string) => {
debugLog('CoreDump: Scene Infra', key, sceneInfra[key])
console.log('CoreDump: Scene Infra', key, sceneInfra[key])
try {
;(clientState.scene_infra as any)[key] = sceneInfra[key]
clientState.scene_infra[key] = sceneInfra[key]
} catch (error) {
console.error(
'CoreDump: unable to parse Scene Infra ' + key + ' data due to ',
@ -363,25 +347,25 @@ export class CoreDumpManager {
}
// Scene Entities Manager - globalThis?.window?.sceneEntitiesManager
const sceneEntitiesManager = (globalThis?.window as any)
?.sceneEntitiesManager
debugLog('CoreDump: sceneEntitiesManager', sceneEntitiesManager)
const sceneEntitiesManager = globalThis?.window?.sceneEntitiesManager
console.log('CoreDump: sceneEntitiesManager', sceneEntitiesManager)
if (sceneEntitiesManager) {
// Scene Entities Manager active segments
debugLog(
console.log(
'CoreDump: Scene Entities Manager active segments',
sceneEntitiesManager?.activeSegments
)
if (sceneEntitiesManager?.activeSegments) {
;(clientState.scene_entities_manager as any).activeSegments =
deepClone(sceneEntitiesManager.activeSegments)
clientState.scene_entities_manager.activeSegments = deepClone(
sceneEntitiesManager.activeSegments
)
}
}
// Editor Manager - globalThis?.window?.editorManager
const editorManager = (globalThis?.window as any)?.editorManager
debugLog('CoreDump: editorManager', editorManager)
const editorManager = globalThis?.window?.editorManager
console.log('CoreDump: editorManager', editorManager)
if (editorManager) {
const editorManagerSkipKeys = ['camControls']
@ -395,13 +379,11 @@ export class CoreDumpManager {
)
})
debugLog('CoreDump: Editor Manager keys', editorManagerKeys)
console.log('CoreDump: Editor Manager keys', editorManagerKeys)
editorManagerKeys.forEach((key: string) => {
debugLog('CoreDump: Editor Manager', key, editorManager[key])
console.log('CoreDump: Editor Manager', key, editorManager[key])
try {
;(clientState.editor_manager as any)[key] = deepClone(
editorManager[key]
)
clientState.editor_manager[key] = deepClone(editorManager[key])
} catch (error) {
console.error(
'CoreDump: unable to parse Editor Manager ' +
@ -415,21 +397,21 @@ export class CoreDumpManager {
// enableMousePositionLogs - Not coredumped
// See https://github.com/KittyCAD/modeling-app/issues/2338#issuecomment-2136441998
debugLog(
console.log(
'CoreDump: enableMousePositionLogs [not coredumped]',
(globalThis?.window as any)?.enableMousePositionLogs
globalThis?.window?.enableMousePositionLogs
)
// XState Machines
debugLog(
console.log(
'CoreDump: xstate services',
(globalThis?.window as any)?.__xstate__?.services
globalThis?.window?.__xstate__?.services
)
debugLog('CoreDump: final clientState', clientState)
console.log('CoreDump: final clientState', clientState)
const clientStateJson = JSON.stringify(clientState)
debugLog('CoreDump: final clientState JSON', clientStateJson)
console.log('CoreDump: final clientState JSON', clientStateJson)
return Promise.resolve(clientStateJson)
} catch (error) {

View File

@ -17,17 +17,15 @@ const prependRoutes =
)
}
type OnboardingPaths = {
[K in keyof typeof onboardingPaths]: `/onboarding${(typeof onboardingPaths)[K]}`
}
export const paths = {
INDEX: '/',
HOME: '/home',
FILE: '/file',
SETTINGS: '/settings',
SIGN_IN: '/signin',
ONBOARDING: prependRoutes(onboardingPaths)('/onboarding') as OnboardingPaths,
ONBOARDING: prependRoutes(onboardingPaths)(
'/onboarding'
) as typeof onboardingPaths,
} as const
export const BROWSER_PATH = `%2F${BROWSER_PROJECT_NAME}%2F${BROWSER_FILE_NAME}${FILE_EXT}`

View File

@ -16,7 +16,6 @@ import { Program } from 'lang/wasm'
import {
doesPipeHaveCallExp,
getNodeFromPath,
hasSketchPipeBeenExtruded,
isSingleCursorInPipe,
} from 'lang/queryAst'
import { CommandArgument } from './commandTypes'
@ -388,7 +387,6 @@ 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))
)
@ -542,7 +540,7 @@ function codeToIdSelections(
.filter(Boolean) as any
}
export async function sendSelectEventToEngine(
export function sendSelectEventToEngine(
e: MouseEvent | React.MouseEvent<HTMLDivElement, MouseEvent>,
el: HTMLVideoElement,
streamDimensions: { streamWidth: number; streamHeight: number }
@ -553,7 +551,7 @@ export async function sendSelectEventToEngine(
el,
...streamDimensions,
})
const result: Models['SelectWithPoint_type'] = await engineCommandManager
const result: Promise<Models['SelectWithPoint_type']> = engineCommandManager
.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {

View File

@ -157,7 +157,7 @@ export function createSettings() {
),
}),
enableSSAO: new Setting<boolean>({
defaultValue: false,
defaultValue: true,
description:
'Whether or not Screen Space Ambient Occlusion (SSAO) is enabled',
validate: (v) => typeof v === 'boolean',

File diff suppressed because one or more lines are too long

View File

@ -11,98 +11,98 @@ import {
export const settingsMachine = createMachine(
{
/** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlnXwEMAHW-Ae2wCNHqAnCHKZNatAFdYAbQAMAXUShajWJizNpIAB6IALAFYAnPgBMARgDsBsQDY969QGYjmzQBoQAT0SnrADnwePY61r0PAwNtMyMAX3CnVAweAiJSCio6BjQACzAAWzAAYUZiRg5xKSQQWXlFbGU1BD1PfFtfE3UzTUNNaydXBCD1b209PTEPTTMtdQNNSOj0LFx4knJKNHxMxggwYh58DYAzakFiNABVbAVi5XKFTCVSmsGxfCMPM08PQaDNU0cXRG1tLwedTaKxif7+UJTKIgGJzPCERZJFYpfDpLJgC6lK6VaqIEx6fBmCw2Do2IJ6MxdRDvTT4MRDdRGEzWbQ6ELTGGzOIIxLLVbrTbbNKYKBpLaitAAUWgcExMjk11uoBqVgM3jMYhsAIMrVs6ipPWChOeYhC9KMFhGHNh3IS5AASnB0AACZZw0SSS4KnF3PFafADTV1YZ2IxiH7dNpGfCaIzAgE+IzWMzBa1c+Y88gxZ3UYjEV3pvBysrem5VX0IFq0y3aTXWOp6JmU34IKMxuz0joGEYWsxp2IZu1kABUxexZdxtRG+EmQMZmne3dNBs0jKewLBsbCwI81n77vwtDAHHksDhBYHeDIEGYYEI2AAbowANZ3o8nzBnm3zMelpWqRAAFp62sJ4jEsZ4AT0UJGwjPFzH6cwNW0AwWXpbRImhbABXgUpvzwL0KgnCtgJMMCII8KCYLsA11EGOkXneDxmXMCk92hfCFlIQjFXLZUgLjddWhaFkgRCaxOhbEYzBnXwXkmOjAjjfduXfU9zzdOIeJ9fiEEA6ckwMClQ2BFpmJXMF9DjYI6hZfxmMw8IgA */
/** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlnXwEMAHW-Ae2wCNHqAnCHKZNatAFdYAbQAMAXUShajWJizNpIAB6IAbAFZN+AOwAWAIwAOYwE4AzGYBM+-ZosAaEAE9Eh62LP51ls+v0LMWt1awMAX3DnVAweAiJSCio6BjQACzAAWzAAYUZiRg5xKSQQWXlFbGU1BA9vQ0N1CwCxdVbdY1DnNwQzPp8zTTFje1D1QwtjSOj0LFx4knJKNHxMxggwYh58DYAzakFiNABVbAVi5XKFTCVSmotNY3w7YysHRuNDTXV1bvdG7w-IKTcbaazWCzTEAxOZ4QiLJIrFL4dJZMAXUpXSrVDTGazPMQPR4GXRBAx-XoGfDWIadMx4n6EqEwuLwxLLVbrTbbNKYKBpLb8tAAUWgcAxMjk11uoHumn0+DEw2sJkMulCgWsFL6YnwfX0ELsYg61jMumZs1ZCXIACU4OgAATLWGiSSXKXYu4aLz4UwWBr6DqBYYUzSePXqUlBLxmyZmC2xeZs8gxB3UYjEJ2W+YSsoem5VL0IKy6z6EsJifQ2Czq0MTHzq8Yjfz6MQmBMu5NkABUuaxBZxCDD+DD5lJgUjxssFP0xl0I+0pm06uMmg8kSiIGwXPgpRZ83dFQHRYAtA0LCO2tZjGIHJNdB5fq5EK3dc1OsNGkrA+oO1bFoe0qFrKiAnlol7BDed5zo+FK+Doc7qhCNaVv4UwbkAA */
id: 'Settings',
predictableActionArguments: true,
context: {} as ReturnType<typeof createSettings>,
initial: 'idle',
states: {
idle: {
entry: ['setThemeClass', 'setClientSideSceneUnits'],
entry: ['setThemeClass', 'setClientSideSceneUnits', 'persistSettings'],
on: {
'*': {
target: 'persisting settings',
actions: ['setSettingAtLevel', 'toastSuccess'],
target: 'idle',
internal: true,
actions: ['setSettingAtLevel', 'toastSuccess', 'persistSettings'],
},
'set.app.onboardingStatus': {
target: 'persisting settings',
// No toast
actions: ['setSettingAtLevel'],
target: 'idle',
internal: true,
actions: ['setSettingAtLevel', 'persistSettings'], // No toast
},
'set.app.themeColor': {
target: 'persisting settings',
// No toast
actions: ['setSettingAtLevel'],
target: 'idle',
internal: true,
actions: ['setSettingAtLevel', 'persistSettings'], // No toast
},
'set.modeling.defaultUnit': {
target: 'persisting settings',
target: 'idle',
internal: true,
actions: [
'setSettingAtLevel',
'toastSuccess',
'setClientSideSceneUnits',
'Execute AST',
'persistSettings',
],
},
'set.app.theme': {
target: 'persisting settings',
target: 'idle',
internal: true,
actions: [
'setSettingAtLevel',
'toastSuccess',
'setThemeClass',
'setEngineTheme',
'persistSettings',
'setClientTheme',
],
},
'set.modeling.highlightEdges': {
target: 'persisting settings',
actions: ['setSettingAtLevel', 'toastSuccess', 'setEngineEdges'],
target: 'idle',
internal: true,
actions: [
'setSettingAtLevel',
'toastSuccess',
'setEngineEdges',
'persistSettings',
],
},
'Reset settings': {
target: 'persisting settings',
target: 'idle',
internal: true,
actions: [
'resetSettings',
'setThemeClass',
'setEngineTheme',
'setClientSideSceneUnits',
'Execute AST',
'persistSettings',
'setClientTheme',
],
},
'Set all settings': {
target: 'persisting settings',
target: 'idle',
internal: true,
actions: [
'setAllSettings',
'setThemeClass',
'setEngineTheme',
'setClientSideSceneUnits',
'Execute AST',
'persistSettings',
'setClientTheme',
],
},
},
},
'persisting settings': {
invoke: {
src: 'Persist settings',
id: 'persistSettings',
onDone: 'idle',
},
},
},
tsTypes: {} as import('./settingsMachine.typegen').Typegen0,
schema: {

View File

@ -1,9 +1,8 @@
import { OnboardingButtons, useDemoCode, useDismiss, useNextClick } from '.'
import { OnboardingButtons, useDismiss, useNextClick } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths'
import { useStore } from '../../useStore'
export default function OnboardingCodeEditor() {
useDemoCode()
export default function CodeEditor() {
const { buttonDownInStream } = useStore((s) => ({
buttonDownInStream: s.buttonDownInStream,
}))

View File

@ -1,19 +1,24 @@
import { OnboardingButtons, useDemoCode, useDismiss } from '.'
import { OnboardingButtons, 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 (

View File

@ -1,16 +1,9 @@
import {
OnboardingButtons,
kbdClasses,
useDemoCode,
useDismiss,
useNextClick,
} from '.'
import { OnboardingButtons, kbdClasses, useDismiss, useNextClick } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths'
import { useStore } from '../../useStore'
import { bracketWidthConstantLine } from 'lib/exampleKcl'
export default function OnboardingInteractiveNumbers() {
useDemoCode()
export default function InteractiveNumbers() {
const { buttonDownInStream } = useStore((s) => ({
buttonDownInStream: s.buttonDownInStream,
}))
@ -40,10 +33,8 @@ export default function OnboardingInteractiveNumbers() {
<kbd className={kbdClasses}>Option</kbd>) key
</li>
<li>
Hover over the number assigned to "width" on{' '}
<em>
<strong>line {bracketWidthConstantLine}</strong>
</em>
Hover over the number assigned to <code>width</code> on line{' '}
{bracketWidthConstantLine}
</li>
<li>Drag the number left and right to change its value</li>
</ol>

View File

@ -1,4 +1,4 @@
import { OnboardingButtons, useDemoCode, useDismiss, useNextClick } from '.'
import { OnboardingButtons, useDismiss, useNextClick } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { Themes, getSystemTheme } from 'lib/theme'
@ -10,6 +10,7 @@ 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 {
@ -91,7 +92,7 @@ function OnboardingWithNewFile() {
)
}
export default function OnboardingIntroduction() {
export default function Introduction() {
const {
settings: {
state: {
@ -111,7 +112,9 @@ export default function OnboardingIntroduction() {
const currentCode = codeManager.code
const isStarterCode = currentCode === '' || currentCode === bracket
useDemoCode()
useEffect(() => {
if (codeManager.code === '') codeManager.updateCodeEditor(bracket)
}, [])
return isStarterCode ? (
<div className="fixed inset-0 z-50 grid place-content-center bg-chalkboard-110/50">
@ -156,12 +159,6 @@ export default function OnboardingIntroduction() {
! 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}

View File

@ -1,12 +1,11 @@
import { OnboardingButtons, useDemoCode, useDismiss, useNextClick } from '.'
import { OnboardingButtons, 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 OnboardingParametricModeling() {
useDemoCode()
export default function ParametricModeling() {
const { buttonDownInStream } = useStore((s) => ({
buttonDownInStream: s.buttonDownInStream,
}))
@ -45,10 +44,8 @@ export default function OnboardingParametricModeling() {
<p className="my-4">
We've received this sketch from a designer highlighting an{' '}
<em>
<strong>aluminum bracket</strong>
</em>{' '}
they need for this shelf:
<em className="text-primary">aluminum bracket</em> they need for
this shelf:
</p>
<figure className="my-4 w-2/3 mx-auto">
<img
@ -62,8 +59,8 @@ export default function OnboardingParametricModeling() {
<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>
<strong>line {bracketThicknessCalculationLine}</strong>
<em className="text-primary">
line {bracketThicknessCalculationLine}
</em>
.
</p>

View File

@ -1,7 +1,6 @@
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) => ({
@ -9,20 +8,6 @@ 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">
@ -35,8 +20,8 @@ export default function UserMenu() {
<section className="flex-1">
<h2 className="text-2xl font-bold">User Menu</h2>
<p className="my-4">
Click {buttonDescription} in the upper right to open the user menu.
You can change your settings, sign out, or request a feature.
Click your avatar on 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

View File

@ -3,7 +3,7 @@ import { Outlet, useNavigate } from 'react-router-dom'
import Introduction from './Introduction'
import Camera from './Camera'
import Sketching from './Sketching'
import { useCallback, useEffect } from 'react'
import { useCallback } from 'react'
import makeUrlPathRelative from '../../lib/makeUrlPathRelative'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import Streaming from './Streaming'
@ -19,11 +19,9 @@ 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 =
'py-0.5 px-1 text-sm rounded bg-chalkboard-10 dark:bg-chalkboard-100 border border-chalkboard-50 border-b-2'
'p-0.5 text-sm rounded-sm bg-chalkboard-10 dark:bg-chalkboard-100 border border-chalkboard-50'
export const onboardingRoutes = [
{
@ -77,13 +75,6 @@ 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 {
@ -103,31 +94,17 @@ export function useNextClick(newStatus: string) {
export function useDismiss() {
const filePath = useAbsoluteFilePath()
const {
settings: { state, send },
settings: { send },
} = useSettingsAuthContext()
const navigate = useNavigate()
const settingsCallback = useCallback(() => {
return useCallback(() => {
send({
type: 'set.app.onboardingStatus',
data: { level: 'user', value: 'dismissed' },
})
}, [send])
/**
* A "listener" for the XState to return to "idle" state
* when the user dismisses the onboarding, using the callback above
*/
useEffect(() => {
if (
state.context.app.onboardingStatus.user === 'dismissed' &&
state.matches('idle')
) {
navigate(filePath)
}
}, [filePath, navigate, state])
return settingsCallback
navigate(filePath)
}, [send, navigate, filePath])
}
// Get the 1-indexed step number of the current onboarding step

186
src/wasm-lib/Cargo.lock generated
View File

@ -297,9 +297,9 @@ dependencies = [
[[package]]
name = "bson"
version = "2.11.0"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8a88e82b9106923b5c4d6edfca9e7db958d4e98a478ec115022e81b9b38e2c8"
checksum = "4d43b38e074cc0de2957f10947e376a1d88b9c4dbab340b590800cc1b2e066b2"
dependencies = [
"ahash",
"base64 0.13.1",
@ -406,9 +406,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.7"
version = "4.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f"
checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
dependencies = [
"clap_builder",
"clap_derive",
@ -416,9 +416,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.7"
version = "4.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f"
checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
dependencies = [
"anstream",
"anstyle",
@ -430,9 +430,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.5"
version = "4.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6"
checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@ -670,9 +670,9 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
[[package]]
name = "databake"
version = "0.1.8"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a04fbfbecca8f0679c8c06fef907594adcc3e2052e11163a6d30535a1a5604d"
checksum = "82175d72e69414ceafbe2b49686794d3a8bed846e0d50267355f83ea8fdd953a"
dependencies = [
"databake-derive",
"proc-macro2",
@ -681,9 +681,9 @@ dependencies = [
[[package]]
name = "databake-derive"
version = "0.1.8"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4078275de501a61ceb9e759d37bdd3d7210e654dbc167ac1a3678ef4435ed57b"
checksum = "377af281d8f23663862a7c84623bc5dcf7f8c44b13c7496a590bdc157f941a43"
dependencies = [
"proc-macro2",
"quote",
@ -1277,12 +1277,6 @@ 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"
@ -1375,7 +1369,7 @@ dependencies = [
[[package]]
name = "kcl-lib"
version = "0.1.60"
version = "0.1.58"
dependencies = [
"anyhow",
"approx",
@ -1405,7 +1399,6 @@ dependencies = [
"mime_guess",
"parse-display",
"pretty_assertions",
"pyo3",
"reqwest",
"ropey",
"schemars",
@ -1441,24 +1434,11 @@ 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.6"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af3de9bb4b1441f198689a9f64a8163a518377e30b348a784680e738985b95eb"
checksum = "b0cbef813153197e60c0e96f59eea0b75f8418380f414b20250ee81b60e522c3"
dependencies = [
"anyhow",
"async-trait",
@ -1471,7 +1451,7 @@ dependencies = [
"format_serde_error",
"futures",
"http 0.2.12",
"itertools 0.13.0",
"itertools 0.12.1",
"log",
"mime_guess",
"parse-display",
@ -1576,15 +1556,6 @@ 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"
@ -1844,12 +1815,6 @@ 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"
@ -1923,12 +1888,6 @@ 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"
@ -1977,76 +1936,13 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.86"
version = "1.0.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23"
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"
@ -2482,9 +2378,9 @@ dependencies = [
[[package]]
name = "schemars"
version = "0.8.21"
version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92"
checksum = "b0218ceea14babe24a4a5836f86ade86c1effbc198164e619194cb5069187e29"
dependencies = [
"bigdecimal",
"bytes",
@ -2499,9 +2395,9 @@ dependencies = [
[[package]]
name = "schemars_derive"
version = "0.8.21"
version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e"
checksum = "3ed5a1ccce8ff962e31a165d41f6e2a2dd1245099dc4d594f5574a86cd90f4d3"
dependencies = [
"proc-macro2",
"quote",
@ -2596,9 +2492,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.117"
version = "1.0.116"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813"
dependencies = [
"indexmap 2.2.5",
"itoa",
@ -2628,9 +2524,9 @@ dependencies = [
[[package]]
name = "serde_tokenstream"
version = "0.2.1"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8790a7c3fe883e443eaa2af6f705952bc5d6e8671a220b9335c8cae92c037e74"
checksum = "8a00ffd23fd882d096f09fcaae2a9de8329a328628e86027e049ee051dc1621f"
dependencies = [
"proc-macro2",
"quote",
@ -2880,12 +2776,6 @@ 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"
@ -3055,9 +2945,9 @@ dependencies = [
[[package]]
name = "tokio-tungstenite"
version = "0.23.1"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd"
checksum = "becd34a233e7e31a3dbf7c7241b38320f57393dcae8e7324b0167d21b8e320b0"
dependencies = [
"futures-util",
"log",
@ -3267,9 +3157,8 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "ts-rs"
version = "9.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e2dcf58e612adda9a83800731e8e4aba04d8a302b9029617b0b6e4b021d5357"
version = "8.1.0"
source = "git+https://github.com/Aleph-Alpha/ts-rs#be0349d5fb07a8ccab713887a61e90e3bc773c7a"
dependencies = [
"chrono",
"serde_json",
@ -3281,9 +3170,8 @@ dependencies = [
[[package]]
name = "ts-rs-macros"
version = "9.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbdee324e50a7402416d9c25270d3df4241ed528af5d36dda18b6f219551c577"
version = "8.1.0"
source = "git+https://github.com/Aleph-Alpha/ts-rs#be0349d5fb07a8ccab713887a61e90e3bc773c7a"
dependencies = [
"proc-macro2",
"quote",
@ -3371,12 +3259,6 @@ 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"
@ -3385,9 +3267,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
version = "2.5.2"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
dependencies = [
"form_urlencoded",
"idna",

View File

@ -10,11 +10,11 @@ rust-version = "1.73"
crate-type = ["cdylib"]
[dependencies]
bson = { version = "2.11.0", features = ["uuid-1", "chrono"] }
clap = "4.5.7"
bson = { version = "2.10.0", features = ["uuid-1", "chrono"] }
clap = "4.5.4"
gloo-utils = "0.2.0"
kcl-lib = { path = "kcl" }
kittycad.workspace = true
kittycad = { workspace = true }
serde_json = "1.0.116"
tokio = { version = "1.38.0", features = ["sync"] }
toml = "0.8.14"
@ -65,11 +65,10 @@ members = [
"derive-docs",
"kcl",
"kcl-macros",
"kcl-test-server",
]
[workspace.dependencies]
kittycad = { version = "0.3.6", default-features = false, features = ["js", "requests"] }
kittycad = { version = "0.3.3", default-features = false, features = ["js", "requests"] }
kittycad-modeling-session = "0.1.4"
[[test]]

View File

@ -96,16 +96,10 @@ 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,
"Stdlib functions may not be generic over types or constants, only lifetimes.",
));
}
errors.push(Error::new_spanned(
&ast.sig.generics,
"generics are not permitted for stdlib functions",
));
}
if ast.sig.variadic.is_some() {
@ -656,12 +650,7 @@ impl Parse for ItemFnForSignature {
}
fn clean_ty_string(t: &str) -> (String, proc_macro2::TokenStream) {
let mut ty_string = t
.replace("& 'a", "")
.replace('&', "")
.replace("mut", "")
.replace("< 'a >", "")
.replace(' ', "");
let mut ty_string = t.replace('&', "").replace("mut", "").replace(' ', "");
if ty_string.starts_with("Args") {
ty_string = "Args".to_string();
}

View File

@ -35,56 +35,6 @@ 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(
@ -114,6 +64,7 @@ fn test_stdlib_line_to() {
},
)
.unwrap();
let _expected = quote! {};
assert!(errors.is_empty());
expectorate::assert_contents("tests/lineTo.gen", &get_text_fmt(&item).unwrap());

View File

@ -1,194 +0,0 @@
#[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
}

View File

@ -1,194 +0,0 @@
#[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
}

View File

@ -11,7 +11,7 @@ repository = "https://github.com/KittyCAD/modeling-app"
proc-macro = true
[dependencies]
databake = "0.1.8"
databake = "0.1.7"
kcl-lib = { path = "../kcl" }
proc-macro2 = "1"
quote = "1"

View File

@ -1,13 +0,0 @@
[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"] }

View File

@ -1,207 +0,0 @@
//! 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);
},
};
}
})
}

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-lib"
description = "KittyCAD Language implementation and tools"
version = "0.1.60"
version = "0.1.58"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"
@ -16,9 +16,9 @@ async-recursion = "1.1.1"
async-trait = "0.1.80"
base64 = "0.22.1"
chrono = "0.4.38"
clap = { version = "4.5.7", default-features = false, optional = true }
clap = { version = "4.5.4", default-features = false, optional = true }
dashmap = "5.5.3"
databake = { version = "0.1.8", features = ["derive"] }
databake = { version = "0.1.7", features = ["derive"] }
derive-docs = { version = "0.1.18", path = "../derive-docs" }
form_urlencoded = "1.2.1"
futures = { version = "0.3.30" }
@ -28,7 +28,6 @@ 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"] }
@ -37,8 +36,9 @@ serde_json = "1.0.116"
sha2 = "0.10.8"
thiserror = "1.0.61"
toml = "0.8.14"
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"] }
# 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", "serde-json-impl"] }
url = { version = "2.5.0", features = ["serde"] }
uuid = { version = "1.8.0", features = ["v4", "js", "serde"] }
validator = { version = "0.18.1", features = ["derive"] }
winnow = "0.5.40"
@ -54,19 +54,15 @@ web-sys = { version = "0.3.69", features = ["console"] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
approx = "0.5"
bson = { version = "2.11.0", features = ["uuid-1", "chrono"] }
bson = { version = "2.10.0", features = ["uuid-1", "chrono"] }
tokio = { version = "1.38.0", features = ["full"] }
tokio-tungstenite = { version = "0.23.1", features = ["rustls-tls-native-roots"] }
tokio-tungstenite = { version = "0.23.0", features = ["rustls-tls-native-roots"] }
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"

View File

@ -1,11 +1,6 @@
//! Data types for the AST.
use std::{
collections::HashMap,
fmt::Write,
ops::RangeInclusive,
sync::{Arc, Mutex},
};
use std::{collections::HashMap, fmt::Write, ops::RangeInclusive};
use anyhow::Result;
use databake::*;
@ -152,21 +147,6 @@ 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 {
@ -1096,12 +1076,7 @@ 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()
@ -1135,7 +1110,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(), memory.clone());
let args = crate::std::Args::new(fn_args, self.into(), ctx.clone());
let result = func.std_lib_fn()(args).await?;
Ok(result)
}
@ -1360,7 +1335,7 @@ impl VariableDeclaration {
indentation,
self.kind,
declaration.id.name,
declaration.init.recast(options, indentation_level, false).trim()
declaration.init.recast(options, indentation_level, false)
);
output
})
@ -1776,7 +1751,7 @@ impl ArrayExpression {
inner_indentation,
self.elements
.iter()
.map(|el| el.recast(options, indentation_level, is_in_pipe))
.map(|el| el.recast(options, indentation_level, false))
.collect::<Vec<String>>()
.join(format!(",\n{}", inner_indentation).as_str()),
if is_in_pipe {
@ -2703,8 +2678,7 @@ impl PipeExpression {
}
fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
let pipe = self
.body
self.body
.iter()
.enumerate()
.map(|(index, statement)| {
@ -2736,8 +2710,7 @@ impl PipeExpression {
}
s
})
.collect::<String>();
format!("{}{}", options.get_indentation(indentation_level), pipe)
.collect::<String>()
}
/// Returns a hover value that includes the given character position.
@ -3024,7 +2997,6 @@ 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 {
@ -3293,132 +3265,6 @@ 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
@ -3486,6 +3332,8 @@ 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,
@ -3808,6 +3656,7 @@ 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.

View File

@ -3,7 +3,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value as JValue;
use crate::ast::types::{Literal, Value};
use super::{Literal, Value};
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Bake)]
#[databake(path = kcl_lib::ast::types)]

View File

@ -4,10 +4,8 @@ use databake::*;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
ast::types::ConstraintLevel,
executor::{MemoryItem, SourceRange, UserVal},
};
use super::ConstraintLevel;
use crate::executor::{MemoryItem, SourceRange, UserVal};
/// KCL value for an optional parameter which was not given an argument.
/// (remember, parameters are in the function declaration,

View File

@ -7,12 +7,10 @@ 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)]
@ -38,18 +36,21 @@ pub trait CoreDump: Clone {
async fn screenshot(&self) -> Result<String>;
/// Get a screenshot of the app and upload it to public cloud storage.
async fn upload_screenshot(&self, coredump_id: &Uuid, zoo_client: &Client) -> Result<String> {
async fn upload_screenshot(&self) -> 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_client
let links = zoo
.meta()
.create_debug_uploads(vec![kittycad::types::multipart::Attachment {
name: "".to_string(),
filename: Some(format!(r#"modeling-app/coredump-{coredump_id}-screenshot.png"#)),
filename: Some("modeling-app/core-dump-screenshot.png".to_string()),
content_type: Some("image/png".to_string()),
data,
}])
@ -57,7 +58,7 @@ pub trait CoreDump: Clone {
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
if links.is_empty() {
anyhow::bail!("Failed to upload screenshot");
anyhow::bail!("Failed to upload coredump");
}
Ok(links[0].clone())
@ -65,18 +66,13 @@ pub trait CoreDump: Clone {
/// Dump the app info.
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 os: OsInfo = self.os().await?;
let webrtc_stats: WebrtcStats = self.get_webrtc_stats().await?;
let client_state: JValue = self.get_client_state().await?;
let screenshot_url: String = self.upload_screenshot().await?;
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(&coredump_id, &zoo_client).await?;
let mut core_dump_info = CoreDumpInfo {
id: coredump_id,
let mut core_dump_info: CoreDumpInfo = CoreDumpInfo {
id: uuid::Uuid::new_v4(),
version: self.version()?,
git_rev: git_rev::try_revision_string!().map_or_else(|| "unknown".to_string(), |s| s.to_string()),
timestamp: chrono::Utc::now(),
@ -88,28 +84,32 @@ pub trait CoreDump: Clone {
client_state,
};
// pretty-printed JSON byte vector of the coredump.
let data = serde_json::to_vec_pretty(&core_dump_info)?;
// (Re)Create the zoo client.
let mut zooClient = kittycad::Client::new(self.token()?);
zooClient.set_base_url(&self.base_api_url()?);
let json = serde_json::to_string_pretty(&core_dump_info).unwrap();
// Upload the coredump.
let links = zoo_client
let result: Vec<String> = zooClient
.meta()
.create_debug_uploads(vec![kittycad::types::multipart::Attachment {
name: "".to_string(),
filename: Some(format!(r#"modeling-app/coredump-{}.json"#, coredump_id)),
filename: Some(format!(r#"modeling-app/coredump-{}.json)"#, core_dump_info.id,)),
content_type: Some("application/json".to_string()),
data,
data: json.into(),
}])
.await
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
.await?;
if links.is_empty() {
if result.is_empty() {
anyhow::bail!("Failed to upload coredump");
}
let coredump_url = &links[0];
println!("{:?}", result);
let coredump_url: String = result[0].clone();
core_dump_info.set_github_issue_url(&screenshot_url, coredump_url, &coredump_id)?;
core_dump_info.set_github_issue_url(&screenshot_url, &coredump_url)?;
Ok(core_dump_info)
}
@ -121,7 +121,7 @@ pub trait CoreDump: Clone {
#[ts(export)]
#[serde(rename_all = "snake_case")]
pub struct CoreDumpInfo {
/// The unique id for the core dump - this helps correlate uploaded files with coredump data.
/// The unique id for the coredump - this helps correlate screenshot with coredump data
pub id: Uuid,
/// The version of the app.
pub version: String,
@ -132,44 +132,43 @@ pub struct CoreDumpInfo {
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).
/// The client state (singletons and xstate)
pub client_state: JValue,
}
impl CoreDumpInfo {
/// Set the github issue url.
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();
pub fn set_github_issue_url(&mut self, screenshot_url: &str, coredump_url: &str) -> Result<()> {
let tauri_or_browser_label = if self.tauri { "tauri" } else { "browser" };
let labels = ["coredump", "bug", tauri_or_browser_label];
let body = format!(
r#"[Add a title above and insert a description of the issue here]
r#"[Insert a description of the issue here]
![Screenshot]({screenshot_url})
![Screenshot]({})
<details>
<summary><b>Core Dump</b></summary>
[{coredump_filename}]({coredump_url})
Reference ID: {coredump_id}
</details>
"#
![Coredump]({})
"#,
screenshot_url, coredump_url
);
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",

View File

@ -1,17 +1,15 @@
//! Functions for generating docs for our stdlib functions.
use std::path::Path;
use crate::std::Primitive;
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")]
@ -65,10 +63,9 @@ impl StdLibFnArg {
pub fn get_autocomplete_snippet(&self, index: usize) -> Result<Option<(usize, String)>> {
if self.type_ == "SketchGroup"
|| self.type_ == "SketchGroupSet"
|| self.type_ == "ExtrudeGroup"
|| self.type_ == "ExtrudeGroupSet"
|| self.type_ == "SketchSurface"
|| self.type_ == "SketchGroupSet"
{
return Ok(Some((index, format!("${{{}:{}}}", index, "%"))));
}

View File

@ -6,7 +6,7 @@ use std::sync::{Arc, Mutex};
use anyhow::{anyhow, Result};
use dashmap::DashMap;
use futures::{SinkExt, StreamExt};
use kittycad::types::{WebSocketRequest, WebSocketResponse};
use kittycad::types::{OkWebSocketResponseData, WebSocketRequest, WebSocketResponse};
use tokio::sync::{mpsc, oneshot, RwLock};
use tokio_tungstenite::tungstenite::Message as WsMsg;
@ -40,54 +40,23 @@ 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) -> std::result::Result<WebSocketResponse, WebSocketReadError> {
pub async fn read(&mut self) -> Result<WebSocketResponse> {
let Some(msg) = self.stream.next().await else {
return Err(anyhow::anyhow!("Failed to read from WebSocket").into());
anyhow::bail!("Failed to read from websocket");
};
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()),
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),
};
Ok(msg)
}
}
#[derive(Debug)]
pub struct TcpReadHandle {
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")
}
handle: Arc<tokio::task::JoinHandle<Result<()>>>,
}
impl Drop for TcpReadHandle {
@ -180,47 +149,15 @@ impl EngineConnection {
loop {
match tcp_read.read().await {
Ok(ws_resp) => {
// 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),
},
);
}
}
for e in ws_resp.errors.iter().flatten() {
println!("got error message: {e}");
}
if let Some(id) = ws_resp.request_id {
responses_clone.insert(id, ws_resp.clone());
}
}
Err(e) => {
match &e {
WebSocketReadError::Read(e) => eprintln!("could not read from WS: {:?}", e),
WebSocketReadError::Deser(e) => eprintln!("could not deserialize msg from WS: {:?}", e),
}
println!("got ws error: {:?}", e);
*socket_health_tcp_read.lock().unwrap() = SocketHealth::Inactive;
return Err(e);
}
@ -275,7 +212,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<WebSocketResponse, KclError> {
) -> Result<OkWebSocketResponseData, KclError> {
let (tx, rx) = oneshot::channel();
// Send the request to the engine, via the actor.
@ -320,7 +257,14 @@ impl EngineManager for EngineConnection {
}
// We pop off the responses to cleanup our mappings.
if let Some((_, resp)) = self.responses.remove(&id) {
return Ok(resp);
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],
}))
};
}
}

View File

@ -1,13 +1,10 @@
//! Functions for setting up our WebSocket and WebRTC connections for communications with the
//! engine.
use std::{
collections::HashMap,
sync::{Arc, Mutex},
};
use std::sync::{Arc, Mutex};
use anyhow::Result;
use kittycad::types::{OkWebSocketResponseData, WebSocketRequest, WebSocketResponse};
use kittycad::types::{OkWebSocketResponseData, WebSocketRequest};
use crate::{errors::KclError, executor::DefaultPlanes};
@ -40,43 +37,13 @@ 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<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,
}),
}
) -> Result<OkWebSocketResponseData, KclError> {
Ok(OkWebSocketResponseData::Modeling {
modeling_response: kittycad::types::OkModelingCmdResponse::Empty {},
})
}
}

View File

@ -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::WebSocketResponse, KclError> {
) -> Result<kittycad::types::OkWebSocketResponseData, 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,6 +182,18 @@ impl crate::engine::EngineManager for EngineConnection {
})
})?;
Ok(ws_result)
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],
}))
}
}
}

View File

@ -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::WebSocketResponse, crate::errors::KclError>;
) -> Result<kittycad::types::OkWebSocketResponseData, crate::errors::KclError>;
async fn clear_scene(&self, source_range: crate::executor::SourceRange) -> Result<(), crate::errors::KclError> {
self.batch_modeling_cmd(
self.send_modeling_cmd(
uuid::Uuid::new_v4(),
source_range,
&kittycad::types::ModelingCmd::SceneClearAll {},
kittycad::types::ModelingCmd::SceneClearAll {},
)
.await?;
@ -67,13 +67,12 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
Ok(())
}
// Add a modeling command to the batch but don't fire it right away.
async fn batch_modeling_cmd(
async fn send_modeling_cmd(
&self,
id: uuid::Uuid,
source_range: crate::executor::SourceRange,
cmd: &kittycad::types::ModelingCmd,
) -> Result<(), crate::errors::KclError> {
cmd: kittycad::types::ModelingCmd,
) -> Result<kittycad::types::OkWebSocketResponseData, crate::errors::KclError> {
let req = WebSocketRequest::ModelingCmdReq {
cmd: cmd.clone(),
cmd_id: id,
@ -82,17 +81,16 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
// Add cmd to the batch.
self.batch().lock().unwrap().push((req, source_range));
Ok(())
}
// 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);
/// 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?;
// Return a fake modeling_request empty response.
if !is_sending {
return Ok(OkWebSocketResponseData::Modeling {
modeling_response: kittycad::types::OkModelingCmdResponse::Empty {},
});
}
// Flush the batch queue.
self.flush_batch(source_range).await
@ -126,7 +124,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
let batched_requests = WebSocketRequest::ModelingCmdBatchReq {
requests,
batch_id: uuid::Uuid::new_v4(),
responses: true,
responses: false,
};
let final_req = if self.batch().lock().unwrap().len() == 1 {
@ -157,41 +155,23 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
self.batch().lock().unwrap().clear();
// We pop off the responses to cleanup our mappings.
match final_req {
let id_final = match final_req {
WebSocketRequest::ModelingCmdBatchReq {
ref requests,
requests: _,
batch_id,
responses: _,
} => {
// 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)?;
} => batch_id,
WebSocketRequest::ModelingCmdReq { cmd: _, cmd_id } => cmd_id,
_ => {
return Err(KclError::Engine(KclErrorDetails {
message: format!("The final request is not a modeling command: {:?}", final_req),
source_ranges: vec![source_range],
}));
}
};
// 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(
@ -206,10 +186,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.batch_modeling_cmd(
self.send_modeling_cmd(
plane_id,
source_range,
&ModelingCmd::MakePlane {
ModelingCmd::MakePlane {
clobber: false,
origin: default_origin,
size: default_size,
@ -222,10 +202,10 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
if let Some(color) = color {
// Set the color.
self.batch_modeling_cmd(
self.send_modeling_cmd(
uuid::Uuid::new_v4(),
source_range,
&ModelingCmd::PlaneSetColor { color, plane_id },
ModelingCmd::PlaneSetColor { color, plane_id },
)
.await?;
}
@ -332,79 +312,62 @@ 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]],
})
})?;
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![],
}))
}
true
}
#[derive(Debug, Hash, Eq, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]

View File

@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};
use thiserror::Error;
use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity};
use crate::{executor::SourceRange, lsp::IntoDiagnostic};
use crate::executor::SourceRange;
#[derive(Error, Debug, Serialize, Deserialize, ts_rs::TS, Clone, PartialEq, Eq)]
#[ts(export)]
@ -42,9 +42,19 @@ pub struct KclErrorDetails {
}
impl KclError {
/// Get the error message.
pub fn get_message(&self) -> String {
format!("{}: {}", self.error_type(), self.message())
/// Get the error message, line and column from the error and input code.
pub fn get_message_line_column(&self, input: &str) -> (String, Option<usize>, Option<usize>) {
// Calculate the line and column of the error from the source range.
let (line, column) = if let Some(range) = self.source_ranges().first() {
let line = input[..range.0[0]].lines().count();
let column = input[..range.0[0]].lines().last().map(|l| l.len()).unwrap_or_default();
(Some(line), Some(column))
} else {
(None, None)
};
(format!("{}: {}", self.error_type(), self.message()), line, column)
}
pub fn error_type(&self) -> &'static str {
@ -96,6 +106,24 @@ impl KclError {
}
}
pub fn to_lsp_diagnostic(&self, code: &str) -> Diagnostic {
let (message, _, _) = self.get_message_line_column(code);
let source_ranges = self.source_ranges();
Diagnostic {
range: source_ranges.first().map(|r| r.to_lsp_range(code)).unwrap_or_default(),
severity: Some(DiagnosticSeverity::ERROR),
code: None,
// TODO: this is neat we can pass a URL to a help page here for this specific error.
code_description: None,
source: Some("kcl".to_string()),
message,
related_information: None,
tags: None,
data: None,
}
}
pub fn override_source_ranges(&self, source_ranges: Vec<SourceRange>) -> Self {
let mut new = self.clone();
match &mut new {
@ -135,26 +163,6 @@ impl KclError {
}
}
impl IntoDiagnostic for KclError {
fn to_lsp_diagnostic(&self, code: &str) -> Diagnostic {
let message = self.get_message();
let source_ranges = self.source_ranges();
Diagnostic {
range: source_ranges.first().map(|r| r.to_lsp_range(code)).unwrap_or_default(),
severity: Some(DiagnosticSeverity::ERROR),
code: None,
// TODO: this is neat we can pass a URL to a help page here for this specific error.
code_description: None,
source: Some("kcl".to_string()),
message,
related_information: None,
tags: None,
data: None,
}
}
}
/// This is different than to_string() in that it will serialize the Error
/// the struct as JSON so we can deserialize it on the js side.
impl From<KclError> for String {
@ -168,20 +176,3 @@ 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())
}
}

View File

@ -15,7 +15,6 @@ use crate::{
engine::EngineManager,
errors::{KclError, KclErrorDetails},
fs::FileManager,
settings::types::UnitLength,
std::{FunctionKind, StdLib},
};
@ -189,15 +188,6 @@ 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)]
@ -207,15 +197,6 @@ 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)]
@ -316,39 +297,6 @@ 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>,
@ -464,88 +412,6 @@ 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.
@ -750,7 +616,6 @@ 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]);
@ -1127,7 +992,7 @@ pub struct ExecutorContext {
#[derive(Debug, Clone)]
pub struct ExecutorSettings {
/// The unit to use in modeling dimensions.
pub units: UnitLength,
pub units: crate::settings::types::UnitLength,
/// Highlight edges of 3D objects?
pub highlight_edges: bool,
/// Whether or not Screen Space Ambient Occlusion (SSAO) is enabled.
@ -1200,10 +1065,10 @@ impl ExecutorContext {
// Set the edge visibility.
engine
.batch_modeling_cmd(
.send_modeling_cmd(
uuid::Uuid::new_v4(),
SourceRange::default(),
&kittycad::types::ModelingCmd::EdgeLinesVisible {
kittycad::types::ModelingCmd::EdgeLinesVisible {
hidden: !settings.highlight_edges,
},
)
@ -1218,57 +1083,6 @@ 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.
@ -1279,11 +1093,11 @@ impl ExecutorContext {
) -> Result<ProgramMemory, KclError> {
// Before we even start executing the program, set the units.
self.engine
.batch_modeling_cmd(
.send_modeling_cmd(
uuid::Uuid::new_v4(),
SourceRange::default(),
&kittycad::types::ModelingCmd::SetSceneUnits {
unit: self.settings.units.into(),
kittycad::types::ModelingCmd::SetSceneUnits {
unit: self.settings.units.clone().into(),
},
)
.await?;
@ -1326,7 +1140,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(), memory.clone());
let args = crate::std::Args::new(args, call_expr.into(), self.clone());
let result = func.std_lib_fn()(args).await?;
memory.return_ = Some(ProgramReturn::Value(result));
}
@ -1495,7 +1309,7 @@ impl ExecutorContext {
}
/// Update the units for the executor.
pub fn update_units(&mut self, units: UnitLength) {
pub fn update_units(&mut self, units: crate::settings::types::UnitLength) {
self.settings.units = units;
}

Some files were not shown because too many files have changed in this diff Show More