Compare commits

...

18 Commits

Author SHA1 Message Date
e489222b6a expose mock executing to python library; (#6980)
* expose mock executing to python library;

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* bump

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* Update rust/kcl-python-bindings/src/lib.rs

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
2025-05-15 12:35:29 -07:00
d93a57d7bf more lsp tests / pass python a bool on parse (#6975)
* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* parse returns bool for python

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-05-15 11:04:53 -07:00
d34aea345b #6548 Fix autocomplete for offsetPlane (#6940)
* handle Plane in get_autocomplete_snippet to fix missing default plane value for offsetPlane

* adds test for offsetPlane default plane value

* cleanup test

* use XY instead of XZ for default plane

* add rust test for offsetplane autocomplete

* remove playwright test for offestPlane, as it has rust test now and there are autocomplete tests already
2025-05-15 19:53:35 +02:00
0b6102b0ac #6214 Fix center circle radius issues (#6971)
* add missing radius line for center circle

* remove some duplication by introducing createLineShape
2025-05-15 13:30:25 -04:00
9e0873ed84 Remove updater-test builds (#6962)
Remove updater-test
2025-05-15 13:25:40 -04:00
8587eb5fea Make idle sestting 30sec in nightly (#6972) 2025-05-15 17:17:59 +00:00
b898c27e74 Add KCL constants for sweep relativeTo strings (#6974)
* Add KCL constants for sweep relativeTo strings

Before:

```
sweep(pill, path = sweepPath, relativeTo = "trajectoryCurve")
```

After:

```
sweep(pill, path = sweepPath, relativeTo = sweep::TRAJECTORY)
```

* Update docs
2025-05-15 10:13:04 -07:00
3026866a16 Remove old dry-run validations on Sweep, Loft, Revolve, and Shell (#6969) 2025-05-15 12:11:40 -05:00
92fc294eae Update mesh clone test (#6597) 2025-05-15 16:46:37 +00:00
21e967ea7f More consistent error handling in modelingMachine codemods (#6910)
* First pass at consistency in modelingMachine codemods

* Add 'no kcl errors' guard instead of if check for all the existing ones

* Add more commands and improve consistency

* Add comments

* Fix test with old kcl that was showcasing the very behavior we're trying to fix
https://kittycadworkspace.slack.com/archives/C07A80B83FS/p1747231832870739?thread_ts=1747231178.515289&cid=C07A80B83FS

* Add test for sketch and helix

* Remove guard use and move hasErrors check closer to updateAst calls

* Revert "Remove guard use and move hasErrors check closer to updateAst calls"

This reverts commit 868ea4b605.

* Remove toasts from guards

* Remove some scene.settled calls

* Lint

* More shaky fixes

* Clean up and more test fixes

---------

Co-authored-by: Frank Noirot <frankjohnson1993@gmail.com>
2025-05-15 12:26:20 -04:00
3f00e7186c Backoff Reconnect (#6358)
* Add a Stop command that does a bit of cleanup in the engineStateMachine

* Major network code startup refactor; backoff reconnect; many more logs for us

* Save camera state after every user interaction in case of disconnection and restoral

* Translate basic WebSocket error numbers into useful text

* Add a RestartRequest event to the engineCommandManager

* Adjust for a rebase

* Add E2E test for stream pause behavior

* Fix snapshot test

* fmt, lint

* small issue

* Fix tests

* Fix up idle test

* One last fix

* fixes

* Remove circ dep

* fix test

* use a const for time

* TEST -> isPlaywright instead

* whoops
2025-05-15 11:58:00 -04:00
d3a4fd8b55 Raw dog parse for python bindings (#6970)
* raw dog parse python

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add tests

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-05-15 15:39:28 +00:00
2be7107cca External: Fix: symbol replace text box dark mode contrast (#6827) (#6927)
* fix: symbol replace text box dark mode contrast (#6827)

* fix: symbol replace text box dark mode contrast (#6827)

* correct fix for rename popup white text on white background #6827

* correct fix for rename popup white text on white background #6827

* added comments explaining the change

---------

Co-authored-by: rajgandhi1 <rajgandhi9952@gmail.com>
2025-05-15 12:57:39 +00:00
94f194a984 new sweep default (#6965)
snaps

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-05-15 11:50:55 +00:00
4fe880a970 Revert "fixes"
This reverts commit 8f5fbfc273.
2025-05-15 04:28:20 -07:00
8f5fbfc273 fixes
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-05-15 04:27:59 -07:00
e660f52bb0 Restore automated snapshot commits (#6960)
* Restore automated snapshot commits

* Update snapshots

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-05-15 03:28:42 +00:00
d74fdd9369 Color picker goober works w single quotes (#6957)
* lsp stuffs

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-05-15 12:37:14 +10:00
83 changed files with 2881 additions and 1629 deletions

View File

@ -123,18 +123,6 @@ jobs:
- id: export_notes
run: echo "notes=`cat release-notes.md`" >> "$GITHUB_OUTPUT"
- name: Prepare electron-builder.yml file for updater test
if: ${{ env.IS_RELEASE == 'true' }}
run: |
yq -i '.publish[0].url = "https://dl.zoo.dev/releases/modeling-app/updater-test"' electron-builder.yml
- uses: actions/upload-artifact@v4
if: ${{ env.IS_RELEASE == 'true' }}
with:
name: prepared-files-updater-test
path: |
electron-builder.yml
build-apps:
needs: [prepare-files]
@ -259,49 +247,6 @@ jobs:
# TODO: add the 'Build for Mac TestFlight (nightly)' stage back
# The steps below are for updater-test builds, only on release
- uses: actions/download-artifact@v4
if: ${{ env.IS_RELEASE == 'true' }}
name: prepared-files-updater-test
- name: Copy updated electron-builder.yml file for updater test
if: ${{ env.IS_RELEASE == 'true' }}
run: |
ls -R prepared-files-updater-test
cp prepared-files-updater-test/electron-builder.yml electron-builder.yml
- name: Build the app (updater-test)
if: ${{ env.IS_RELEASE == 'true' }}
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
run: npm run tronb:package:prod
- uses: actions/upload-artifact@v4
if: ${{ env.IS_RELEASE == 'true' }}
with:
name: updater-test-arm64-${{ matrix.platform }}
path: |
out/*-arm64-win.exe
out/*-arm64-mac.dmg
out/*-arm64-linux.AppImage
- uses: actions/upload-artifact@v4
if: ${{ env.IS_RELEASE == 'true' }}
with:
name: updater-test-x64-${{ matrix.platform }}
path: |
out/*-x64-win.exe
out/*-x64-mac.dmg
out/*-x86_64-linux.AppImage
upload-apps-release:
runs-on: ubuntu-22.04

View File

@ -40,7 +40,7 @@ jobs:
- name: Install dependencies
run: npm install
- name: Download Wasm Cache
- name: Download Wasm cache
id: download-wasm
if: ${{ github.event_name != 'schedule' && steps.filter.outputs.rust == 'false' }}
uses: dawidd6/action-download-artifact@v7
@ -52,7 +52,7 @@ jobs:
branch: main
path: rust/kcl-wasm-lib/pkg
- name: Build WASM condition
- name: Build Wasm condition
id: wasm
run: |
set -euox pipefail
@ -70,7 +70,7 @@ jobs:
run: |
[ -e rust-toolchain.toml ] || cp rust/rust-toolchain.toml ./
- name: Install rust
- name: Install Rust
if: ${{ steps.wasm.outputs.should-build-wasm == 'true' }}
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
@ -81,7 +81,7 @@ jobs:
with:
tool: wasm-pack
- name: Rust Cache
- name: Use Rust cache
if: ${{ steps.wasm.outputs.should-build-wasm == 'true' }}
uses: Swatinem/rust-cache@v2
with:
@ -117,7 +117,7 @@ jobs:
- uses: actions/download-artifact@v4
name: prepared-wasm
- name: Copy prepared wasm
- name: Copy prepared Wasm
run: |
ls -R prepared-wasm
cp prepared-wasm/kcl_wasm_lib_bg.wasm public
@ -133,20 +133,17 @@ jobs:
id: deps-install
run: npm install
- name: Cache Playwright Browsers
- name: Cache browsers
uses: actions/cache@v4
with:
path: |
~/.cache/ms-playwright/
key: ${{ runner.os }}-playwright-${{ hashFiles('package-lock.json') }}
- name: Install Playwright Browsers
- name: Install browsers
run: npm run playwright install --with-deps
- name: build web
run: npm run tronb:vite:dev
- name: Run ubuntu/chrome snapshots
- name: Capture snapshots
uses: nick-fields/retry@v3.0.2
with:
shell: bash
@ -170,7 +167,7 @@ jobs:
retention-days: 30
overwrite: true
- name: Check for changes
- name: Check diff
if: ${{ github.ref != 'refs/heads/main' }}
shell: bash
id: git-check
@ -181,9 +178,8 @@ jobs:
else echo "modified=false" >> $GITHUB_OUTPUT
fi
- name: Commit changes, if any
# TODO: find a more reliable way to detect visual changes
if: ${{ false && steps.git-check.outputs.modified == 'true' }}
- name: Commit changes
if: ${{ steps.git-check.outputs.modified == 'true' }}
shell: bash
run: |
git add e2e/playwright/snapshot-tests.spec.ts-snapshots e2e/playwright/snapshots
@ -193,7 +189,7 @@ jobs:
git fetch origin
echo ${{ github.head_ref }}
git checkout ${{ github.head_ref }}
git commit -m "A snapshot a day keeps the bugs away! 📷🐛" || true
git commit --message "Update snapshots" || true
git push
git push origin ${{ github.head_ref }}

View File

@ -130,7 +130,7 @@ git tag $VERSION
git push origin --tags
```
This will trigger the `build-apps` workflow, set the version, build & sign the apps, and generate release files as well as updater-test artifacts.
This will trigger the `build-apps` workflow, set the version, build & sign the apps, and generate release files.
The workflow should be listed right away [in this list](https://github.com/KittyCAD/modeling-app/actions/workflows/build-apps.yml?query=event%3Apush)).
@ -142,13 +142,10 @@ The release builds can be found under the `out-{arch}-{platform}` zip files, at
Manually test against this [list](https://github.com/KittyCAD/modeling-app/issues/3588) across Windows, MacOS, Linux and posting results as comments in the issue.
##### Updater-test builds
The other `build-apps` output in the release `build-apps` workflow (triggered by 2.) is `updater-test-{arch}-{platform}`. It's a semi-automated process: for macOS, Windows, and Linux, download the corresponding updater-test artifact file, install the app, run it, expect an updater prompt to a dummy v0.255.255, install it and check that the app comes back at that version.
The only difference with these builds is that they point to a different update location on the release bucket, with this dummy v0.255.255 always available. This helps ensuring that the version we release will be able to update to the next one available.
If the prompt doesn't show up, start the app in command line to grab the electron-updater logs. This is likely an issue with the current build that needs addressing (or the updater-test location in the storage bucket).
A prompt should show up asking for a downgrade to the last release version. Running through that at the end of testing
and making sure the current release candidate has the ability to be updated to what electron-updater points to is critical,
but what is actually being downloaded and installed isn't.
If the prompt doesn't show up, start the app in command line to grab the electron-updater logs. This is likely an issue with the current build that needs addressing.
```
# Windows (PowerShell)

View File

@ -0,0 +1,16 @@
---
title: "sweep::SKETCH_PLANE"
subtitle: "Constant in std::sweep"
excerpt: "Local/relative to a position centered within the plane being sketched on"
layout: manual
---
Local/relative to a position centered within the plane being sketched on
```kcl
sweep::SKETCH_PLANE: string = 'sketchPlane'
```

View File

@ -0,0 +1,16 @@
---
title: "sweep::TRAJECTORY"
subtitle: "Constant in std::sweep"
excerpt: "Local/relative to the trajectory curve"
layout: manual
---
Local/relative to the trajectory curve
```kcl
sweep::TRAJECTORY: string = 'trajectoryCurve'
```

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -128,6 +128,9 @@ layout: manual
* [`E`](/docs/kcl-std/consts/std-math-E)
* [`PI`](/docs/kcl-std/consts/std-math-PI)
* [`TAU`](/docs/kcl-std/consts/std-math-TAU)
* [**std::sweep**](/docs/kcl-std/modules/std-sweep)
* [`sweep::SKETCH_PLANE`](/docs/kcl-std/consts/std-sweep-SKETCH_PLANE)
* [`sweep::TRAJECTORY`](/docs/kcl-std/consts/std-sweep-TRAJECTORY)
* [**std::turns**](/docs/kcl-std/modules/std-turns)
* [`turns::HALF_TURN`](/docs/kcl-std/consts/std-turns-HALF_TURN)
* [`turns::QUARTER_TURN`](/docs/kcl-std/consts/std-turns-QUARTER_TURN)

View File

@ -0,0 +1,17 @@
---
title: "sweep"
subtitle: "Module in std"
excerpt: ""
layout: manual
---
## Functions and constants
* [`sweep::SKETCH_PLANE`](/docs/kcl-std/consts/std-sweep-SKETCH_PLANE)
* [`sweep::TRAJECTORY`](/docs/kcl-std/consts/std-sweep-TRAJECTORY)

View File

@ -19,6 +19,7 @@ You might also want the [KCL language reference](/docs/kcl-lang) or the [KCL gui
* [`math`](/docs/kcl-std/modules/std-math)
* [`sketch`](/docs/kcl-std/modules/std-sketch)
* [`solid`](/docs/kcl-std/modules/std-solid)
* [`sweep::sweep`](/docs/kcl-std/modules/std-sweep)
* [`transform`](/docs/kcl-std/modules/std-transform)
* [`turns::turns`](/docs/kcl-std/modules/std-turns)
* [`types`](/docs/kcl-std/modules/std-types)

View File

@ -251786,7 +251786,7 @@
}
},
"required": false,
"description": "What is the sweep relative to? Can be either 'sketchPlane' or 'trajectoryCurve'. Defaults to sketchPlane.",
"description": "What is the sweep relative to? Can be either 'sketchPlane' or 'trajectoryCurve'. Defaults to trajectoryCurve.",
"labelRequired": true
},
{
@ -256720,7 +256720,7 @@
false
],
[
"// Create a spring by sweeping around a helix path.\n\n// Create a helix around the Z axis.\nhelixPath = helix(\n angleStart = 0,\n ccw = true,\n revolutions = 4,\n length = 10,\n radius = 5,\n axis = Z,\n)\n\n// Create a spring by sweeping around the helix path.\nspringSketch = startSketchOn(YZ)\n |> circle(center = [0, 0], radius = 1)\n |> sweep(path = helixPath)",
"// Create a spring by sweeping around a helix path.\n\n// Create a helix around the Z axis.\nhelixPath = helix(\n angleStart = 0,\n ccw = true,\n revolutions = 4,\n length = 10,\n radius = 5,\n axis = Z,\n)\n\n// Create a spring by sweeping around the helix path.\nspringSketch = startSketchOn(YZ)\n |> circle(center = [0, 0], radius = 1)\n |> sweep(path = helixPath, relativeTo = \"sketchPlane\")",
false
],
[

File diff suppressed because one or more lines are too long

View File

@ -134,8 +134,6 @@ extrude001 = extrude(sketch001, length = 5)`
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await page.waitForTimeout(1000)
// Ensure badge is present
const codePaneButtonHolder = page.locator('#code-button-holder')
await expect(codePaneButtonHolder).toContainText('notification', {
@ -183,7 +181,7 @@ extrude001 = extrude(sketch001, length = 5)`
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await scene.settled(cmdBar)
// await scene.settled(cmdBar)
// Ensure badge is present
const codePaneButtonHolder = page.locator('#code-button-holder')

View File

@ -1533,7 +1533,6 @@ sketch001 = startSketchOn(XZ)
await homePage.goToModelingScene()
await scene.connectionEstablished()
await scene.settled(cmdBar)
await scene.expectPixelColor(
TEST_COLORS.DARK_MODE_BKGD,

View File

@ -1931,84 +1931,6 @@ sketch002 = startSketchOn(XZ)
})
})
test(`Sweep point-and-click failing validation`, async ({
context,
page,
homePage,
scene,
toolbar,
cmdBar,
}) => {
const initialCode = `@settings(defaultLengthUnit = in)
sketch001 = startSketchOn(YZ)
|> circle(
center = [0, 0],
radius = 500
)
sketch002 = startSketchOn(XZ)
|> startProfile(at = [0, 0])
|> xLine(length = -500)
|> line(endAbsolute = [-2000, 500])
`
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.settled(cmdBar)
// One dumb hardcoded screen pixel value
const testPoint = { x: 700, y: 250 }
const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const [clickOnSketch2] = scene.makeMouseHelpers(
testPoint.x - 50,
testPoint.y
)
await test.step(`Look for sketch001`, async () => {
await toolbar.closePane('code')
await scene.expectPixelColor([53, 53, 53], testPoint, 15)
})
await test.step(`Go through the command bar flow and fail validation with a toast`, async () => {
await toolbar.sweepButton.click()
await expect
.poll(() => page.getByText('Please select one').count())
.toBe(1)
await cmdBar.expectState({
commandName: 'Sweep',
currentArgKey: 'sketches',
currentArgValue: '',
headerArguments: {
Sectional: '',
Sketches: '',
Path: '',
},
highlightedHeaderArg: 'sketches',
stage: 'arguments',
})
await clickOnSketch1()
await cmdBar.progressCmdBar()
await cmdBar.expectState({
commandName: 'Sweep',
currentArgKey: 'path',
currentArgValue: '',
headerArguments: {
Sectional: '',
Sketches: '1 face',
Path: '',
},
highlightedHeaderArg: 'path',
stage: 'arguments',
})
await clickOnSketch2()
await cmdBar.progressCmdBar()
await expect(
page.getByText('Unable to sweep with the current selection. Reason:')
).toBeVisible()
})
})
test(`Fillet point-and-click`, async ({
context,
page,
@ -3627,67 +3549,6 @@ profile001 = startProfile(sketch001, at = [-20, 20])
})
})
test(`Shell dry-run validation rejects sweeps`, async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
const initialCode = `sketch001 = startSketchOn(YZ)
|> circle(
center = [0, 0],
radius = 500
)
sketch002 = startSketchOn(XZ)
|> startProfile(at = [0, 0])
|> xLine(length = -2000)
sweep001 = sweep(sketch001, path = sketch002)
`
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.settled(cmdBar)
// One dumb hardcoded screen pixel value
const testPoint = { x: 500, y: 250 }
const [clickOnSweep] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
await test.step(`Confirm sweep exists`, async () => {
await toolbar.closePane('code')
await scene.expectPixelColor([231, 231, 231], testPoint, 15)
})
await test.step(`Go through the Shell flow and fail validation with a toast`, async () => {
await toolbar.shellButton.click()
await expect
.poll(() => page.getByText('Please select one').count())
.toBe(1)
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'selection',
currentArgValue: '',
headerArguments: {
Selection: '',
Thickness: '',
},
highlightedHeaderArg: 'selection',
commandName: 'Shell',
})
await clickOnSweep()
await page.waitForTimeout(500)
await cmdBar.progressCmdBar()
await expect(
page.getByText('Unable to shell with the current selection. Reason:')
).toBeVisible()
await page.waitForTimeout(1000)
})
})
test.describe('Revolve point and click workflows', () => {
test('Base case workflow, auto spam continue in command bar', async ({
context,
@ -4943,4 +4804,34 @@ path001 = startProfile(sketch001, at = [0, 0])
)
})
})
test(`Point and click codemods can't run on KCL errors`, async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
const badCode = `sketch001 = startSketchOn(XZ)
profile001 = circle(sketch001, center = [0, 0], radius = 1)
extrude001 = extrude(profile001 length = 1)`
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, badCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.connectionEstablished()
await test.step(`Start Sketch is disabled`, async () => {
await expect(toolbar.startSketchBtn).not.toBeEnabled()
await editor.expectEditor.toContain(badCode, { shouldNormalise: true })
})
await test.step(`Helix is disabled`, async () => {
await expect(toolbar.helixButton).not.toBeEnabled()
await editor.expectEditor.toContain(badCode, { shouldNormalise: true })
})
})
})

View File

@ -19,11 +19,12 @@ test.describe('Regression tests', () => {
context,
page,
homePage,
scene,
}) => {
// because the model has `line([0,0]..` it is valid code, but the model is invalid
// regression test for https://github.com/KittyCAD/modeling-app/issues/3251
// Since the bad model also found as issue with the artifact graph, which in tern blocked the editor diognostics
const u = await getUtils(page)
// const u = await getUtils(page)
await context.addInitScript(async () => {
localStorage.setItem(
'persistCode',
@ -40,7 +41,8 @@ test.describe('Regression tests', () => {
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await u.waitForPageLoad()
await scene.connectionEstablished()
// await u.waitForPageLoad()
// error in guter
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
@ -188,8 +190,8 @@ extrude001 = extrude(sketch001, length = 50)
page.locator('.pretty-json-container >> text=myVar:"67')
).toBeVisible()
})
test('ProgramMemory can be serialised', async ({ page, homePage }) => {
const u = await getUtils(page)
test('ProgramMemory can be serialised', async ({ page, homePage, scene }) => {
// const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
@ -214,11 +216,12 @@ extrude001 = extrude(sketch001, length = 50)
// Listen for all console events and push the message text to an array
page.on('console', (message) => messages.push(message.text()))
await homePage.goToModelingScene()
await u.waitForPageLoad()
// await u.waitForPageLoad()
await scene.connectionEstablished()
// wait for execution done
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
// await u.openDebugPanel()
// await u.expectCmdLog('[data-message-type="execution-done"]')
const forbiddenMessages = ['cannot serialize tagged newtype variant']
forbiddenMessages.forEach((forbiddenMessage) => {
@ -232,6 +235,7 @@ extrude001 = extrude(sketch001, length = 50)
context,
page,
homePage,
scene,
}) => {
const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio
@ -250,11 +254,10 @@ extrude001 = extrude(sketch001, length = 50)
shell(exampleSketch, faces = ['end'], thickness = 0.25)`
)
})
await homePage.goToModelingScene()
await scene.connectionEstablished()
await expect(async () => {
await homePage.goToModelingScene()
await u.waitForPageLoad()
// error in guter
await expect(page.locator('.cm-lint-marker-error')).toBeVisible({
timeout: 1_000,

View File

@ -1365,18 +1365,18 @@ solid001 = subtract([extrude001], tools = [extrude002])
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`fn in2mm = (inches) => {
`fn in2mm(@inches) {
return inches * 25.4
}
const railTop = in2mm(.748)
const railSide = in2mm(.024)
const railBaseWidth = in2mm(.612)
const railWideWidth = in2mm(.835)
const railBaseLength = in2mm(.200)
const railClampable = in2mm(.200)
railTop = in2mm(.748)
railSide = in2mm(.024)
railBaseWidth = in2mm(.612)
railWideWidth = in2mm(.835)
railBaseLength = in2mm(.200)
railClampable = in2mm(.200)
const rail = startSketchOn(XZ)
rail = startSketchOn(XZ)
|> startProfile(at = [-railTop / 2, railClampable + railBaseLength])
|> line(endAbsolute = [
railTop / 2,
@ -3540,7 +3540,6 @@ profile001 = startProfile(sketch001, at = [127.56, 179.02])
await homePage.openProject('multi-file-sketch-test')
await scene.connectionEstablished()
await scene.settled(cmdBar)
await u.closeDebugPanel()
@ -3555,9 +3554,6 @@ profile001 = startProfile(sketch001, at = [127.56, 179.02])
await toolbar.openFile('error.kcl')
// Ensure filetree is populated
await scene.settled(cmdBar)
await expect(
toolbar.featureTreePane.getByRole('button', { name: 'Sketch' })
).toHaveCount(0)

View File

@ -1,4 +1,3 @@
import { KCL_DEFAULT_LENGTH } from '@src/lib/constants'
import type { CmdBarFixture } from '@e2e/playwright/fixtures/cmdBarFixture'
import type { SceneFixture } from '@e2e/playwright/fixtures/sceneFixture'
import { TEST_SETTINGS, TEST_SETTINGS_KEY } from '@e2e/playwright/storageStates'
@ -9,6 +8,7 @@ import {
settingsToToml,
} from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test'
import { KCL_DEFAULT_LENGTH } from '@src/lib/constants'
test.beforeEach(async ({ page, context }) => {
// Make the user avatar image always 404
@ -766,7 +766,7 @@ test.describe('Grid visibility', { tag: '@snapshot' }, () => {
})
})
test('theme persists', async ({ page, context }) => {
test('theme persists', async ({ page, context, homePage }) => {
const u = await getUtils(page)
await context.addInitScript(async () => {
localStorage.setItem(
@ -784,7 +784,7 @@ test('theme persists', async ({ page, context }) => {
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await homePage.goToModelingScene()
await page.waitForTimeout(500)
// await page.getByRole('link', { name: 'Settings Settings (tooltip)' }).click()
@ -812,7 +812,7 @@ test('theme persists', async ({ page, context }) => {
// Disconnect and reconnect to check the theme persists through a reload
// Expect the network to be down
await expect(networkToggle).toContainText('Offline')
await expect(networkToggle).toContainText('Problem')
// simulate network up
await u.emulateNetworkConditions({
@ -873,6 +873,50 @@ sweepSketch = startSketchOn(XY)
mask: lowerRightMasks(page),
})
})
test('code color goober works with single quotes', async ({
page,
context,
scene,
cmdBar,
}) => {
const u = await getUtils(page)
await context.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`// Create a pipe using a sweep.
// Create a path for the sweep.
sweepPath = startSketchOn(XZ)
|> startProfile(at = [0.05, 0.05])
|> line(end = [0, 7])
|> tangentialArc(angle = 90, radius = 5)
|> line(end = [-3, 0])
|> tangentialArc(angle = -90, radius = 5)
|> line(end = [0, 7])
sweepSketch = startSketchOn(XY)
|> startProfile(at = [2, 0])
|> arc(angleStart = 0, angleEnd = 360, radius = 2)
|> sweep(path = sweepPath)
|> appearance(
color = '#bb00ff',
metalness = 90,
roughness = 90
)
`
)
})
await page.setViewportSize({ width: 1200, height: 1000 })
await u.waitForAuthSkipAppStart()
await scene.settled(cmdBar)
await expect(page, 'expect small color widget').toHaveScreenshot({
maxDiffPixels: 100,
mask: lowerRightMasks(page),
})
})
test('code color goober opening window', async ({
page,

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -1,236 +1,257 @@
import type { EngineCommand } from '@src/lang/std/artifactGraph'
import { uuidv4 } from '@src/lib/utils'
import { commonPoints, getUtils } from '@e2e/playwright/test-utils'
import {
commonPoints,
getUtils,
TEST_COLORS,
circleMove,
} from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test'
test.describe(
'Test network and connection issues',
{
tag: ['@macos', '@windows'],
},
() => {
test(
'simulate network down and network little widget',
{ tag: '@skipLocalEngine' },
async ({ page, homePage }) => {
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
test.describe('Test network related behaviors', () => {
test(
'simulate network down and network little widget',
{ tag: '@skipLocalEngine' },
async ({ page, homePage }) => {
const networkToggleConnectedText = page.getByText('Connected')
const networkToggleWeakText = page.getByText('Network health (Weak)')
await homePage.goToModelingScene()
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
const networkToggle = page.getByTestId('network-toggle')
await homePage.goToModelingScene()
// This is how we wait until the stream is online
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled({ timeout: 15000 })
const networkToggle = page.getByTestId('network-toggle')
const networkWidget = page.locator('[data-testid="network-toggle"]')
await expect(networkWidget).toBeVisible()
await networkWidget.hover()
// This is how we wait until the stream is online
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled({ timeout: 15000 })
const networkPopover = page.locator('[data-testid="network-popover"]')
await expect(networkPopover).not.toBeVisible()
const networkWidget = page.locator('[data-testid="network-toggle"]')
await expect(networkWidget).toBeVisible()
await networkWidget.hover()
// (First check) Expect the network to be up
await expect(networkToggle).toContainText('Connected')
const networkPopover = page.locator('[data-testid="network-popover"]')
await expect(networkPopover).not.toBeVisible()
// Click the network widget
await networkWidget.click()
// (First check) Expect the network to be up
await expect(
networkToggleConnectedText.or(networkToggleWeakText)
).toBeVisible()
// Check the modal opened.
await expect(networkPopover).toBeVisible()
// Click the network widget
await networkWidget.click()
// Click off the modal.
await page.mouse.click(100, 100)
await expect(networkPopover).not.toBeVisible()
// Check the modal opened.
await expect(networkPopover).toBeVisible()
// Turn off the network
await u.emulateNetworkConditions({
offline: true,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
// Click off the modal.
await page.mouse.click(100, 100)
await expect(networkPopover).not.toBeVisible()
// Expect the network to be down
await expect(networkToggle).toContainText('Problem')
// Turn off the network
await u.emulateNetworkConditions({
offline: true,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
// Click the network widget
await networkWidget.click()
// Expect the network to be down
await expect(networkToggle).toContainText('Problem')
// Check the modal opened.
await expect(networkPopover).toBeVisible()
// Click the network widget
await networkWidget.click()
// Click off the modal.
await page.mouse.click(0, 0)
await expect(networkPopover).not.toBeVisible()
// Check the modal opened.
await expect(networkPopover).toBeVisible()
// Turn back on the network
await u.emulateNetworkConditions({
offline: false,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
// Click off the modal.
await page.mouse.click(0, 0)
await expect(networkPopover).not.toBeVisible()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled({ timeout: 15000 })
// Turn back on the network
await u.emulateNetworkConditions({
offline: false,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
// (Second check) expect the network to be up
await expect(networkToggle).toContainText('Connected')
}
)
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled({ timeout: 15000 })
test(
'Engine disconnect & reconnect in sketch mode',
{ tag: '@skipLocalEngine' },
async ({ page, homePage, toolbar, scene, cmdBar }) => {
const networkToggle = page.getByTestId('network-toggle')
// (Second check) expect the network to be up
await expect(
networkToggleConnectedText.or(networkToggleWeakText)
).toBeVisible()
}
)
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio
test(
'Engine disconnect & reconnect in sketch mode',
{ tag: '@skipLocalEngine' },
async ({ page, homePage, toolbar, scene, cmdBar }) => {
const networkToggle = page.getByTestId('network-toggle')
const networkToggleConnectedText = page.getByText('Connected')
const networkToggleWeakText = page.getByText('Network health (Weak)')
await homePage.goToModelingScene()
await u.waitForPageLoad()
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio
await u.openDebugPanel()
// click on "Start Sketch" button
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(100)
await homePage.goToModelingScene()
await u.waitForPageLoad()
// select a plane
await page.mouse.click(700, 200)
await u.openDebugPanel()
// click on "Start Sketch" button
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).toHaveText(
`sketch001 = startSketchOn(XZ)`
)
await u.closeDebugPanel()
// select a plane
await page.mouse.click(700, 200)
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
await expect(page.locator('.cm-content')).toHaveText(
`@settings(defaultLengthUnit = in)sketch001 = startSketchOn(XZ)`
)
await u.closeDebugPanel()
const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await expect(page.locator('.cm-content')).toHaveText(
`sketch001 = startSketchOn(XZ)profile001 = startProfile(sketch001, at = ${commonPoints.startAt})`
)
await page.waitForTimeout(100)
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100)
const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await expect(page.locator('.cm-content')).toHaveText(
`@settings(defaultLengthUnit = in)sketch001 = startSketchOn(XZ)profile001 = startProfile(sketch001, at = ${commonPoints.startAt})`
)
await page.waitForTimeout(100)
await expect(
page.locator('.cm-content')
).toHaveText(`sketch001 = startSketchOn(XZ)profile001 = startProfile(sketch001, at = ${commonPoints.startAt})
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100)
await expect(
page.locator('.cm-content')
).toHaveText(`@settings(defaultLengthUnit = in)sketch001 = startSketchOn(XZ)profile001 = startProfile(sketch001, at = ${commonPoints.startAt})
|> xLine(length = ${commonPoints.num1})`)
// Expect the network to be up
await expect(networkToggle).toContainText('Connected')
// Expect the network to be up
await networkToggle.hover()
await expect(
networkToggleConnectedText.or(networkToggleWeakText)
).toBeVisible()
// simulate network down
await u.emulateNetworkConditions({
offline: true,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
// simulate network down
await u.emulateNetworkConditions({
offline: true,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
// Expect the network to be down
await expect(networkToggle).toContainText('Problem')
// Expect the network to be down
await networkToggle.hover()
await expect(networkToggle).toContainText('Problem')
// Ensure we are not in sketch mode
await expect(
page.getByRole('button', { name: 'Exit Sketch' })
).not.toBeVisible()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeVisible()
// Ensure we are not in sketch mode
await expect(
page.getByRole('button', { name: 'Exit Sketch' })
).not.toBeVisible()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeVisible()
// simulate network up
await u.emulateNetworkConditions({
offline: false,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
// simulate network up
await u.emulateNetworkConditions({
offline: false,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
// Wait for the app to be ready for use
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled({ timeout: 15000 })
// Wait for the app to be ready for use
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled({ timeout: 15000 })
// Expect the network to be up
await expect(networkToggle).toContainText('Connected')
await scene.settled(cmdBar)
// Expect the network to be up
await networkToggle.hover()
await expect(
networkToggleConnectedText.or(networkToggleWeakText)
).toBeVisible()
// Click off the code pane.
await page.mouse.click(100, 100)
await scene.settled(cmdBar)
// select a line
await page
.getByText(`startProfile(sketch001, at = ${commonPoints.startAt})`)
.click()
// Click off the code pane.
await page.mouse.click(100, 100)
// enter sketch again
await toolbar.editSketch()
// select a line
await page
.getByText(`startProfile(sketch001, at = ${commonPoints.startAt})`)
.click()
// Click the line tool
await page
.getByRole('button', { name: 'line Line', exact: true })
.click()
// enter sketch again
await toolbar.editSketch()
await page.waitForTimeout(150)
// Click the line tool
await page.getByRole('button', { name: 'line Line', exact: true }).click()
const camCommand: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center: { x: 109, y: 0, z: -152 },
vantage: { x: 115, y: -505, z: -152 },
up: { x: 0, y: 0, z: 1 },
},
}
const updateCamCommand: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
}
await toolbar.openPane('debug')
await u.sendCustomCmd(camCommand)
await page.waitForTimeout(100)
await u.sendCustomCmd(updateCamCommand)
await page.waitForTimeout(100)
await page.waitForTimeout(150)
// click to continue profile
await page.mouse.click(1007, 400)
await page.waitForTimeout(100)
// Ensure we can continue sketching
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect
.poll(u.normalisedEditorCode)
.toBe(`sketch001 = startSketchOn(XZ)
const camCommand: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center: { x: 109, y: 0, z: -152 },
vantage: { x: 115, y: -505, z: -152 },
up: { x: 0, y: 0, z: 1 },
},
}
const updateCamCommand: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
}
await toolbar.openPane('debug')
await u.sendCustomCmd(camCommand)
await page.waitForTimeout(100)
await u.sendCustomCmd(updateCamCommand)
await page.waitForTimeout(100)
// click to continue profile
await page.mouse.click(1007, 400)
await page.waitForTimeout(100)
// Ensure we can continue sketching
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect
.poll(u.normalisedEditorCode)
.toBe(`@settings(defaultLengthUnit = in)
sketch001 = startSketchOn(XZ)
profile001 = startProfile(sketch001, at = [12.34, -12.34])
|> xLine(length = 12.34)
|> line(end = [-12.34, 12.34])
`)
await page.waitForTimeout(100)
await page.mouse.click(startXPx, 500 - PUR * 20)
await page.waitForTimeout(100)
await page.mouse.click(startXPx, 500 - PUR * 20)
await expect
.poll(u.normalisedEditorCode)
.toBe(`sketch001 = startSketchOn(XZ)
await expect
.poll(u.normalisedEditorCode)
.toBe(`@settings(defaultLengthUnit = in)
sketch001 = startSketchOn(XZ)
profile001 = startProfile(sketch001, at = [12.34, -12.34])
|> xLine(length = 12.34)
|> line(end = [-12.34, 12.34])
@ -238,22 +259,105 @@ profile001 = startProfile(sketch001, at = [12.34, -12.34])
`)
// Unequip line tool
await page.keyboard.press('Escape')
// Make sure we didn't pop out of sketch mode.
await expect(
page.getByRole('button', { name: 'Exit Sketch' })
).toBeVisible()
await expect(
page.getByRole('button', { name: 'line Line', exact: true })
).not.toHaveAttribute('aria-pressed', 'true')
// Unequip line tool
await page.keyboard.press('Escape')
// Make sure we didn't pop out of sketch mode.
await expect(
page.getByRole('button', { name: 'Exit Sketch' })
).toBeVisible()
await expect(
page.getByRole('button', { name: 'line Line', exact: true })
).not.toHaveAttribute('aria-pressed', 'true')
// Exit sketch
await page.keyboard.press('Escape')
await expect(
page.getByRole('button', { name: 'Exit Sketch' })
).not.toBeVisible()
// Exit sketch
await page.keyboard.press('Escape')
await expect(
page.getByRole('button', { name: 'Exit Sketch' })
).not.toBeVisible()
}
)
test(
'Paused stream freezes view frame, unpause reconnect is seamless to user',
{ tag: ['@electron', '@skipLocalEngine'] },
async ({ page, homePage, scene, cmdBar, toolbar, tronApp }) => {
const networkToggle = page.getByTestId('network-toggle')
const networkToggleConnectedText = page.getByText('Connected')
const networkToggleWeakText = page.getByText('Network health (Weak)')
if (!tronApp) {
fail()
}
)
}
)
await tronApp.cleanProjectDir({
app: {
stream_idle_mode: 5000,
},
})
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`sketch001 = startSketchOn(XY)
profile001 = startProfile(sketch001, at = [0.0, 0.0])
|> line(end = [10.0, 0])
|> line(end = [0, 10.0])
|> close()`
)
})
const dim = { width: 1200, height: 500 }
await page.setBodyDimensions(dim)
await test.step('Go to modeling scene', async () => {
await homePage.goToModelingScene()
await scene.settled(cmdBar)
})
await test.step('Verify pausing behavior', async () => {
// Wait 5s + 1s to pause.
await page.waitForTimeout(6000)
// We should now be paused. To the user, it should appear we're still
// connected.
await networkToggle.hover()
await expect(
networkToggleConnectedText.or(networkToggleWeakText)
).toBeVisible()
const center = {
x: dim.width / 2,
y: dim.height / 2,
}
let probe = { x: 0, y: 0 }
// ... and the model's still visibly there
probe.x = center.x + dim.width / 100
probe.y = center.y
await scene.expectPixelColor(TEST_COLORS.GREY, probe, 15)
probe = { ...center }
// Now move the mouse around to unpause!
await circleMove(page, probe.x, probe.y, 20, 10)
// ONCE AGAIN! Check the view area hasn't changed at all.
// Check the pixel a couple times as it reconnects.
// NOTE: Remember, idle behavior is still on at this point -
// if this test takes longer than 5s shit WILL go south!
probe.x = center.x + dim.width / 100
probe.y = center.y
await scene.expectPixelColor(TEST_COLORS.GREY, probe, 15)
await page.waitForTimeout(1000)
await scene.expectPixelColor(TEST_COLORS.GREY, probe, 15)
probe = { ...center }
// Ensure we're still connected
await networkToggle.hover()
await expect(
networkToggleConnectedText.or(networkToggleWeakText)
).toBeVisible()
})
}
)
})

View File

@ -44,6 +44,8 @@ export const lowerRightMasks = (page: Page) => [
export type TestColor = [number, number, number]
export const TEST_COLORS: { [key: string]: TestColor } = {
WHITE: [249, 249, 249],
OFFWHITE: [237, 237, 237],
GREY: [142, 142, 142],
YELLOW: [255, 255, 0],
BLUE: [0, 0, 255],
DARK_MODE_BKGD: [27, 27, 27],

34
rust/Cargo.lock generated
View File

@ -535,7 +535,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -963,7 +963,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -1746,7 +1746,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
dependencies = [
"hermit-abi",
"libc",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -1815,7 +1815,7 @@ dependencies = [
[[package]]
name = "kcl-bumper"
version = "0.1.70"
version = "0.1.73"
dependencies = [
"anyhow",
"clap",
@ -1826,7 +1826,7 @@ dependencies = [
[[package]]
name = "kcl-derive-docs"
version = "0.1.70"
version = "0.1.73"
dependencies = [
"Inflector",
"anyhow",
@ -1845,7 +1845,7 @@ dependencies = [
[[package]]
name = "kcl-directory-test-macro"
version = "0.1.70"
version = "0.1.73"
dependencies = [
"proc-macro2",
"quote",
@ -1854,7 +1854,7 @@ dependencies = [
[[package]]
name = "kcl-language-server"
version = "0.2.70"
version = "0.2.73"
dependencies = [
"anyhow",
"clap",
@ -1875,7 +1875,7 @@ dependencies = [
[[package]]
name = "kcl-language-server-release"
version = "0.1.70"
version = "0.1.73"
dependencies = [
"anyhow",
"clap",
@ -1895,7 +1895,7 @@ dependencies = [
[[package]]
name = "kcl-lib"
version = "0.2.70"
version = "0.2.73"
dependencies = [
"anyhow",
"approx 0.5.1",
@ -1971,7 +1971,7 @@ dependencies = [
[[package]]
name = "kcl-python-bindings"
version = "0.3.70"
version = "0.3.73"
dependencies = [
"anyhow",
"kcl-lib",
@ -1986,7 +1986,7 @@ dependencies = [
[[package]]
name = "kcl-test-server"
version = "0.1.70"
version = "0.1.73"
dependencies = [
"anyhow",
"hyper 0.14.32",
@ -1999,7 +1999,7 @@ dependencies = [
[[package]]
name = "kcl-to-core"
version = "0.1.70"
version = "0.1.73"
dependencies = [
"anyhow",
"async-trait",
@ -2013,7 +2013,7 @@ dependencies = [
[[package]]
name = "kcl-wasm-lib"
version = "0.1.70"
version = "0.1.73"
dependencies = [
"anyhow",
"bson",
@ -2987,7 +2987,7 @@ dependencies = [
"once_cell",
"socket2",
"tracing",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -3306,7 +3306,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -3900,7 +3900,7 @@ dependencies = [
"getrandom 0.3.1",
"once_cell",
"rustix",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -4753,7 +4753,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-bumper"
version = "0.1.70"
version = "0.1.73"
edition = "2021"
repository = "https://github.com/KittyCAD/modeling-api"
rust-version = "1.76"

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-derive-docs"
description = "A tool for generating documentation from Rust derive macros"
version = "0.1.70"
version = "0.1.73"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-directory-test-macro"
description = "A tool for generating tests from a directory of kcl files"
version = "0.1.70"
version = "0.1.73"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -1,6 +1,6 @@
[package]
name = "kcl-language-server-release"
version = "0.1.70"
version = "0.1.73"
edition = "2021"
authors = ["KittyCAD Inc <kcl@kittycad.io>"]
publish = false

View File

@ -2,7 +2,7 @@
name = "kcl-language-server"
description = "A language server for KCL."
authors = ["KittyCAD Inc <kcl@kittycad.io>"]
version = "0.2.70"
version = "0.2.73"
edition = "2021"
license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-lib"
description = "KittyCAD Language implementation and tools"
version = "0.2.70"
version = "0.2.73"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -788,6 +788,7 @@ impl ArgData {
Some("Axis2d | Edge") | Some("Axis3d | Edge") => Some((index, format!(r#"{label}${{{index}:X}}"#))),
Some("Edge") => Some((index, format!(r#"{label}${{{index}:tag_or_edge_fn}}"#))),
Some("[Edge; 1+]") => Some((index, format!(r#"{label}[${{{index}:tag_or_edge_fn}}]"#))),
Some("Plane") => Some((index, format!(r#"{label}${{{}:XY}}"#, index))),
Some("string") => Some((index, format!(r#"{label}${{{}:"string"}}"#, index))),
Some("bool") => Some((index, format!(r#"{label}${{{}:false}}"#, index))),

View File

@ -1167,6 +1167,16 @@ mod tests {
assert_eq!(snippet, r#"clone(${0:part001})"#);
}
#[test]
fn get_autocomplete_snippet_offset_plane() {
let data = kcl_doc::walk_prelude();
let DocData::Fn(offset_plane_fn) = data.find_by_name("offsetPlane").unwrap() else {
panic!();
};
let snippet = offset_plane_fn.to_autocomplete_snippet();
assert_eq!(snippet, r#"offsetPlane(${0:XY}, offset = ${1:3.14})"#);
}
// We want to test the snippets we compile at lsp start.
#[test]
fn get_all_stdlib_autocomplete_snippets() {

View File

@ -426,14 +426,14 @@ impl ExecutorContext {
}
#[cfg(not(target_arch = "wasm32"))]
pub async fn new_mock() -> Self {
pub async fn new_mock(settings: Option<ExecutorSettings>) -> Self {
ExecutorContext {
engine: Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new().await.unwrap(),
)),
fs: Arc::new(FileManager::new()),
stdlib: Arc::new(StdLib::new()),
settings: Default::default(),
settings: settings.unwrap_or_default(),
context_type: ContextType::Mock,
}
}
@ -2232,7 +2232,7 @@ w = f() + f()
let result = ctx.run_with_caching(program).await.unwrap();
assert_eq!(result.variables.get("x").unwrap().as_f64().unwrap(), 2.0);
let ctx2 = ExecutorContext::new_mock().await;
let ctx2 = ExecutorContext::new_mock(None).await;
let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
let result = ctx2.run_mock(program2, true).await.unwrap();
assert_eq!(result.variables.get("z").unwrap().as_f64().unwrap(), 3.0);

View File

@ -1460,7 +1460,7 @@ mod test {
#[tokio::test(flavor = "multi_thread")]
async fn coerce_idempotent() {
let mut exec_state = ExecState::new(&crate::ExecutorContext::new_mock().await);
let mut exec_state = ExecState::new(&crate::ExecutorContext::new_mock(None).await);
let values = values(&mut exec_state);
for v in &values {
// Identity subtype
@ -1550,7 +1550,7 @@ mod test {
#[tokio::test(flavor = "multi_thread")]
async fn coerce_none() {
let mut exec_state = ExecState::new(&crate::ExecutorContext::new_mock().await);
let mut exec_state = ExecState::new(&crate::ExecutorContext::new_mock(None).await);
let none = KclValue::KclNone {
value: crate::parsing::ast::types::KclNone::new(),
meta: Vec::new(),
@ -1608,7 +1608,7 @@ mod test {
#[tokio::test(flavor = "multi_thread")]
async fn coerce_record() {
let mut exec_state = ExecState::new(&crate::ExecutorContext::new_mock().await);
let mut exec_state = ExecState::new(&crate::ExecutorContext::new_mock(None).await);
let obj0 = KclValue::Object {
value: HashMap::new(),
@ -1690,7 +1690,7 @@ mod test {
#[tokio::test(flavor = "multi_thread")]
async fn coerce_array() {
let mut exec_state = ExecState::new(&crate::ExecutorContext::new_mock().await);
let mut exec_state = ExecState::new(&crate::ExecutorContext::new_mock(None).await);
let hom_arr = KclValue::HomArray {
value: vec![
@ -1843,7 +1843,7 @@ mod test {
#[tokio::test(flavor = "multi_thread")]
async fn coerce_union() {
let mut exec_state = ExecState::new(&crate::ExecutorContext::new_mock().await);
let mut exec_state = ExecState::new(&crate::ExecutorContext::new_mock(None).await);
// Subtyping smaller unions
assert!(RuntimeType::Union(vec![]).subtype(&RuntimeType::Union(vec![
@ -1894,7 +1894,7 @@ mod test {
#[tokio::test(flavor = "multi_thread")]
async fn coerce_axes() {
let mut exec_state = ExecState::new(&crate::ExecutorContext::new_mock().await);
let mut exec_state = ExecState::new(&crate::ExecutorContext::new_mock(None).await);
// Subtyping
assert!(RuntimeType::Primitive(PrimitiveType::Axis2d).subtype(&RuntimeType::Primitive(PrimitiveType::Axis2d)));
@ -2009,7 +2009,7 @@ mod test {
#[tokio::test(flavor = "multi_thread")]
async fn coerce_numeric() {
let mut exec_state = ExecState::new(&crate::ExecutorContext::new_mock().await);
let mut exec_state = ExecState::new(&crate::ExecutorContext::new_mock(None).await);
let count = KclValue::Number {
value: 1.0,
@ -2237,7 +2237,7 @@ d = cos(30)
#[tokio::test(flavor = "multi_thread")]
async fn coerce_nested_array() {
let mut exec_state = ExecState::new(&crate::ExecutorContext::new_mock().await);
let mut exec_state = ExecState::new(&crate::ExecutorContext::new_mock(None).await);
let mixed1 = KclValue::HomArray {
value: vec![

View File

@ -4220,8 +4220,8 @@ sketch001 = startSketchOn(XY)
result,
vec![tower_lsp::lsp_types::ColorInformation {
range: tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position { line: 4, character: 24 },
end: tower_lsp::lsp_types::Position { line: 4, character: 33 },
start: tower_lsp::lsp_types::Position { line: 4, character: 25 },
end: tower_lsp::lsp_types::Position { line: 4, character: 32 },
},
color: tower_lsp::lsp_types::Color {
red: 1.0,
@ -4272,8 +4272,8 @@ sketch001 = startSketchOn(XY)
result,
vec![tower_lsp::lsp_types::ColorInformation {
range: tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position { line: 4, character: 24 },
end: tower_lsp::lsp_types::Position { line: 4, character: 33 },
start: tower_lsp::lsp_types::Position { line: 4, character: 25 },
end: tower_lsp::lsp_types::Position { line: 4, character: 32 },
},
color: tower_lsp::lsp_types::Color {
red: 1.0,
@ -4291,8 +4291,8 @@ sketch001 = startSketchOn(XY)
uri: "file:///test.kcl".try_into().unwrap(),
},
range: tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position { line: 4, character: 24 },
end: tower_lsp::lsp_types::Position { line: 4, character: 33 },
start: tower_lsp::lsp_types::Position { line: 4, character: 25 },
end: tower_lsp::lsp_types::Position { line: 4, character: 32 },
},
color: tower_lsp::lsp_types::Color {
red: 1.0,
@ -4316,3 +4316,64 @@ sketch001 = startSketchOn(XY)
}]
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_kcl_lsp_diagnostic_compilation_warnings() {
let server = kcl_lsp_server(false).await.unwrap();
// Send open file.
server
.did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams {
text_document: tower_lsp::lsp_types::TextDocumentItem {
uri: "file:///test.kcl".try_into().unwrap(),
language_id: "kcl".to_string(),
version: 1,
text: r#"foo = 42
@settings(defaultLengthUnit = mm)"#
.to_string(),
},
})
.await;
// Send diagnostics request.
let diagnostics = server
.diagnostic(tower_lsp::lsp_types::DocumentDiagnosticParams {
text_document: tower_lsp::lsp_types::TextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(),
},
partial_result_params: Default::default(),
work_done_progress_params: Default::default(),
identifier: None,
previous_result_id: None,
})
.await
.unwrap();
// Check the diagnostics.
if let tower_lsp::lsp_types::DocumentDiagnosticReportResult::Report(diagnostics) = diagnostics {
if let tower_lsp::lsp_types::DocumentDiagnosticReport::Full(diagnostics) = diagnostics {
assert_eq!(diagnostics.full_document_diagnostic_report.items.len(), 1);
assert_eq!(
diagnostics.full_document_diagnostic_report.items[0],
tower_lsp::lsp_types::Diagnostic {
range: tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position { line: 0, character: 8 },
end: tower_lsp::lsp_types::Position { line: 1, character: 33 },
},
severity: Some(tower_lsp::lsp_types::DiagnosticSeverity::WARNING),
code: None,
source: Some("kcl".to_string()),
message: "Named attributes should appear before any declarations or expressions.\n\nBecause named attributes apply to the whole function or module, including code written before them, it can be confusing for readers to not have these attributes at the top of code blocks.".to_string(),
related_information: None,
tags: None,
data: None,
code_description: None,
}
);
} else {
panic!("Expected full diagnostics");
}
} else {
panic!("Expected diagnostics");
}
}

View File

@ -96,6 +96,7 @@ pub(crate) fn read_std(mod_name: &str) -> Option<&'static str> {
"solid" => Some(include_str!("../std/solid.kcl")),
"units" => Some(include_str!("../std/units.kcl")),
"array" => Some(include_str!("../std/array.kcl")),
"sweep" => Some(include_str!("../std/sweep.kcl")),
"transform" => Some(include_str!("../std/transform.kcl")),
_ => None,
}

View File

@ -438,8 +438,15 @@ impl Node<Program> {
let add_color = |literal: &Node<Literal>| {
// Check if the string is a color.
if let Some(c) = literal.value.is_color() {
let source_range = literal.as_source_range();
// We subtract 1 from either side because of the "'s in the literal.
let fixed_source_range = SourceRange::new(
source_range.start() + 1,
source_range.end() - 1,
source_range.module_id(),
);
let color = ColorInformation {
range: literal.as_source_range().to_lsp_range(code),
range: fixed_source_range.to_lsp_range(code),
color: tower_lsp::lsp_types::Color {
red: c.r,
green: c.g,
@ -498,7 +505,11 @@ impl Node<Program> {
crate::walk::walk(self, |node: crate::walk::Node<'a>| {
match node {
crate::walk::Node::Literal(literal) => {
if literal.start == pos_start && literal.end == pos_end && literal.value.is_color().is_some() {
// Account for the quotes in the literal.
if (literal.start + 1) == pos_start
&& (literal.end - 1) == pos_end
&& literal.value.is_color().is_some()
{
found.replace(true);
return Ok(true);
}

View File

@ -2748,7 +2748,6 @@ mod import_mesh_clone {
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
#[ignore = "turn on when katie fixes the mesh import"]
async fn kcl_test_execute() {
super::execute(TEST_NAME, true).await
}

View File

@ -674,7 +674,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread")]
async fn test_array_to_point3d() {
let mut exec_state = ExecState::new(&ExecutorContext::new_mock().await);
let mut exec_state = ExecState::new(&ExecutorContext::new_mock(None).await);
let input = KclValue::HomArray {
value: vec![
KclValue::Number {
@ -706,7 +706,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread")]
async fn test_tuple_to_point3d() {
let mut exec_state = ExecState::new(&ExecutorContext::new_mock().await);
let mut exec_state = ExecState::new(&ExecutorContext::new_mock(None).await);
let input = KclValue::Tuple {
value: vec![
KclValue::Number {

View File

@ -112,7 +112,7 @@ pub async fn sweep(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
/// // Create a spring by sweeping around the helix path.
/// springSketch = startSketchOn(YZ)
/// |> circle( center = [0, 0], radius = 1)
/// |> sweep(path = helixPath)
/// |> sweep(path = helixPath, relativeTo = "sketchPlane")
/// ```
///
/// ```no_run
@ -167,7 +167,7 @@ pub async fn sweep(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
path = { docs = "The path to sweep the sketch along" },
sectional = { docs = "If true, the sweep will be broken up into sub-sweeps (extrusions, revolves, sweeps) based on the trajectory path components." },
tolerance = { docs = "Tolerance for this operation" },
relative_to = { docs = "What is the sweep relative to? Can be either 'sketchPlane' or 'trajectoryCurve'. Defaults to sketchPlane."},
relative_to = { docs = "What is the sweep relative to? Can be either 'sketchPlane' or 'trajectoryCurve'. Defaults to trajectoryCurve."},
tag_start = { docs = "A named tag for the face at the start of the sweep, i.e. the original sketch" },
tag_end = { docs = "A named tag for the face at the end of the sweep" },
},
@ -191,14 +191,13 @@ async fn inner_sweep(
};
let relative_to = match relative_to.as_deref() {
Some("sketchPlane") => RelativeTo::SketchPlane,
Some("trajectoryCurve") => RelativeTo::TrajectoryCurve,
Some("trajectoryCurve") | None => RelativeTo::TrajectoryCurve,
Some(_) => {
return Err(KclError::Syntax(crate::errors::KclErrorDetails {
source_ranges: vec![args.source_range],
message: "If you provide relativeTo, it must either be 'sketchPlane' or 'trajectoryCurve'".to_owned(),
}))
}
None => RelativeTo::default(),
};
let mut solids = Vec::new();

View File

@ -267,7 +267,7 @@ import \"a.kcl\"
);
modules.insert("b.kcl".to_owned(), into_module_info(b));
let ctx = ExecutorContext::new_mock().await;
let ctx = ExecutorContext::new_mock(None).await;
let order = import_graph(&modules, &ctx).unwrap();
assert_eq!(vec![vec!["a.kcl".to_owned()], vec!["b.kcl".to_owned()]], order);
}
@ -290,7 +290,7 @@ x = 1
);
modules.insert("b.kcl".to_owned(), into_module_info(b));
let ctx = ExecutorContext::new_mock().await;
let ctx = ExecutorContext::new_mock(None).await;
let order = import_graph(&modules, &ctx).unwrap();
assert_eq!(vec![vec!["a.kcl".to_owned(), "b.kcl".to_owned()]], order);
}
@ -316,7 +316,7 @@ import \"a.kcl\"
);
modules.insert("c.kcl".to_owned(), into_module_info(c));
let ctx = ExecutorContext::new_mock().await;
let ctx = ExecutorContext::new_mock(None).await;
let order = import_graph(&modules, &ctx).unwrap();
assert_eq!(
vec![vec!["a.kcl".to_owned()], vec!["b.kcl".to_owned(), "c.kcl".to_owned()]],
@ -342,7 +342,7 @@ import \"a.kcl\"
);
modules.insert("b.kcl".to_owned(), into_module_info(b));
let ctx = ExecutorContext::new_mock().await;
let ctx = ExecutorContext::new_mock(None).await;
import_graph(&modules, &ctx).unwrap_err();
}
}

View File

@ -21,6 +21,7 @@ export import * from "std::sketch"
export import * from "std::solid"
export import * from "std::transform"
export import "std::turns"
export import "std::sweep"
/// An abstract 3d plane aligned with the X and Y axes. Its normal is the positive Z axis.
export XY = {
@ -83,7 +84,7 @@ export END = 'end'
/// // Create a spring by sweeping around the helix path.
/// springSketch = startSketchOn(YZ)
/// |> circle( center = [0, 0], radius = 0.5)
/// |> sweep(path = helixPath)
/// |> sweep(path = helixPath, relativeTo = sweep::SKETCH_PLANE)
/// ```
///
/// ```
@ -104,7 +105,7 @@ export END = 'end'
/// // Create a spring by sweeping around the helix path.
/// springSketch = startSketchOn(XY)
/// |> circle( center = [0, 0], radius = 0.5 )
/// |> sweep(path = helixPath)
/// |> sweep(path = helixPath, relativeTo = sweep::SKETCH_PLANE)
/// ```
///
/// ```
@ -124,7 +125,7 @@ export END = 'end'
/// // Create a spring by sweeping around the helix path.
/// springSketch = startSketchOn(XY)
/// |> circle( center = [0, 0], radius = 1 )
/// |> sweep(path = helixPath)
/// |> sweep(path = helixPath, relativeTo = sweep::SKETCH_PLANE)
/// ```
///
/// ```
@ -413,7 +414,7 @@ export fn offsetPlane(
/// // Create a spring by sweeping around the helix path.
/// sweepedSpring = clone(springSketch)
/// |> translate(x=100)
/// |> sweep(path = helixPath)
/// |> sweep(path = helixPath, relativeTo = sweep::SKETCH_PLANE)
/// ```
///
/// ```kcl

View File

@ -0,0 +1,5 @@
/// Local/relative to the trajectory curve
export TRAJECTORY = 'trajectoryCurve'
/// Local/relative to a position centered within the plane being sketched on
export SKETCH_PLANE = 'sketchPlane'

View File

@ -996,10 +996,51 @@ description: Artifact commands import_mesh_clone.kcl
"direction": "positive"
}
},
"units": "mm"
"units": "m"
}
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "set_object_transform",
"object_id": "[uuid]",
"transforms": [
{
"translate": {
"property": {
"x": -2000.0,
"y": -2000.0,
"z": 0.0
},
"set": false,
"is_local": true
},
"rotate_rpy": null,
"rotate_angle_axis": null,
"scale": null
}
]
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "object_set_material_params_pbr",
"object_id": "[uuid]",
"color": {
"r": 1.0,
"g": 1.0,
"b": 0.0,
"a": 100.0
},
"metalness": 0.0,
"roughness": 0.0,
"ambient_occlusion": 0.0
}
},
{
"cmdId": "[uuid]",
"range": [],
@ -1008,6 +1049,22 @@ description: Artifact commands import_mesh_clone.kcl
"entity_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "entity_get_all_child_uuids",
"entity_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "entity_get_all_child_uuids",
"entity_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
@ -1018,7 +1075,7 @@ description: Artifact commands import_mesh_clone.kcl
{
"translate": {
"property": {
"x": 1020.0,
"x": 4000.0,
"y": 0.0,
"z": 0.0
},
@ -1044,8 +1101,138 @@ description: Artifact commands import_mesh_clone.kcl
"b": 0.0,
"a": 100.0
},
"metalness": 0.5,
"roughness": 0.5,
"metalness": 0.0,
"roughness": 0.0,
"ambient_occlusion": 0.0
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "entity_clone",
"entity_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "entity_get_all_child_uuids",
"entity_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "entity_get_all_child_uuids",
"entity_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "set_object_transform",
"object_id": "[uuid]",
"transforms": [
{
"translate": {
"property": {
"x": 0.0,
"y": 4000.0,
"z": 0.0
},
"set": false,
"is_local": true
},
"rotate_rpy": null,
"rotate_angle_axis": null,
"scale": null
}
]
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "object_set_material_params_pbr",
"object_id": "[uuid]",
"color": {
"r": 0.0,
"g": 1.0,
"b": 0.0,
"a": 100.0
},
"metalness": 0.0,
"roughness": 0.0,
"ambient_occlusion": 0.0
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "entity_clone",
"entity_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "entity_get_all_child_uuids",
"entity_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "entity_get_all_child_uuids",
"entity_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "set_object_transform",
"object_id": "[uuid]",
"transforms": [
{
"translate": {
"property": {
"x": 0.0,
"y": 4000.0,
"z": 0.0
},
"set": false,
"is_local": true
},
"rotate_rpy": null,
"rotate_angle_axis": null,
"scale": null
}
]
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "object_set_material_params_pbr",
"object_id": "[uuid]",
"color": {
"r": 0.0,
"g": 0.0,
"b": 1.0,
"a": 100.0
},
"metalness": 0.0,
"roughness": 0.0,
"ambient_occlusion": 0.0
}
}

View File

@ -8,6 +8,46 @@ description: Result of parsing import_mesh_clone.kcl
{
"commentStart": 0,
"end": 0,
"outerAttrs": [
{
"commentStart": 0,
"end": 0,
"name": null,
"properties": [
{
"commentStart": 0,
"end": 0,
"key": {
"commentStart": 0,
"end": 0,
"name": "lengthUnit",
"start": 0,
"type": "Identifier"
},
"start": 0,
"type": "ObjectProperty",
"value": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "m",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name",
"type": "Name"
}
}
],
"start": 0,
"type": "Annotation"
}
],
"path": {
"type": "Foreign",
"path": "../inputs/cube.obj"
@ -17,7 +57,7 @@ description: Result of parsing import_mesh_clone.kcl
"alias": {
"commentStart": 0,
"end": 0,
"name": "cube",
"name": "yellow",
"start": 0,
"type": "Identifier"
}
@ -28,24 +68,180 @@ description: Result of parsing import_mesh_clone.kcl
},
{
"commentStart": 0,
"declaration": {
"end": 0,
"expression": {
"body": [
{
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "yellow",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name",
"type": "Name"
},
{
"arguments": [
{
"type": "LabeledArg",
"label": {
"commentStart": 0,
"end": 0,
"name": "x",
"start": 0,
"type": "Identifier"
},
"arg": {
"argument": {
"commentStart": 0,
"end": 0,
"raw": "2000",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 2000.0,
"suffix": "None"
}
},
"commentStart": 0,
"end": 0,
"operator": "-",
"start": 0,
"type": "UnaryExpression",
"type": "UnaryExpression"
}
},
{
"type": "LabeledArg",
"label": {
"commentStart": 0,
"end": 0,
"name": "y",
"start": 0,
"type": "Identifier"
},
"arg": {
"argument": {
"commentStart": 0,
"end": 0,
"raw": "2000",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 2000.0,
"suffix": "None"
}
},
"commentStart": 0,
"end": 0,
"operator": "-",
"start": 0,
"type": "UnaryExpression",
"type": "UnaryExpression"
}
}
],
"callee": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "translate",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name"
},
"commentStart": 0,
"end": 0,
"start": 0,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": {
"commentStart": 0,
"end": 0,
"start": 0,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
}
}
],
"commentStart": 0,
"end": 0,
"id": {
"commentStart": 0,
"end": 0,
"name": "model",
"start": 0,
"type": "Identifier"
},
"init": {
"start": 0,
"type": "PipeExpression",
"type": "PipeExpression"
},
"start": 0,
"type": "ExpressionStatement",
"type": "ExpressionStatement"
},
{
"commentStart": 0,
"end": 0,
"expression": {
"arguments": [
{
"type": "LabeledArg",
"label": {
"commentStart": 0,
"end": 0,
"name": "color",
"start": 0,
"type": "Identifier"
},
"arg": {
"commentStart": 0,
"end": 0,
"raw": "\"#ffff00\"",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": "#ffff00"
}
}
],
"callee": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "cube",
"name": "appearance",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name"
},
"commentStart": 0,
"end": 0,
"start": 0,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "yellow",
"start": 0,
"type": "Identifier"
},
@ -53,15 +249,11 @@ description: Result of parsing import_mesh_clone.kcl
"start": 0,
"type": "Name",
"type": "Name"
},
"start": 0,
"type": "VariableDeclarator"
}
},
"end": 0,
"kind": "const",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
"type": "ExpressionStatement",
"type": "ExpressionStatement"
},
{
"commentStart": 0,
@ -71,7 +263,7 @@ description: Result of parsing import_mesh_clone.kcl
"id": {
"commentStart": 0,
"end": 0,
"name": "model2",
"name": "red",
"start": 0,
"type": "Identifier"
},
@ -105,7 +297,7 @@ description: Result of parsing import_mesh_clone.kcl
"name": {
"commentStart": 0,
"end": 0,
"name": "model",
"name": "yellow",
"start": 0,
"type": "Identifier"
},
@ -129,12 +321,12 @@ description: Result of parsing import_mesh_clone.kcl
"arg": {
"commentStart": 0,
"end": 0,
"raw": "1020",
"raw": "4000",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 1020.0,
"value": 4000.0,
"suffix": "None"
}
}
@ -160,95 +352,13 @@ description: Result of parsing import_mesh_clone.kcl
"start": 0,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": null
},
{
"arguments": [
{
"type": "LabeledArg",
"label": {
"commentStart": 0,
"end": 0,
"name": "color",
"start": 0,
"type": "Identifier"
},
"arg": {
"commentStart": 0,
"end": 0,
"raw": "\"#ff0000\"",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": "#ff0000"
}
},
{
"type": "LabeledArg",
"label": {
"commentStart": 0,
"end": 0,
"name": "metalness",
"start": 0,
"type": "Identifier"
},
"arg": {
"commentStart": 0,
"end": 0,
"raw": "50",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 50.0,
"suffix": "None"
}
}
},
{
"type": "LabeledArg",
"label": {
"commentStart": 0,
"end": 0,
"name": "roughness",
"start": 0,
"type": "Identifier"
},
"arg": {
"commentStart": 0,
"end": 0,
"raw": "50",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 50.0,
"suffix": "None"
}
}
}
],
"callee": {
"abs_path": false,
"unlabeled": {
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "appearance",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name"
},
"commentStart": 0,
"end": 0,
"start": 0,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": null
"type": "PipeSubstitution",
"type": "PipeSubstitution"
}
}
],
"commentStart": 0,
@ -265,6 +375,446 @@ description: Result of parsing import_mesh_clone.kcl
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"commentStart": 0,
"end": 0,
"expression": {
"arguments": [
{
"type": "LabeledArg",
"label": {
"commentStart": 0,
"end": 0,
"name": "color",
"start": 0,
"type": "Identifier"
},
"arg": {
"commentStart": 0,
"end": 0,
"raw": "\"#ff0000\"",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": "#ff0000"
}
}
],
"callee": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "appearance",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name"
},
"commentStart": 0,
"end": 0,
"start": 0,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "red",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name",
"type": "Name"
}
},
"start": 0,
"type": "ExpressionStatement",
"type": "ExpressionStatement"
},
{
"commentStart": 0,
"declaration": {
"commentStart": 0,
"end": 0,
"id": {
"commentStart": 0,
"end": 0,
"name": "green",
"start": 0,
"type": "Identifier"
},
"init": {
"body": [
{
"callee": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "clone",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name"
},
"commentStart": 0,
"end": 0,
"start": 0,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "yellow",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name",
"type": "Name"
}
},
{
"arguments": [
{
"type": "LabeledArg",
"label": {
"commentStart": 0,
"end": 0,
"name": "y",
"start": 0,
"type": "Identifier"
},
"arg": {
"commentStart": 0,
"end": 0,
"raw": "4000",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 4000.0,
"suffix": "None"
}
}
}
],
"callee": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "translate",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name"
},
"commentStart": 0,
"end": 0,
"start": 0,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": {
"commentStart": 0,
"end": 0,
"start": 0,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
}
}
],
"commentStart": 0,
"end": 0,
"start": 0,
"type": "PipeExpression",
"type": "PipeExpression"
},
"start": 0,
"type": "VariableDeclarator"
},
"end": 0,
"kind": "const",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"commentStart": 0,
"end": 0,
"expression": {
"arguments": [
{
"type": "LabeledArg",
"label": {
"commentStart": 0,
"end": 0,
"name": "color",
"start": 0,
"type": "Identifier"
},
"arg": {
"commentStart": 0,
"end": 0,
"raw": "\"#00ff00\"",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": "#00ff00"
}
}
],
"callee": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "appearance",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name"
},
"commentStart": 0,
"end": 0,
"start": 0,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "green",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name",
"type": "Name"
}
},
"start": 0,
"type": "ExpressionStatement",
"type": "ExpressionStatement"
},
{
"commentStart": 0,
"declaration": {
"commentStart": 0,
"end": 0,
"id": {
"commentStart": 0,
"end": 0,
"name": "blue",
"start": 0,
"type": "Identifier"
},
"init": {
"body": [
{
"callee": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "clone",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name"
},
"commentStart": 0,
"end": 0,
"start": 0,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "red",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name",
"type": "Name"
}
},
{
"arguments": [
{
"type": "LabeledArg",
"label": {
"commentStart": 0,
"end": 0,
"name": "y",
"start": 0,
"type": "Identifier"
},
"arg": {
"commentStart": 0,
"end": 0,
"raw": "4000",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 4000.0,
"suffix": "None"
}
}
}
],
"callee": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "translate",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name"
},
"commentStart": 0,
"end": 0,
"start": 0,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": {
"commentStart": 0,
"end": 0,
"start": 0,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
}
}
],
"commentStart": 0,
"end": 0,
"start": 0,
"type": "PipeExpression",
"type": "PipeExpression"
},
"start": 0,
"type": "VariableDeclarator"
},
"end": 0,
"kind": "const",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"commentStart": 0,
"end": 0,
"expression": {
"arguments": [
{
"type": "LabeledArg",
"label": {
"commentStart": 0,
"end": 0,
"name": "color",
"start": 0,
"type": "Identifier"
},
"arg": {
"commentStart": 0,
"end": 0,
"raw": "\"#0000ff\"",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": "#0000ff"
}
}
],
"callee": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "appearance",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name"
},
"commentStart": 0,
"end": 0,
"start": 0,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "blue",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name",
"type": "Name"
}
},
"start": 0,
"type": "ExpressionStatement",
"type": "ExpressionStatement"
}
],
"commentStart": 0,
@ -282,7 +832,29 @@ description: Result of parsing import_mesh_clone.kcl
}
}
],
"1": [
"2": [
{
"commentStart": 0,
"end": 0,
"start": 0,
"type": "NonCodeNode",
"value": {
"type": "newLine"
}
}
],
"4": [
{
"commentStart": 0,
"end": 0,
"start": 0,
"type": "NonCodeNode",
"value": {
"type": "newLine"
}
}
],
"6": [
{
"commentStart": 0,
"end": 0,

View File

@ -1,10 +0,0 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Error from executing import_mesh_clone.kcl
---
KCL Engine error
× engine: Modeling command failed: websocket closed early
╭────
13 │ )
╰────

View File

@ -1,13 +1,18 @@
import "../inputs/cube.obj" as cube
@(lengthUnit = m)
import "../inputs/cube.obj" as yellow
model = cube
yellow
|> translate(%, x = -2000, y = -2000)
appearance(yellow, color = "#ffff00")
model2 = clone(model)
|> translate(
x = 1020,
)
|> appearance(
color = "#ff0000",
metalness = 50,
roughness = 50
)
red = clone(yellow)
|> translate(%, x = 4000)
appearance(red, color = "#ff0000")
green = clone(yellow)
|> translate(%, y = 4000)
appearance(green, color = "#00ff00")
blue = clone(red)
|> translate(%, y = 4000)
appearance(blue, color = "#0000ff")

View File

@ -25,6 +25,32 @@ description: Operations executed import_mesh_clone.kcl
"labeledArgs": {},
"sourceRange": []
},
{
"type": "KclStdLibCall",
"name": "clone",
"unlabeledArg": {
"value": {
"type": "ImportedGeometry",
"artifact_id": "[uuid]"
},
"sourceRange": []
},
"labeledArgs": {},
"sourceRange": []
},
{
"type": "KclStdLibCall",
"name": "clone",
"unlabeledArg": {
"value": {
"type": "ImportedGeometry",
"artifact_id": "[uuid]"
},
"sourceRange": []
},
"labeledArgs": {},
"sourceRange": []
},
{
"type": "GroupEnd"
}

View File

@ -0,0 +1,31 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Variables in memory after executing import_mesh_clone.kcl
---
{
"blue": {
"type": "ImportedGeometry",
"id": "[uuid]",
"value": [
"cube.obj"
]
},
"green": {
"type": "ImportedGeometry",
"id": "[uuid]",
"value": [
"cube.obj"
]
},
"red": {
"type": "ImportedGeometry",
"id": "[uuid]",
"value": [
"cube.obj"
]
},
"yellow": {
"type": "Module",
"value": 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

@ -2,10 +2,21 @@
source: kcl-lib/src/simulation_tests.rs
description: Result of unparsing import_mesh_clone.kcl
---
import "../inputs/cube.obj" as cube
@(lengthUnit = m)
import "../inputs/cube.obj" as yellow
model = cube
yellow
|> translate(%, x = -2000, y = -2000)
appearance(yellow, color = "#ffff00")
model2 = clone(model)
|> translate(x = 1020)
|> appearance(color = "#ff0000", metalness = 50, roughness = 50)
red = clone(yellow)
|> translate(%, x = 4000)
appearance(red, color = "#ff0000")
green = clone(yellow)
|> translate(%, y = 4000)
appearance(green, color = "#00ff00")
blue = clone(red)
|> translate(%, y = 4000)
appearance(blue, color = "#0000ff")

View File

@ -5122,7 +5122,7 @@ description: Artifact commands bench.kcl
"trajectory": "[uuid]",
"sectional": false,
"tolerance": 0.0000001,
"relative_to": "sketch_plane"
"relative_to": "trajectory_curve"
}
},
{
@ -5134,7 +5134,7 @@ description: Artifact commands bench.kcl
"trajectory": "[uuid]",
"sectional": false,
"tolerance": 0.0000001,
"relative_to": "sketch_plane"
"relative_to": "trajectory_curve"
}
}
]

View File

@ -906,7 +906,7 @@ description: Artifact commands cold-plate.kcl
"trajectory": "[uuid]",
"sectional": false,
"tolerance": 0.0000001,
"relative_to": "sketch_plane"
"relative_to": "trajectory_curve"
}
},
{

View File

@ -5576,7 +5576,7 @@ description: Artifact commands cpu-cooler.kcl
"trajectory": "[uuid]",
"sectional": false,
"tolerance": 0.0000001,
"relative_to": "sketch_plane"
"relative_to": "trajectory_curve"
}
},
{
@ -6111,7 +6111,7 @@ description: Artifact commands cpu-cooler.kcl
"trajectory": "[uuid]",
"sectional": false,
"tolerance": 0.0000001,
"relative_to": "sketch_plane"
"relative_to": "trajectory_curve"
}
},
{
@ -9469,7 +9469,7 @@ description: Artifact commands cpu-cooler.kcl
"trajectory": "[uuid]",
"sectional": false,
"tolerance": 0.0000001,
"relative_to": "sketch_plane"
"relative_to": "trajectory_curve"
}
},
{
@ -9601,7 +9601,7 @@ description: Artifact commands cpu-cooler.kcl
"trajectory": "[uuid]",
"sectional": false,
"tolerance": 0.0000001,
"relative_to": "sketch_plane"
"relative_to": "trajectory_curve"
}
},
{
@ -10120,7 +10120,7 @@ description: Artifact commands cpu-cooler.kcl
"trajectory": "[uuid]",
"sectional": false,
"tolerance": 0.0000001,
"relative_to": "sketch_plane"
"relative_to": "trajectory_curve"
}
},
{
@ -10252,7 +10252,7 @@ description: Artifact commands cpu-cooler.kcl
"trajectory": "[uuid]",
"sectional": false,
"tolerance": 0.0000001,
"relative_to": "sketch_plane"
"relative_to": "trajectory_curve"
}
},
{

View File

@ -1598,7 +1598,7 @@ description: Artifact commands exhaust-manifold.kcl
"trajectory": "[uuid]",
"sectional": false,
"tolerance": 0.0000001,
"relative_to": "sketch_plane"
"relative_to": "trajectory_curve"
}
},
{
@ -1610,7 +1610,7 @@ description: Artifact commands exhaust-manifold.kcl
"trajectory": "[uuid]",
"sectional": false,
"tolerance": 0.0000001,
"relative_to": "sketch_plane"
"relative_to": "trajectory_curve"
}
},
{
@ -1622,7 +1622,7 @@ description: Artifact commands exhaust-manifold.kcl
"trajectory": "[uuid]",
"sectional": false,
"tolerance": 0.0000001,
"relative_to": "sketch_plane"
"relative_to": "trajectory_curve"
}
},
{
@ -1634,7 +1634,7 @@ description: Artifact commands exhaust-manifold.kcl
"trajectory": "[uuid]",
"sectional": false,
"tolerance": 0.0000001,
"relative_to": "sketch_plane"
"relative_to": "trajectory_curve"
}
},
{

View File

@ -4491,7 +4491,7 @@ description: Artifact commands utility-sink.kcl
"trajectory": "[uuid]",
"sectional": false,
"tolerance": 0.0000001,
"relative_to": "sketch_plane"
"relative_to": "trajectory_curve"
}
},
{

View File

@ -418,7 +418,7 @@ description: Artifact commands subtract_regression03.kcl
"trajectory": "[uuid]",
"sectional": false,
"tolerance": 0.0000001,
"relative_to": "sketch_plane"
"relative_to": "trajectory_curve"
}
},
{

View File

@ -395,7 +395,7 @@ description: Artifact commands subtract_regression05.kcl
"trajectory": "[uuid]",
"sectional": false,
"tolerance": 0.0000001,
"relative_to": "sketch_plane"
"relative_to": "trajectory_curve"
}
},
{

View File

@ -1,6 +1,6 @@
[package]
name = "kcl-python-bindings"
version = "0.3.70"
version = "0.3.73"
edition = "2021"
repository = "https://github.com/kittycad/modeling-app"
exclude = ["tests/*", "files/*", "venv/*"]

View File

@ -217,16 +217,48 @@ async fn get_code_and_file_path(path: &str) -> Result<(String, std::path::PathBu
Ok((code, path))
}
async fn new_context_state(current_file: Option<std::path::PathBuf>) -> Result<(ExecutorContext, kcl_lib::ExecState)> {
async fn new_context_state(
current_file: Option<std::path::PathBuf>,
mock: bool,
) -> Result<(ExecutorContext, kcl_lib::ExecState)> {
let mut settings: kcl_lib::ExecutorSettings = Default::default();
if let Some(current_file) = current_file {
settings.with_current_file(kcl_lib::TypedPath(current_file));
}
let ctx = ExecutorContext::new_with_client(settings, None, None).await?;
let ctx = if mock {
ExecutorContext::new_mock(Some(settings)).await
} else {
ExecutorContext::new_with_client(settings, None, None).await?
};
let state = kcl_lib::ExecState::new(&ctx);
Ok((ctx, state))
}
/// Parse the kcl code from a file path.
#[pyfunction]
async fn parse(path: String) -> PyResult<bool> {
tokio()
.spawn(async move {
let (code, path) = get_code_and_file_path(&path)
.await
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?;
let _program = kcl_lib::Program::parse_no_errs(&code)
.map_err(|err| into_miette_for_parse(&path.display().to_string(), &code, err))?;
Ok(true)
})
.await
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?
}
/// Parse the kcl code.
#[pyfunction]
fn parse_code(code: String) -> PyResult<bool> {
let _program = kcl_lib::Program::parse_no_errs(&code).map_err(|err| into_miette_for_parse("", &code, err))?;
Ok(true)
}
/// Execute the kcl code from a file path.
#[pyfunction]
async fn execute(path: String) -> PyResult<()> {
@ -238,7 +270,7 @@ async fn execute(path: String) -> PyResult<()> {
let program = kcl_lib::Program::parse_no_errs(&code)
.map_err(|err| into_miette_for_parse(&path.display().to_string(), &code, err))?;
let (ctx, mut state) = new_context_state(Some(path))
let (ctx, mut state) = new_context_state(Some(path), false)
.await
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?;
// Execute the program.
@ -260,7 +292,7 @@ async fn execute_code(code: String) -> PyResult<()> {
let program =
kcl_lib::Program::parse_no_errs(&code).map_err(|err| into_miette_for_parse("", &code, err))?;
let (ctx, mut state) = new_context_state(None)
let (ctx, mut state) = new_context_state(None, false)
.await
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?;
// Execute the program.
@ -274,6 +306,53 @@ async fn execute_code(code: String) -> PyResult<()> {
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?
}
/// Mock execute the kcl code.
#[pyfunction]
async fn mock_execute_code(code: String) -> PyResult<bool> {
tokio()
.spawn(async move {
let program =
kcl_lib::Program::parse_no_errs(&code).map_err(|err| into_miette_for_parse("", &code, err))?;
let (ctx, mut state) = new_context_state(None, true)
.await
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?;
// Execute the program.
ctx.run(&program, &mut state)
.await
.map_err(|err| into_miette(err, &code))?;
Ok(true)
})
.await
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?
}
/// Mock execute the kcl code from a file path.
#[pyfunction]
async fn mock_execute(path: String) -> PyResult<bool> {
tokio()
.spawn(async move {
let (code, path) = get_code_and_file_path(&path)
.await
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?;
let program = kcl_lib::Program::parse_no_errs(&code)
.map_err(|err| into_miette_for_parse(&path.display().to_string(), &code, err))?;
let (ctx, mut state) = new_context_state(Some(path), true)
.await
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?;
// Execute the program.
ctx.run(&program, &mut state)
.await
.map_err(|err| into_miette(err, &code))?;
Ok(true)
})
.await
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?
}
/// Execute a kcl file and snapshot it in a specific format.
#[pyfunction]
async fn execute_and_snapshot(path: String, image_format: ImageFormat) -> PyResult<Vec<u8>> {
@ -285,7 +364,7 @@ async fn execute_and_snapshot(path: String, image_format: ImageFormat) -> PyResu
let program = kcl_lib::Program::parse_no_errs(&code)
.map_err(|err| into_miette_for_parse(&path.display().to_string(), &code, err))?;
let (ctx, mut state) = new_context_state(Some(path))
let (ctx, mut state) = new_context_state(Some(path), false)
.await
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?;
// Execute the program.
@ -342,7 +421,7 @@ async fn execute_code_and_snapshot(code: String, image_format: ImageFormat) -> P
let program =
kcl_lib::Program::parse_no_errs(&code).map_err(|err| into_miette_for_parse("", &code, err))?;
let (ctx, mut state) = new_context_state(None)
let (ctx, mut state) = new_context_state(None, false)
.await
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?;
// Execute the program.
@ -402,7 +481,7 @@ async fn execute_and_export(path: String, export_format: FileExportFormat) -> Py
let program = kcl_lib::Program::parse_no_errs(&code)
.map_err(|err| into_miette_for_parse(&path.display().to_string(), &code, err))?;
let (ctx, mut state) = new_context_state(Some(path.clone()))
let (ctx, mut state) = new_context_state(Some(path.clone()), false)
.await
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?;
// Execute the program.
@ -450,7 +529,7 @@ async fn execute_code_and_export(code: String, export_format: FileExportFormat)
let program =
kcl_lib::Program::parse_no_errs(&code).map_err(|err| into_miette_for_parse("", &code, err))?;
let (ctx, mut state) = new_context_state(None)
let (ctx, mut state) = new_context_state(None, false)
.await
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?;
// Execute the program.
@ -534,8 +613,12 @@ fn kcl(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<Discovered>()?;
// Add our functions to the module.
m.add_function(wrap_pyfunction!(parse, m)?)?;
m.add_function(wrap_pyfunction!(parse_code, m)?)?;
m.add_function(wrap_pyfunction!(execute, m)?)?;
m.add_function(wrap_pyfunction!(execute_code, m)?)?;
m.add_function(wrap_pyfunction!(mock_execute, m)?)?;
m.add_function(wrap_pyfunction!(mock_execute_code, m)?)?;
m.add_function(wrap_pyfunction!(execute_and_snapshot, m)?)?;
m.add_function(wrap_pyfunction!(execute_code_and_snapshot, m)?)?;
m.add_function(wrap_pyfunction!(execute_and_export, m)?)?;

View File

@ -39,6 +39,64 @@ async def test_kcl_execute():
await kcl.execute(lego_file)
@pytest.mark.asyncio
async def test_kcl_parse_with_exception():
# Read from a file.
try:
await kcl.parse(os.path.join(files_dir, "parse_file_error"))
except Exception as e:
assert e is not None
assert len(str(e)) > 0
assert "lksjndflsskjfnak;jfna##" in str(e)
@pytest.mark.asyncio
async def test_kcl_parse():
# Read from a file.
result = await kcl.parse(lego_file)
assert result is True
@pytest.mark.asyncio
async def test_kcl_parse_code():
# Read from a file.
with open(lego_file, "r") as f:
code = str(f.read())
assert code is not None
assert len(code) > 0
result = kcl.parse_code(code)
assert result is True
@pytest.mark.asyncio
async def test_kcl_mock_execute_with_exception():
# Read from a file.
try:
await kcl.mock_execute(os.path.join(files_dir, "parse_file_error"))
except Exception as e:
assert e is not None
assert len(str(e)) > 0
assert "lksjndflsskjfnak;jfna##" in str(e)
@pytest.mark.asyncio
async def test_kcl_mock_execute():
# Read from a file.
result = await kcl.mock_execute(lego_file)
assert result is True
@pytest.mark.asyncio
async def test_kcl_mock_execute_code():
# Read from a file.
with open(lego_file, "r") as f:
code = str(f.read())
assert code is not None
assert len(code) > 0
result = await kcl.mock_execute_code(code)
assert result is True
@pytest.mark.asyncio
async def test_kcl_execute_code():
# Read from a file.
@ -97,9 +155,7 @@ async def test_kcl_execute_and_snapshot():
@pytest.mark.asyncio
async def test_kcl_execute_and_snapshot_dir():
# Read from a file.
image_bytes = await kcl.execute_and_snapshot(
car_wheel_dir, kcl.ImageFormat.Jpeg
)
image_bytes = await kcl.execute_and_snapshot(car_wheel_dir, kcl.ImageFormat.Jpeg)
assert image_bytes is not None
assert len(image_bytes) > 0
@ -129,10 +185,12 @@ def test_kcl_format():
assert formatted_code is not None
assert len(formatted_code) > 0
@pytest.mark.asyncio
async def test_kcl_format_dir():
await kcl.format_dir(car_wheel_dir)
def test_kcl_lint():
# Read from a file.
with open(os.path.join(files_dir, "box_with_linter_errors.kcl"), "r") as f:

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-test-server"
description = "A test server for KCL"
version = "0.1.70"
version = "0.1.73"
edition = "2021"
license = "MIT"

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-to-core"
description = "Utility methods to convert kcl to engine core executable tests"
version = "0.1.70"
version = "0.1.73"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -1,6 +1,6 @@
[package]
name = "kcl-wasm-lib"
version = "0.1.70"
version = "0.1.73"
edition = "2021"
repository = "https://github.com/KittyCAD/modeling-app"
rust-version = "1.83"

View File

@ -117,7 +117,9 @@ export function App() {
// When leaving the modeling scene, cut the engine stream.
return () => {
engineStreamActor.send({ type: EngineStreamTransition.Pause })
// When leaving the modeling scene, cut the engine stream.
// Stop is more serious than Pause
engineStreamActor.send({ type: EngineStreamTransition.Stop })
}
}, [])

View File

@ -158,7 +158,8 @@ export function Toolbar({
const isDisabled =
disableAllButtons ||
!isConfiguredAvailable ||
maybeIconConfig.disabled?.(state) === true
maybeIconConfig.disabled?.(state) === true ||
kclManager.hasErrors()
return {
...maybeIconConfig,
@ -444,6 +445,15 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
contentClassName={contentClassName}
>
{children}
{kclManager.hasErrors() && (
<p className="text-xs p-1 text-chalkboard-70 dark:text-chalkboard-40">
<CustomIcon
name="exclamationMark"
className="w-4 h-4 inline-block mr-1 text-destroy-80 bg-destroy-10"
/>
Fix KCL errors to enable tools
</p>
)}
</Tooltip>
)
})

View File

@ -975,7 +975,6 @@ export class CameraControls {
},
})
}
await this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),

View File

@ -13,6 +13,7 @@ export const STRAIGHT_SEGMENT_SNAP_LINE = 'straight-segment-snap-line'
export const CIRCLE_SEGMENT = 'circle-segment'
export const CIRCLE_SEGMENT_BODY = 'circle-segment-body'
export const CIRCLE_SEGMENT_DASH = 'circle-segment-body-dashed'
export const CIRCLE_SEGMENT_RADIUS_BODY = 'circle-segment-radius-body'
export const TANGENTIAL_ARC_TO_SEGMENT = 'tangential-arc-to-segment'
export const TANGENTIAL_ARC_TO_SEGMENT_BODY = 'tangential-arc-to-segment-body'
export const TANGENTIAL_ARC_TO__SEGMENT_DASH =

View File

@ -60,7 +60,6 @@ import {
PROFILE_START,
SEGMENT_BODIES,
SEGMENT_BODIES_PLUS_PROFILE_START,
SEGMENT_WIDTH_PX,
STRAIGHT_SEGMENT,
STRAIGHT_SEGMENT_DASH,
TANGENTIAL_ARC_TO_SEGMENT,
@ -90,6 +89,7 @@ import {
getSceneScale,
} from '@src/clientSideScene/sceneUtils'
import type { SegmentUtils } from '@src/clientSideScene/segments'
import { createLineShape } from '@src/clientSideScene/segments'
import {
createProfileStartHandle,
dashedStraight,
@ -3653,10 +3653,6 @@ export class SceneEntities {
this.sceneInfra._baseUnitMultiplier
const from = group.userData.from
const shape = new Shape()
shape.moveTo(0, (-SEGMENT_WIDTH_PX / 2) * scale) // The width of the line in px (2.4px in this case)
shape.lineTo(0, (SEGMENT_WIDTH_PX / 2) * scale)
const straightSegmentBodyDashed = group.children.find(
(child) => child.userData.type === STRAIGHT_SEGMENT_DASH
) as Mesh
@ -3664,7 +3660,7 @@ export class SceneEntities {
straightSegmentBodyDashed.geometry = dashedStraight(
from,
to,
shape,
createLineShape(scale),
scale
)
}

View File

@ -64,6 +64,7 @@ import {
THREE_POINT_ARC_SEGMENT_BODY,
THREE_POINT_ARC_SEGMENT_DASH,
getParentGroup,
CIRCLE_SEGMENT_RADIUS_BODY,
} from '@src/clientSideScene/sceneConstants'
import type { SceneInfra } from '@src/clientSideScene/sceneInfra'
import {
@ -255,9 +256,7 @@ class StraightSegment implements SegmentUtils {
const { from, to } = input
group.userData.from = from
group.userData.to = to
const shape = new Shape()
shape.moveTo(0, (-SEGMENT_WIDTH_PX / 2) * scale) // The width of the line in px (2.4px in this case)
shape.lineTo(0, (SEGMENT_WIDTH_PX / 2) * scale)
const shape = createLineShape(scale)
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
const labelGroup = group.getObjectByName(SEGMENT_LENGTH_LABEL) as Group
@ -350,6 +349,7 @@ class StraightSegment implements SegmentUtils {
new Vector3(from[0], from[1], 0),
new Vector3(to[0], to[1], 0)
)
straightSegmentBody.geometry?.dispose()
straightSegmentBody.geometry = new ExtrudeGeometry(shape, {
steps: 2,
bevelEnabled: false,
@ -607,6 +607,21 @@ class CircleSegment implements SegmentUtils {
const arcMesh = new Mesh(geometry, mat)
const meshType = isDraftSegment ? CIRCLE_SEGMENT_DASH : CIRCLE_SEGMENT_BODY
const arrowGroup = createArrowhead(scale, theme, color)
const shape = new Shape()
const line = new LineCurve3(
new Vector3(from[0], from[1], 0),
new Vector3(center[0], center[1], 0)
)
const arrowGeometry = new ExtrudeGeometry(shape, {
steps: 2,
bevelEnabled: false,
extrudePath: line,
})
const body = new MeshBasicMaterial({ color })
const arrowBody = new Mesh(arrowGeometry, body)
arrowBody.name = CIRCLE_SEGMENT_RADIUS_BODY
const circleCenterGroup = createCircleCenterHandle(scale, theme, color)
// A radius indicator that appears from the center to the perimeter
const radiusIndicatorGroup = createLengthIndicator({
@ -633,7 +648,13 @@ class CircleSegment implements SegmentUtils {
}
group.name = CIRCLE_SEGMENT
group.add(arcMesh, arrowGroup, circleCenterGroup, radiusIndicatorGroup)
group.add(
arcMesh,
arrowGroup,
arrowBody,
circleCenterGroup,
radiusIndicatorGroup
)
const updateOverlaysCallback = this.update({
prevSegment,
input,
@ -700,6 +721,25 @@ class CircleSegment implements SegmentUtils {
)
arrowGroup.scale.set(scale, scale, scale)
arrowGroup.visible = isHandlesVisible
const straightSegmentBody = group.getObjectByName(
CIRCLE_SEGMENT_RADIUS_BODY
) as Mesh
if (straightSegmentBody) {
const line = new LineCurve3(
new Vector3(center[0], center[1], 0),
new Vector3(arrowPoint.x, arrowPoint.y, 0)
)
straightSegmentBody.geometry?.dispose()
straightSegmentBody.geometry = new ExtrudeGeometry(
createLineShape(scale),
{
steps: 2,
bevelEnabled: false,
extrudePath: line,
}
)
}
}
if (radiusLengthIndicator) {
@ -1838,10 +1878,8 @@ export function createArcGeometry({
ccw,
0
)
const shape = new Shape()
shape.moveTo(0, (-SEGMENT_WIDTH_PX / 2) * scale)
shape.lineTo(0, (SEGMENT_WIDTH_PX / 2) * scale) // The width of the line
const shape = createLineShape(scale)
if (!isDashed) {
const points = arcStart.getPoints(50)
const path = new CurvePath<Vector3>()
@ -2110,6 +2148,14 @@ function updateAngleIndicator(
angleIndicator.geometry.setFromPoints(points)
}
// Used to create a line with thickness
export function createLineShape(scale: number) {
const shape = new Shape()
shape.moveTo(0, (-SEGMENT_WIDTH_PX / 2) * scale) // The width of the line in px (2.4px in this case)
shape.lineTo(0, (SEGMENT_WIDTH_PX / 2) * scale)
return shape
}
export const segmentUtils = {
straight: new StraightSegment(),
tangentialArc: new TangentialArcToSegment(),

View File

@ -136,8 +136,9 @@ function optionIsDisabled(option: Command): boolean {
option.disabled ||
('machineActor' in option &&
option.machineActor !== undefined &&
!getActorNextEvents(option.machineActor.getSnapshot()).includes(
(!getActorNextEvents(option.machineActor.getSnapshot()).includes(
option.name
))
) ||
!option.machineActor?.getSnapshot().can({ type: option.name })))
)
}

View File

@ -1,3 +1,4 @@
import { isPlaywright } from '@src/lib/isPlaywright'
import { useAppState } from '@src/AppState'
import { ClientSideScene } from '@src/clientSideScene/ClientSideSceneComp'
import { ViewControlContextMenu } from '@src/components/ViewControlMenu'
@ -5,7 +6,10 @@ import { useModelingContext } from '@src/hooks/useModelingContext'
import { useNetworkContext } from '@src/hooks/useNetworkContext'
import { NetworkHealthState } from '@src/hooks/useNetworkStatus'
import { getArtifactOfTypes } from '@src/lang/std/artifactGraph'
import { EngineCommandManagerEvents } from '@src/lang/std/engineConnection'
import {
EngineCommandManagerEvents,
EngineConnectionStateType,
} from '@src/lang/std/engineConnection'
import { btnName } from '@src/lib/cameraControls'
import { PATHS } from '@src/lib/paths'
import { sendSelectEventToEngine } from '@src/lib/selections'
@ -33,22 +37,38 @@ import { createThumbnailPNGOnDesktop } from '@src/lib/screenshot'
import type { SettingsViaQueryString } from '@src/lib/settings/settingsTypes'
import { resetCameraPosition } from '@src/lib/resetCameraPosition'
const TIME_1_SECOND = 1000
export const EngineStream = (props: {
pool: string | null
authToken: string | undefined
}) => {
const { setAppState } = useAppState()
const [firstPlay, setFirstPlay] = useState(true)
const { overallState } = useNetworkContext()
const settings = useSettings()
const engineStreamState = useSelector(engineStreamActor, (state) => state)
const { state: modelingMachineState, send: modelingMachineActorSend } =
useModelingContext()
const { file, project } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const last = useRef<number>(Date.now())
const [firstPlay, setFirstPlay] = useState(true)
const [isRestartRequestStarting, setIsRestartRequestStarting] =
useState(false)
const [attemptTimes, setAttemptTimes] = useState<[number, number]>([
0,
TIME_1_SECOND,
])
// These will be passed to the engineStreamActor to handle.
const videoRef = useRef<HTMLVideoElement>(null)
const canvasRef = useRef<HTMLCanvasElement>(null)
// For attaching right-click menu events
const videoWrapperRef = useRef<HTMLDivElement>(null)
const { overallState } = useNetworkContext()
const engineStreamState = useSelector(engineStreamActor, (state) => state)
/**
* We omit `pool` here because `engineStreamMachine` will override it anyway
* within the `EngineStreamTransition.StartOrReconfigureEngine` Promise actor.
@ -62,19 +82,46 @@ export const EngineStream = (props: {
cameraOrbit: settings.modeling.cameraOrbit.current,
}
const { state: modelingMachineState, send: modelingMachineActorSend } =
useModelingContext()
const streamIdleMode = settings.app.streamIdleMode.current
useEffect(() => {
engineStreamActor.send({
type: EngineStreamTransition.SetVideoRef,
videoRef: { current: videoRef.current },
})
}, [videoRef.current])
useEffect(() => {
engineStreamActor.send({
type: EngineStreamTransition.SetCanvasRef,
canvasRef: { current: canvasRef.current },
})
}, [canvasRef.current])
useEffect(() => {
engineStreamActor.send({
type: EngineStreamTransition.SetPool,
pool: props.pool,
})
}, [props.pool])
useEffect(() => {
engineStreamActor.send({
type: EngineStreamTransition.SetAuthToken,
authToken: props.authToken,
})
}, [props.authToken])
// We have to call this here because of the dependencies:
// modelingMachineActorSend, setAppState, settingsEngine
// It's possible to pass these in earlier but I (lee) don't want to
// restructure this further at the moment.
const startOrReconfigureEngine = () => {
engineStreamActor.send({
type: EngineStreamTransition.StartOrReconfigureEngine,
modelingMachineActorSend,
settings: settingsEngine,
setAppState,
// It's possible a reconnect happens as we drag the window :')
onMediaStream(mediaStream: MediaStream) {
engineStreamActor.send({
type: EngineStreamTransition.SetMediaStream,
@ -84,18 +131,47 @@ export const EngineStream = (props: {
})
}
// When the scene is ready play the stream and execute!
useEffect(() => {
if (
engineStreamState.value !== EngineStreamState.WaitingForDependencies &&
engineStreamState.value !== EngineStreamState.Stopped
)
return
startOrReconfigureEngine()
}, [engineStreamState, setAppState])
// I would inline this but it needs to be a function for removeEventListener.
const play = () => {
engineStreamActor.send({
type: EngineStreamTransition.Play,
})
}
useEffect(() => {
engineCommandManager.addEventListener(
EngineCommandManagerEvents.SceneReady,
play
)
return () => {
engineCommandManager.removeEventListener(
EngineCommandManagerEvents.SceneReady,
play
)
}
}, [])
// When the scene is ready, execute kcl!
const executeKcl = () => {
console.log('scene is ready, execute kcl')
const kmp = kclManager.executeCode().catch(trap)
if (!firstPlay) return
setFirstPlay(false)
console.log('scene is ready, fire!')
setFirstPlay(false)
// Reset the restart timeouts
setAttemptTimes([0, TIME_1_SECOND])
console.log('firstPlay true, zoom to fit')
kmp
.then(async () => {
await resetCameraPosition()
@ -112,51 +188,65 @@ export const EngineStream = (props: {
useEffect(() => {
engineCommandManager.addEventListener(
EngineCommandManagerEvents.SceneReady,
play
executeKcl
)
return () => {
engineCommandManager.removeEventListener(
EngineCommandManagerEvents.SceneReady,
play
executeKcl
)
}
}, [firstPlay])
useEffect(() => {
// We do a back-off restart, using a fibonacci sequence, since it
// has a nice retry time curve (somewhat quick then exponential)
const attemptRestartIfNecessary = () => {
if (isRestartRequestStarting) return
setIsRestartRequestStarting(true)
setTimeout(() => {
engineStreamState.context.videoRef.current?.pause()
engineCommandManager.tearDown()
startOrReconfigureEngine()
setFirstPlay(false)
setIsRestartRequestStarting(false)
}, attemptTimes[0] + attemptTimes[1])
setAttemptTimes([attemptTimes[1], attemptTimes[0] + attemptTimes[1]])
}
// Poll that we're connected. If not, send a reset signal.
// Do not restart if we're in idle mode.
const connectionCheckIntervalId = setInterval(() => {
// SKIP DURING TESTS BECAUSE IT WILL MESS WITH REUSING THE
// ELECTRON INSTANCE.
if (isPlaywright()) {
return
}
// Don't try try to restart if we're already connected!
const hasEngineConnectionInst = engineCommandManager.engineConnection
const isDisconnected =
engineCommandManager.engineConnection?.state.type ===
EngineConnectionStateType.Disconnected
const inIdleMode = engineStreamState.value === EngineStreamState.Paused
if ((hasEngineConnectionInst && !isDisconnected) || inIdleMode) return
attemptRestartIfNecessary()
}, TIME_1_SECOND)
engineCommandManager.addEventListener(
EngineCommandManagerEvents.SceneReady,
play
EngineCommandManagerEvents.EngineRestartRequest,
attemptRestartIfNecessary
)
engineStreamActor.send({
type: EngineStreamTransition.SetPool,
data: { pool: props.pool },
})
engineStreamActor.send({
type: EngineStreamTransition.SetAuthToken,
data: { authToken: props.authToken },
})
return () => {
engineCommandManager.tearDown()
}
}, [])
clearInterval(connectionCheckIntervalId)
// In the past we'd try to play immediately, but the proper thing is to way
// for the 'canplay' event to tell us data is ready.
useEffect(() => {
const videoRef = engineStreamState.context.videoRef.current
if (!videoRef) {
return
engineCommandManager.removeEventListener(
EngineCommandManagerEvents.EngineRestartRequest,
attemptRestartIfNecessary
)
}
const play = () => {
videoRef.play().catch(console.error)
}
videoRef.addEventListener('canplay', play)
return () => {
videoRef.removeEventListener('canplay', play)
}
}, [engineStreamState.context.videoRef.current])
}, [engineStreamState, attemptTimes, isRestartRequestStarting])
useEffect(() => {
if (engineStreamState.value === EngineStreamState.Reconfiguring) return
@ -184,25 +274,6 @@ export const EngineStream = (props: {
}).observe(document.body)
}, [engineStreamState.value])
// When the video and canvas element references are set, start the engine.
useEffect(() => {
if (
engineStreamState.context.canvasRef.current &&
engineStreamState.context.videoRef.current
) {
startOrReconfigureEngine()
}
}, [
engineStreamState.context.canvasRef.current,
engineStreamState.context.videoRef.current,
])
// On settings change, reconfigure the engine. When paused this gets really tricky,
// and also requires onMediaStream to be set!
useEffect(() => {
startOrReconfigureEngine()
}, Object.values(settingsEngine))
/**
* Subscribe to execute code when the file changes
* but only if the scene is already ready.
@ -285,18 +356,7 @@ export const EngineStream = (props: {
}
if (engineStreamState.value === EngineStreamState.Paused) {
engineStreamActor.send({
type: EngineStreamTransition.StartOrReconfigureEngine,
modelingMachineActorSend,
settings: settingsEngine,
setAppState,
onMediaStream(mediaStream: MediaStream) {
engineStreamActor.send({
type: EngineStreamTransition.SetMediaStream,
mediaStream,
})
},
})
startOrReconfigureEngine()
}
timeoutStart.current = Date.now()
@ -314,7 +374,7 @@ export const EngineStream = (props: {
window.document.addEventListener('mouseup', onAnyInput)
window.document.addEventListener('scroll', onAnyInput)
window.document.addEventListener('touchstart', onAnyInput)
window.document.addEventListener('touchstop', onAnyInput)
window.document.addEventListener('touchend', onAnyInput)
return () => {
timeoutStart.current = null
@ -325,10 +385,34 @@ export const EngineStream = (props: {
window.document.removeEventListener('mouseup', onAnyInput)
window.document.removeEventListener('scroll', onAnyInput)
window.document.removeEventListener('touchstart', onAnyInput)
window.document.removeEventListener('touchstop', onAnyInput)
window.document.removeEventListener('touchend', onAnyInput)
}
}, [streamIdleMode, engineStreamState.value])
// On various inputs save the camera state, in case we get disconnected.
useEffect(() => {
const onInput = () => {
// Save the remote camera state to restore on stream restore.
// Fire-and-forget because we don't know when a camera movement is
// completed on the engine side (there are no responses to data channel
// mouse movements.)
sceneInfra.camControls.saveRemoteCameraState().catch(trap)
}
// These usually signal a user is done some sort of operation.
window.document.addEventListener('keyup', onInput)
window.document.addEventListener('mouseup', onInput)
window.document.addEventListener('scroll', onInput)
window.document.addEventListener('touchend', onInput)
return () => {
window.document.removeEventListener('keyup', onInput)
window.document.removeEventListener('mouseup', onInput)
window.document.removeEventListener('scroll', onInput)
window.document.removeEventListener('touchend', onInput)
}
}, [])
const isNetworkOkay =
overallState === NetworkHealthState.Ok ||
overallState === NetworkHealthState.Weak
@ -399,7 +483,7 @@ export const EngineStream = (props: {
autoPlay
muted
key={engineStreamActor.id + 'video'}
ref={engineStreamState.context.videoRef}
ref={videoRef}
controls={false}
className="w-full cursor-pointer h-full"
disablePictureInPicture
@ -407,7 +491,7 @@ export const EngineStream = (props: {
/>
<canvas
key={engineStreamActor.id + 'canvas'}
ref={engineStreamState.context.canvasRef}
ref={canvasRef}
className="cursor-pointer"
id="freeze-frame"
>
@ -424,9 +508,11 @@ export const EngineStream = (props: {
}
menuTargetElement={videoWrapperRef}
/>
{![EngineStreamState.Playing, EngineStreamState.Paused].some(
(s) => s === engineStreamState.value
) && (
{![
EngineStreamState.Playing,
EngineStreamState.Paused,
EngineStreamState.Resuming,
].some((s) => s === engineStreamState.value) && (
<Loading dataTestId="loading-engine" className="fixed inset-0 h-screen">
Connecting to engine
</Loading>

View File

@ -138,7 +138,9 @@ const Loading = ({ children, className, dataTestId }: LoadingProps) => {
CONNECTION_ERROR_TEXT[error.error] +
(error.context
? '\n\nThe error details are: ' +
JSON.stringify(error.context)
(error.context instanceof Object
? JSON.stringify(error.context)
: error.context)
: ''),
{
renderer: new SafeRenderer(markedOptions),

View File

@ -204,12 +204,13 @@ export const ModelingMachineProvider = ({
sceneInfra.camControls.syncDirection = 'engineToClient'
// TODO: Re-evaluate if this pause/play logic is needed.
store.videoElement?.pause()
return kclManager
.executeCode()
.then(() => {
if (engineCommandManager.engineConnection?.idleMode) return
if (engineCommandManager.idleMode) return
store.videoElement?.play().catch((e) => {
console.warn('Video playing was prevented', e)
@ -579,24 +580,23 @@ export const ModelingMachineProvider = ({
selectionRanges
)
},
'Has exportable geometry': () => {
if (!kclManager.hasErrors() && kclManager.ast.body.length > 0)
return true
else {
let errorMessage = 'Unable to Export '
if (kclManager.hasErrors()) errorMessage += 'due to KCL Errors'
else if (kclManager.ast.body.length === 0)
errorMessage += 'due to Empty Scene'
console.error(errorMessage)
toast.error(errorMessage)
return false
}
},
'Has exportable geometry': () =>
!kclManager.hasErrors() && kclManager.ast.body.length > 0,
},
actors: {
exportFromEngine: fromPromise(
async ({ input }: { input?: ModelingCommandSchema['Export'] }) => {
if (!input) {
if (kclManager.hasErrors() || kclManager.ast.body.length === 0) {
let errorMessage = 'Unable to Export '
if (kclManager.hasErrors()) {
errorMessage += 'due to KCL Errors'
} else if (kclManager.ast.body.length === 0) {
errorMessage += 'due to Empty Scene'
}
console.error(errorMessage)
toast.error(errorMessage)
return new Error(errorMessage)
} else if (!input) {
return new Error('No input provided')
}

View File

@ -111,7 +111,7 @@ function discoverColorsInKCL(
}
export function parseColorLiteral(colorLiteral: string): ColorData | null {
const literal = colorLiteral.replace(/"/g, '')
const literal = colorLiteral.replace(/"/g, '').replace(/'/g, '')
const match = hexRegex.exec(literal)
if (!match) {
return null

View File

@ -212,6 +212,12 @@ code {
z-index: 99999999999 !important;
}
.cm-rename-popup input {
/* use black text on white background in both light and dark mode */
color: black !important;
background: white !important;
}
@keyframes blink {
0%,
100% {

View File

@ -1,3 +1,4 @@
import { TEST } from '@src/env'
import type { Models } from '@kittycad/lib'
import { VITE_KC_API_WS_MODELING_URL, VITE_KC_DEV_TOKEN } from '@src/env'
import { jsAppSettings } from '@src/lib/settings/settingsUtils'
@ -7,6 +8,7 @@ import type { MachineManager } from '@src/components/MachineManagerProvider'
import type { useModelingContext } from '@src/hooks/useModelingContext'
import type { KclManager } from '@src/lang/KclSingleton'
import type CodeManager from '@src/lang/codeManager'
import type { SceneInfra } from '@src/clientSideScene/sceneInfra'
import type { EngineCommand, ResponseMap } from '@src/lang/std/artifactGraph'
import type { CommandLog } from '@src/lang/std/commandLog'
import { CommandLogType } from '@src/lang/std/commandLog'
@ -109,6 +111,13 @@ export const CONNECTION_ERROR_TEXT: Record<ConnectionError, string> = {
'An unexpected error occurred. Please report this to us.',
}
export const WEBSOCKET_READYSTATE_TEXT: Record<number, string> = {
[WebSocket.CONNECTING]: 'WebSocket.CONNECTING',
[WebSocket.OPEN]: 'WebSocket.OPEN',
[WebSocket.CLOSING]: 'WebSocket.CLOSING',
[WebSocket.CLOSED]: 'WebSocket.CLOSED',
}
export interface ErrorType {
// The error we've encountered.
error: ConnectionError
@ -208,6 +217,9 @@ export enum EngineConnectionEvents {
// We can eventually use it for more, but one step at a time.
ConnectionStateChanged = 'connection-state-changed', // (state: EngineConnectionState) => void
// There are various failure scenarios where we want to try a restart.
RestartRequest = 'restart-request',
// These are used for the EngineCommandManager and were created
// before onConnectionStateChange existed.
ConnectionStarted = 'connection-started', // (engineConnection: EngineConnection) => void
@ -238,11 +250,23 @@ class EngineConnection extends EventTarget {
pc?: RTCPeerConnection
unreliableDataChannel?: RTCDataChannel
mediaStream?: MediaStream
idleMode: boolean = false
promise?: Promise<void>
sdpAnswer?: RTCSessionDescriptionInit
triggeredStart = false
onWebSocketOpen = function (event: Event) {}
onWebSocketClose = function (event: Event) {}
onWebSocketError = function (event: Event) {}
onWebSocketMessage = function (event: MessageEvent) {}
onIceGatheringStateChange = function (
this: RTCPeerConnection,
event: Event
) {}
onIceConnectionStateChange = function (
this: RTCPeerConnection,
event: Event
) {}
onNegotiationNeeded = function (this: RTCPeerConnection, event: Event) {}
onIceCandidate = function (
this: RTCPeerConnection,
event: RTCPeerConnectionIceEvent
@ -252,6 +276,9 @@ class EngineConnection extends EventTarget {
event: RTCPeerConnectionIceErrorEvent
) {}
onConnectionStateChange = function (this: RTCPeerConnection, event: Event) {}
onSignalingStateChange = function (this: RTCDataChannel, event: Event) {}
onTrack = function (this: RTCPeerConnection, event: RTCTrackEvent) {}
onDataChannelOpen = function (this: RTCDataChannel, event: Event) {}
onDataChannelClose = function (this: RTCDataChannel, event: Event) {}
onDataChannelError = function (this: RTCDataChannel, event: Event) {}
@ -260,11 +287,7 @@ class EngineConnection extends EventTarget {
this: RTCPeerConnection,
event: RTCDataChannelEvent
) {}
onTrack = function (this: RTCPeerConnection, event: RTCTrackEvent) {}
onWebSocketOpen = function (event: Event) {}
onWebSocketClose = function (event: Event) {}
onWebSocketError = function (event: Event) {}
onWebSocketMessage = function (event: MessageEvent) {}
onNetworkStatusReady = () => {}
private _state: EngineConnectionState = {
@ -309,10 +332,10 @@ class EngineConnection extends EventTarget {
private engineCommandManager: EngineCommandManager
private pingPongSpan: { ping?: number; pong?: number }
private pingIntervalId: ReturnType<typeof setInterval> | null = null
private pingIntervalId: ReturnType<typeof setInterval> | undefined = undefined
isUsingConnectionLite: boolean = false
timeoutToForceConnectId: ReturnType<typeof setTimeout> | null = null
timeoutToForceConnectId: ReturnType<typeof setTimeout> | undefined = undefined
constructor({
engineCommandManager,
@ -416,18 +439,18 @@ class EngineConnection extends EventTarget {
return this.state.type === EngineConnectionStateType.ConnectionEstablished
}
tearDown(opts?: { idleMode: boolean }) {
this.idleMode = opts?.idleMode ?? false
if (this.pingIntervalId) {
clearInterval(this.pingIntervalId)
}
if (this.timeoutToForceConnectId) {
clearTimeout(this.timeoutToForceConnectId)
}
tearDown() {
clearInterval(this.pingIntervalId)
clearTimeout(this.timeoutToForceConnectId)
// As each network connection (websocket, webrtc, peer connection) is
// closed, they will handle removing their own event listeners.
// If they didn't then it'd be possible we stop listened to close events
// which is what we want to do in the first place :)
this.disconnectAll()
if (this.idleMode) {
if (this.engineCommandManager.idleMode) {
this.state = {
type: EngineConnectionStateType.Disconnecting,
value: {
@ -435,6 +458,7 @@ class EngineConnection extends EventTarget {
},
}
}
// Pass the state along
if (this.state.type === EngineConnectionStateType.Disconnecting) return
if (this.state.type === EngineConnectionStateType.Disconnected) return
@ -568,30 +592,41 @@ class EngineConnection extends EventTarget {
}, 3000)
}
this.pc?.addEventListener?.('icecandidate', this.onIceCandidate)
// Watch out human! The names of the next couple events are really similar!
this.onIceGatheringStateChange = (event) => {
console.log('icegatheringstatechange', event)
that.initiateConnectionExclusive()
}
this.pc?.addEventListener?.(
'icegatheringstatechange',
function (_event) {
console.log('icegatheringstatechange', this.iceGatheringState)
if (this.iceGatheringState !== 'complete') return
that.initiateConnectionExclusive()
}
this.onIceGatheringStateChange
)
this.onIceConnectionStateChange = (event: Event) => {
console.log('iceconnectionstatechange', event)
}
this.pc?.addEventListener?.(
'iceconnectionstatechange',
function (_event) {
console.log('iceconnectionstatechange', this.iceConnectionState)
console.log('iceconnectionstatechange', this.iceGatheringState)
}
this.onIceConnectionStateChange
)
this.onNegotiationNeeded = (event: Event) => {
console.log('negotiationneeded', event)
}
this.pc?.addEventListener?.(
'negotiationneeded',
this.onNegotiationNeeded
)
this.onSignalingStateChange = (event) => {
console.log('signalingstatechange', event)
}
this.pc?.addEventListener?.(
'signalingstatechange',
this.onSignalingStateChange
)
this.pc?.addEventListener?.('negotiationneeded', function (_event) {
console.log('negotiationneeded', this.iceConnectionState)
console.log('negotiationneeded', this.iceGatheringState)
})
this.pc?.addEventListener?.('signalingstatechange', function (event) {
console.log('signalingstatechange', this.signalingState)
})
this.onIceCandidateError = (_event: Event) => {
const event = _event as RTCPeerConnectionIceErrorEvent
@ -618,38 +653,12 @@ class EngineConnection extends EventTarget {
detail: { conn: this, mediaStream: this.mediaStream! },
})
)
setTimeout(() => {
// Everything is now connected.
this.state = {
type: EngineConnectionStateType.ConnectionEstablished,
}
this.engineCommandManager.inSequence = 1
this.dispatchEvent(
new CustomEvent(EngineConnectionEvents.Opened, {
detail: this,
})
)
markOnce('code/endInitialEngineConnect')
}, 2000)
break
case 'connecting':
break
case 'disconnected':
case 'failed':
this.pc?.removeEventListener('icecandidate', this.onIceCandidate)
this.pc?.removeEventListener(
'icecandidateerror',
this.onIceCandidateError
)
this.pc?.removeEventListener(
'connectionstatechange',
this.onConnectionStateChange
)
this.pc?.removeEventListener('track', this.onTrack)
case 'failed':
this.state = {
type: EngineConnectionStateType.Disconnecting,
value: {
@ -662,6 +671,43 @@ class EngineConnection extends EventTarget {
}
this.disconnectAll()
break
// The remote end broke up with us! :(
case 'disconnected':
this.dispatchEvent(
new CustomEvent(EngineConnectionEvents.RestartRequest, {})
)
break
case 'closed':
this.pc?.removeEventListener('icecandidate', this.onIceCandidate)
this.pc?.removeEventListener(
'icegatheringstatechange',
this.onIceGatheringStateChange
)
this.pc?.removeEventListener(
'iceconnectionstatechange',
this.onIceConnectionStateChange
)
this.pc?.removeEventListener(
'negotiationneeded',
this.onNegotiationNeeded
)
this.pc?.removeEventListener(
'signalingstatechange',
this.onSignalingStateChange
)
this.pc?.removeEventListener(
'icecandidateerror',
this.onIceCandidateError
)
this.pc?.removeEventListener(
'connectionstatechange',
this.onConnectionStateChange
)
this.pc?.removeEventListener('track', this.onTrack)
this.pc?.removeEventListener('datachannel', this.onDataChannel)
break
default:
break
}
@ -735,9 +781,7 @@ class EngineConnection extends EventTarget {
// The app is eager to use the MediaStream; as soon as onNewTrack is
// called, the following sequence happens:
// EngineConnection.onNewTrack -> StoreState.setMediaStream ->
// Stream.tsx reacts to mediaStream change, setting a video element.
// We wait until connectionstatechange changes to "connected"
// to pass it to the rest of the application.
// EngineStream.tsx reacts to mediaStream change, setting a video element.
this.mediaStream = mediaStream
}
@ -761,6 +805,25 @@ class EngineConnection extends EventTarget {
type: ConnectingType.DataChannelEstablished,
},
}
// Start firing off engine commands at this point.
// They could be fired at an earlier time, onWebSocketOpen,
// but DataChannel can offer some benefits like speed,
// and it's nice to say everything's connected before interacting
// with the server.
this.state = {
type: EngineConnectionStateType.ConnectionEstablished,
}
this.engineCommandManager.inSequence = 1
this.dispatchEvent(
new CustomEvent(EngineConnectionEvents.Opened, {
detail: this,
})
)
markOnce('code/endInitialEngineConnect')
}
this.unreliableDataChannel?.addEventListener(
'open',
@ -784,7 +847,6 @@ class EngineConnection extends EventTarget {
'message',
this.onDataChannelMessage
)
this.pc?.removeEventListener('datachannel', this.onDataChannel)
this.disconnectAll()
}
@ -898,16 +960,19 @@ class EngineConnection extends EventTarget {
}
this.websocket.addEventListener('close', this.onWebSocketClose)
this.onWebSocketError = (event) => {
this.state = {
type: EngineConnectionStateType.Disconnecting,
value: {
type: DisconnectingType.Error,
this.onWebSocketError = (event: Event) => {
if (event.target instanceof WebSocket) {
this.state = {
type: EngineConnectionStateType.Disconnecting,
value: {
error: ConnectionError.WebSocketError,
context: event,
type: DisconnectingType.Error,
value: {
error: ConnectionError.WebSocketError,
context:
WEBSOCKET_READYSTATE_TEXT[event.target.readyState] ?? event,
},
},
},
}
}
this.disconnectAll()
@ -1213,20 +1278,27 @@ class EngineConnection extends EventTarget {
!this.websocket ||
this.websocket?.readyState === 3
if (closedPc && closedUDC && closedWS) {
if (!this.idleMode) {
// Do not notify the rest of the program that we have cut off anything.
this.state = { type: EngineConnectionStateType.Disconnected }
} else {
this.state = {
type: EngineConnectionStateType.Disconnecting,
value: {
type: DisconnectingType.Pause,
},
}
}
this.triggeredStart = false
if (!(closedPc && closedUDC && closedWS)) {
return
}
// Clean up all the event listeners.
if (!this.engineCommandManager.idleMode) {
// Do not notify the rest of the program that we have cut off anything.
this.state = { type: EngineConnectionStateType.Disconnected }
this.dispatchEvent(
new CustomEvent(EngineConnectionEvents.RestartRequest, {})
)
} else {
this.state = {
type: EngineConnectionStateType.Disconnecting,
value: {
type: DisconnectingType.Pause,
},
}
}
this.triggeredStart = false
}
}
@ -1254,6 +1326,9 @@ export enum EngineCommandManagerEvents {
// engineConnection is available but scene setup may not have run
EngineAvailable = 'engine-available',
// request a restart of engineConnection
EngineRestartRequest = 'engine-restart-request',
// the whole scene is ready (settings loaded)
SceneReady = 'scene-ready',
}
@ -1366,10 +1441,29 @@ export class EngineCommandManager extends EventTarget {
kclManager: null | KclManager = null
codeManager?: CodeManager
rustContext?: RustContext
sceneInfra?: SceneInfra
// The current "manufacturing machine" aka 3D printer, CNC, etc.
public machineManager: MachineManager | null = null
// Dispatch to the application the engine needs a restart.
private onEngineConnectionRestartRequest = () => {
this.dispatchEvent(
new CustomEvent(EngineCommandManagerEvents.EngineRestartRequest, {})
)
}
private onOffline = () => {
console.log('Browser reported network is offline')
if (TEST) {
console.warn('DURING TESTS ENGINECONNECTION.ONOFFLINE WILL DO NOTHING.')
return
}
this.onEngineConnectionRestartRequest()
}
idleMode: boolean = false
start({
setMediaStream,
setIsStreamReady,
@ -1415,6 +1509,8 @@ export class EngineCommandManager extends EventTarget {
return
}
window.addEventListener('offline', this.onOffline)
let additionalSettings = this.settings.enableSSAO ? '&post_effect=ssao' : ''
additionalSettings +=
'&show_grid=' + (this.settings.showScaleGrid ? 'true' : 'false')
@ -1436,29 +1532,51 @@ export class EngineCommandManager extends EventTarget {
})
)
this.engineConnection.addEventListener(
EngineConnectionEvents.RestartRequest,
this.onEngineConnectionRestartRequest as EventListener
)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
this.onEngineConnectionOpened = async () => {
await this.rustContext?.clearSceneAndBustCache(
await jsAppSettings(),
this.codeManager?.currentFilePath || undefined
)
console.log('onEngineConnectionOpened')
try {
console.log('clearing scene and busting cache')
await this.rustContext?.clearSceneAndBustCache(
await jsAppSettings(),
this.codeManager?.currentFilePath || undefined
)
} catch (e) {
// If this happens shit's actually gone south aka the websocket closed.
// Let's restart.
console.warn("shit's gone south")
console.warn(e)
this.engineConnection?.dispatchEvent(
new CustomEvent(EngineConnectionEvents.RestartRequest, {})
)
return
}
// Set the stream's camera projection type
// We don't send a command to the engine if in perspective mode because
// for now it's the engine's default.
if (settings.cameraProjection === 'orthographic') {
this.sendSceneCommand({
console.log('Setting camera to orthographic')
await this.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_set_orthographic',
},
}).catch(reportRejection)
})
}
// Set the theme
this.setTheme(this.settings.theme).catch(reportRejection)
console.log('Setting theme', this.settings.theme)
await this.setTheme(this.settings.theme)
// Set up a listener for the dark theme media query
console.log('Setup theme media query change')
darkModeMatcher?.addEventListener(
'change',
this.onDarkThemeMediaQueryChange
@ -1466,7 +1584,8 @@ export class EngineCommandManager extends EventTarget {
// Set the edge lines visibility
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.sendSceneCommand({
console.log('setting edge_lines_visible')
await this.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
@ -1475,21 +1594,30 @@ export class EngineCommandManager extends EventTarget {
},
})
console.log('camControlsCameraChange')
this._camControlsCameraChange()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.sendSceneCommand({
// CameraControls subscribes to default_camera_get_settings response events
// firing this at connection ensure the camera's are synced initially
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
// We should eventually only have 1 restoral call.
if (this.idleMode) {
await this.sceneInfra?.camControls.restoreRemoteCameraStateAndTriggerSync()
} else {
// NOTE: This code is old. It uses the old hack to restore camera.
console.log('call default_camera_get_settings')
// eslint-disable-next-line @typescript-eslint/no-floating-promises
await this.sendSceneCommand({
// CameraControls subscribes to default_camera_get_settings response events
// firing this at connection ensure the camera's are synced initially
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
}
setIsStreamReady(true)
console.log('Dispatching SceneReady')
// Other parts of the application should use this to react on scene ready.
this.dispatchEvent(
new CustomEvent(EngineCommandManagerEvents.SceneReady, {
@ -1555,7 +1683,7 @@ export class EngineCommandManager extends EventTarget {
}) as EventListener)
this.onVideoTrackMute = () => {
console.error('video track mute: check webrtc internals -> inbound rtp')
console.warn('video track mute - potentially lost stream for a moment')
}
this.onEngineConnectionNewTrack = ({
@ -1746,9 +1874,11 @@ export class EngineCommandManager extends EventTarget {
this.engineConnection?.send(resizeCmd)
}
tearDown(opts?: {
idleMode: boolean
}) {
tearDown(opts?: { idleMode: boolean }) {
this.idleMode = opts?.idleMode ?? false
window.removeEventListener('offline', this.onOffline)
if (this.engineConnection) {
for (const [cmdId, pending] of Object.entries(this.pendingCommands)) {
pending.reject([
@ -1786,14 +1916,14 @@ export class EngineCommandManager extends EventTarget {
this.onDarkThemeMediaQueryChange
)
this.engineConnection?.tearDown(opts)
this.engineConnection?.tearDown()
// Our window.engineCommandManager.tearDown assignment causes this case to happen which is
// only really for tests.
// @ts-ignore
} else if (this.engineCommandManager?.engineConnection) {
// @ts-ignore
this.engineCommandManager?.engineConnection?.tearDown(opts)
this.engineCommandManager?.engineConnection?.tearDown()
// @ts-ignore
this.engineCommandManager.engineConnection = null
}
@ -2112,25 +2242,25 @@ export class EngineCommandManager extends EventTarget {
// Set the stream background color
// This takes RGBA values from 0-1
// So we convert from the conventional 0-255 found in Figma
this.sendSceneCommand({
await this.sendSceneCommand({
cmd_id: uuidv4(),
type: 'modeling_cmd_req',
cmd: {
type: 'set_background_color',
color: getThemeColorForEngine(theme),
},
}).catch(reportRejection)
})
// Sets the default line colors
const opposingTheme = getOppositeTheme(theme)
this.sendSceneCommand({
await this.sendSceneCommand({
cmd_id: uuidv4(),
type: 'modeling_cmd_req',
cmd: {
type: 'set_default_system_properties',
color: getThemeColorForEngine(opposingTheme),
},
}).catch(reportRejection)
})
}
}

View File

@ -12,12 +12,6 @@ import type {
VariableDeclarator,
} from '@src/lang/wasm'
import { isPathToNode } from '@src/lang/wasm'
import {
loftValidator,
revolveAxisValidator,
shellValidator,
sweepValidator,
} from '@src/lib/commandBarConfigs/validators'
import type {
KclCommandValue,
StateMachineCommandSetConfig,
@ -428,7 +422,6 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
selectionTypes: ['segment'],
required: true,
multiple: false,
validation: sweepValidator,
hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit),
},
sectional: {
@ -455,7 +448,6 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
selectionTypes: ['solid2d'],
multiple: true,
required: true,
validation: loftValidator,
},
},
},
@ -508,7 +500,6 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
inputType: 'selection',
selectionTypes: ['segment', 'sweepEdge', 'edgeCutEdge'],
multiple: false,
validation: revolveAxisValidator,
hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit),
},
angle: {
@ -535,7 +526,6 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
selectionTypes: ['cap', 'wall'],
multiple: true,
required: true,
validation: shellValidator,
hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit),
},
thickness: {

View File

@ -1,13 +0,0 @@
import { parseEngineErrorMessage } from '@src/lib/commandBarConfigs/validators'
describe('parseEngineErrorMessage', () => {
it('takes an engine error string and parses its json message', () => {
const engineError =
'[{"success": false,"request_id": "e6c0104b-ec60-4779-8e98-722f0a5019ec","errors": [{"error_code": "internal_engine","message": "Trajectory curve must be G1 continuous (with continuous tangents)"}]}]'
const parsedEngineError = JSON.parse(engineError)
const message = parseEngineErrorMessage(parsedEngineError)
expect(message).toEqual(
'Trajectory curve must be G1 continuous (with continuous tangents)'
)
})
})

View File

@ -1,317 +0,0 @@
import type { Models } from '@kittycad/lib'
import type { Selections } from '@src/lib/selections'
import { engineCommandManager, kclManager } from '@src/lib/singletons'
import { isArray, uuidv4 } from '@src/lib/utils'
import type { CommandBarContext } from '@src/machines/commandBarMachine'
export const disableDryRunWithRetry = async (numberOfRetries = 3) => {
for (let tries = 0; tries < numberOfRetries; tries++) {
try {
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'disable_dry_run' },
})
// Exit out since the command was successful
return
} catch (e) {
console.error(e)
console.error('disable_dry_run failed. This is bad!')
}
}
}
// Takes a callback function and wraps it around enable_dry_run and disable_dry_run
export const dryRunWrapper = async (
callback: () => Promise<
| Models['WebSocketResponse_type']
| [Models['WebSocketResponse_type']]
| undefined
| null
>
): Promise<[Models['WebSocketResponse_type']] | undefined> => {
// Gotcha: What about race conditions?
try {
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'enable_dry_run' },
})
const result = await callback()
if (!result) {
return undefined
}
if (isArray(result)) {
return result
}
return [result]
} catch (e) {
console.error(e)
} finally {
await disableDryRunWithRetry(5)
}
}
function isSelections(selections: unknown): selections is Selections {
return (
(selections as Selections).graphSelections !== undefined &&
(selections as Selections).otherSelections !== undefined
)
}
export function parseEngineErrorMessage(
engineErrors?: [Models['WebSocketResponse_type']]
): string | undefined {
if (!engineErrors) {
return undefined
}
if (!engineErrors[0]) {
return undefined
}
const engineError = engineErrors[0]
if (engineError.success) {
return undefined
}
const errors = engineError.errors
if (!errors[0]) {
return undefined
}
return errors[0].message
}
export const revolveAxisValidator = async ({
data,
context,
}: {
data: { [key: string]: Selections }
context: CommandBarContext
}): Promise<boolean | string> => {
if (!isSelections(context.argumentsToSubmit.sketches)) {
return 'Unable to revolve, selections are missing'
}
// Gotcha: this validation only works for the first sketch
const artifact =
context.argumentsToSubmit.sketches.graphSelections[0].artifact
if (!artifact) {
return 'Unable to revolve, sketch not found'
}
if (!('pathId' in artifact)) {
return 'Unable to revolve, sketch has no path'
}
const sketchSelection = artifact.pathId
let edgeSelection = data.edge.graphSelections[0].artifact?.id
if (!sketchSelection) {
return 'Unable to revolve, sketch is missing'
}
if (!edgeSelection) {
return 'Unable to revolve, edge is missing'
}
const angleInDegrees: Models['Angle_type'] = {
unit: 'degrees',
value: 360,
}
const command = async () => {
return await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'revolve_about_edge',
angle: angleInDegrees,
edge_id: edgeSelection,
target: sketchSelection,
// Gotcha: Playwright will fail with larger tolerances, need to use a smaller one.
tolerance: 1e-7,
// WARNING: I'm not sure this is what it should be.
opposite: 'None',
},
})
}
const result = await dryRunWrapper(command)
if (result && result[0] && result[0].success) {
return true
}
const reason = parseEngineErrorMessage(result) || 'unknown'
return `Unable to revolve with the current selection. Reason: ${reason}`
}
export const loftValidator = async ({
data,
}: {
data: { [key: string]: Selections }
context: CommandBarContext
}): Promise<boolean | string> => {
if (!isSelections(data.sketches)) {
return 'Unable to loft, selections are missing'
}
const { sketches } = data
if (sketches.graphSelections.some((s) => s.artifact?.type !== 'solid2d')) {
return 'Unable to loft, some selection are not solid2ds'
}
const sectionIds = sketches.graphSelections.flatMap((s) =>
s.artifact?.type === 'solid2d' ? s.artifact.pathId : []
)
if (sectionIds.length < 2) {
return 'Unable to loft, selection contains less than two solid2ds'
}
const command = async () => {
// TODO: check what to do with these
const DEFAULT_V_DEGREE = 2
const DEFAULT_TOLERANCE = 2
const DEFAULT_BEZ_APPROXIMATE_RATIONAL = false
return await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
section_ids: sectionIds,
type: 'loft',
bez_approximate_rational: DEFAULT_BEZ_APPROXIMATE_RATIONAL,
tolerance: DEFAULT_TOLERANCE,
v_degree: DEFAULT_V_DEGREE,
},
})
}
const result = await dryRunWrapper(command)
if (result && result[0] && result[0].success) {
return true
}
const reason = parseEngineErrorMessage(result) || 'unknown'
return `Unable to loft with the current selection. Reason: ${reason}`
}
export const shellValidator = async ({
data,
}: {
data: { selection: Selections }
}): Promise<boolean | string> => {
if (!isSelections(data.selection)) {
return 'Unable to shell, selections are missing'
}
// No validation on the faces, filtering is done upstream and we have the dry run validation just below
const face_ids = data.selection.graphSelections.flatMap((s) =>
s.artifact ? s.artifact.id : []
)
// We don't have the concept of solid3ds in TS yet.
// So we're listing out the sweeps as if they were solids and taking the first one, just like in Rust for Shell:
// https://github.com/KittyCAD/modeling-app/blob/e61fff115b9fa94aaace6307b1842cc15d41655e/src/wasm-lib/kcl/src/std/shell.rs#L237-L238
// TODO: This is one cheap way to make sketch-on-face supported now but will likely fail multiple solids
const object_id = kclManager.artifactGraph
.values()
.find((v) => v.type === 'sweep')?.pathId
if (!object_id) {
return "Unable to shell, couldn't find the solid"
}
const command = async () => {
// TODO: figure out something better than an arbitrarily small value
const DEFAULT_THICKNESS: Models['LengthUnit_type'] = 1e-9
const DEFAULT_HOLLOW = false
const cmdArgs = {
face_ids,
object_id,
hollow: DEFAULT_HOLLOW,
shell_thickness: DEFAULT_THICKNESS,
}
return await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'solid3d_shell_face',
...cmdArgs,
},
})
}
const result = await dryRunWrapper(command)
if (result && result[0] && result[0].success) {
return true
}
const reason = parseEngineErrorMessage(result) || 'unknown'
return `Unable to shell with the current selection. Reason: ${reason}`
}
export const sweepValidator = async ({
context,
data,
}: {
context: CommandBarContext
data: { path: Selections }
}): Promise<boolean | string> => {
if (!isSelections(data.path)) {
console.log('Unable to sweep, selections are missing')
return 'Unable to sweep, selections are missing'
}
// Retrieve the parent path from the segment selection directly
const trajectoryArtifact = data.path.graphSelections[0].artifact
if (!trajectoryArtifact) {
return "Unable to sweep, couldn't find the trajectory artifact"
}
if (trajectoryArtifact.type !== 'segment') {
return "Unable to sweep, couldn't find the target from a non-segment selection"
}
const trajectory = trajectoryArtifact.pathId
// Get the former arg in the command bar flow, and retrieve the path from the solid2d directly
const targetArg = context.argumentsToSubmit['sketches'] as Selections
const targetArtifact = targetArg.graphSelections[0].artifact
if (!targetArtifact) {
return "Unable to sweep, couldn't find the profile artifact"
}
if (targetArtifact.type !== 'solid2d') {
return "Unable to sweep, couldn't find the target from a non-solid2d selection"
}
const target = targetArtifact.pathId
const command = async () => {
// TODO: second look on defaults here
const DEFAULT_TOLERANCE: Models['LengthUnit_type'] = 1e-7
const DEFAULT_SECTIONAL = false
const cmdArgs = {
target,
trajectory,
sectional: DEFAULT_SECTIONAL,
tolerance: DEFAULT_TOLERANCE,
}
return await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'sweep',
...cmdArgs,
},
})
}
const result = await dryRunWrapper(command)
if (result && result[0] && result[0].success) {
return true
}
const reason = parseEngineErrorMessage(result) || 'unknown'
return `Unable to sweep with the current selection. Reason: ${reason}`
}

View File

@ -24,6 +24,7 @@ import { Themes } from '@src/lib/theme'
import { reportRejection } from '@src/lib/trap'
import { isEnumMember } from '@src/lib/types'
import { capitaliseFC, isArray, toSync } from '@src/lib/utils'
import { IS_NIGHTLY_OR_DEBUG } from '@src/routes/utils'
/**
* A setting that can be set at the user or project level
@ -211,7 +212,7 @@ export function createSettings() {
* Stream resource saving behavior toggle
*/
streamIdleMode: new Setting<number | undefined>({
defaultValue: 5 * MS_IN_MINUTE,
defaultValue: IS_NIGHTLY_OR_DEBUG ? 30 * 1000 : 5 * MS_IN_MINUTE,
hideOnLevel: 'project',
hideOnPlatform: 'both',
description: 'Save bandwidth & battery',

View File

@ -74,6 +74,7 @@ editorManager.kclManager = kclManager
// TODO: proper dependency injection.
engineCommandManager.kclManager = kclManager
engineCommandManager.codeManager = codeManager
engineCommandManager.sceneInfra = sceneInfra
engineCommandManager.rustContext = rustContext
kclManager.sceneInfraBaseUnitMultiplierSetter = (unit: BaseUnit) => {

View File

@ -4,41 +4,53 @@ import { assign, fromPromise, setup } from 'xstate'
import type { AppMachineContext } from '@src/lib/types'
export enum EngineStreamState {
Off = 'off',
On = 'on',
WaitForMediaStream = 'wait-for-media-stream',
WaitingForDependencies = 'waiting-for-dependencies',
WaitingForMediaStream = 'waiting-for-media-stream',
WaitingToPlay = 'waiting-to-play',
Playing = 'playing',
Reconfiguring = 'reconfiguring',
Paused = 'paused',
Stopped = 'stopped',
// The is the state in-between Paused and Playing *specifically that order*.
Resuming = 'resuming',
}
export enum EngineStreamTransition {
SetMediaStream = 'set-media-stream',
// This brings us back to the configuration loop
WaitForDependencies = 'wait-for-dependencies',
// Our dependencies to set
SetPool = 'set-pool',
SetAuthToken = 'set-auth-token',
SetVideoRef = 'set-video-ref',
SetCanvasRef = 'set-canvas-ref',
SetMediaStream = 'set-media-stream',
// Stream operations
Play = 'play',
Resume = 'resume',
Pause = 'pause',
Stop = 'stop',
// Used to reconfigure the stream during connection
StartOrReconfigureEngine = 'start-or-reconfigure-engine',
}
export interface EngineStreamContext {
pool: string | null
authToken: string | undefined
mediaStream: MediaStream | null
videoRef: MutableRefObject<HTMLVideoElement | null>
canvasRef: MutableRefObject<HTMLCanvasElement | null>
mediaStream: MediaStream | null
zoomToFit: boolean
}
export const engineStreamContextCreate = (): EngineStreamContext => ({
pool: null,
authToken: undefined,
mediaStream: null,
videoRef: { current: null },
canvasRef: { current: null },
mediaStream: null,
zoomToFit: true,
})
@ -77,76 +89,6 @@ export const engineStreamMachine = setup({
input: {} as EngineStreamContext,
},
actors: {
[EngineStreamTransition.Play]: fromPromise(
async ({
input: { context, params, rootContext },
}: {
input: {
context: EngineStreamContext
params: { zoomToFit: boolean }
rootContext: AppMachineContext
}
}) => {
const canvas = context.canvasRef.current
if (!canvas) return false
const video = context.videoRef.current
if (!video) return false
const mediaStream = context.mediaStream
if (!mediaStream) return false
// If the video is already playing it means we're doing a reconfigure.
// We don't want to re-run the KCL or touch the video element at all.
if (!video.paused) {
return
}
await rootContext.sceneInfra.camControls.restoreRemoteCameraStateAndTriggerSync()
video.style.display = 'block'
canvas.style.display = 'none'
video.srcObject = mediaStream
}
),
[EngineStreamTransition.Pause]: fromPromise(
async ({
input: { context, rootContext },
}: {
input: { context: EngineStreamContext; rootContext: AppMachineContext }
}) => {
const video = context.videoRef.current
if (!video) return
video.pause()
const canvas = context.canvasRef.current
if (!canvas) return
await holdOntoVideoFrameInCanvas(video, canvas)
video.style.display = 'none'
await rootContext.sceneInfra.camControls.saveRemoteCameraState()
// Make sure we're on the next frame for no flickering between canvas
// and the video elements.
window.requestAnimationFrame(
() =>
void (async () => {
// Destroy the media stream. We will re-establish it. We could
// leave everything at pausing, preventing video decoders from running
// but we can do even better by significantly reducing network
// cards also.
context.mediaStream?.getVideoTracks()[0].stop()
context.mediaStream = null
video.srcObject = null
rootContext.engineCommandManager.tearDown({ idleMode: true })
})()
)
}
),
[EngineStreamTransition.StartOrReconfigureEngine]: fromPromise(
async ({
input: { context, event, rootContext },
@ -157,21 +99,17 @@ export const engineStreamMachine = setup({
rootContext: AppMachineContext
}
}) => {
if (!context.authToken) return
const video = context.videoRef.current
if (!video) return
const canvas = context.canvasRef.current
if (!canvas) return
if (!context.authToken) return Promise.reject()
if (!context.videoRef.current) return Promise.reject()
if (!context.canvasRef.current) return Promise.reject()
const { width, height } = getDimensions(
window.innerWidth,
window.innerHeight
)
video.width = width
video.height = height
context.videoRef.current.width = width
context.videoRef.current.height = height
const settingsNext = {
// override the pool param (?pool=) to request a specific engine instance
@ -206,51 +144,183 @@ export const engineStreamMachine = setup({
})
}
),
[EngineStreamTransition.Play]: fromPromise(
async ({
input: { context, params },
}: {
input: { context: EngineStreamContext; params: { zoomToFit: boolean } }
}) => {
if (!context.canvasRef.current) return
if (!context.videoRef.current) return
if (!context.mediaStream) return
// If the video is already playing it means we're doing a reconfigure.
// We don't want to re-run the KCL or touch the video element at all.
if (!context.videoRef.current.paused) {
return
}
// In the past we'd try to play immediately, but the proper thing is to way
// for the 'canplay' event to tell us data is ready.
const onCanPlay = () => {
if (!context.videoRef.current) {
return
}
context.videoRef.current.play().catch(console.error)
// Yes, event listeners can remove themselves because of the
// lazy nature of interpreted languages :D
context.videoRef.current.removeEventListener('canplay', onCanPlay)
}
// We're receiving video frames, so show the video now.
const onPlay = () => {
// We have to give engine time to crunch all the scene setup we
// ask it to do. As far as I can tell it doesn't block until
// they are done, so we must wait.
setTimeout(() => {
if (!context.videoRef.current) {
return
}
if (!context.canvasRef.current) {
return
}
context.videoRef.current.style.display = 'block'
context.canvasRef.current.style.display = 'none'
context.videoRef.current.removeEventListener('play', onPlay)
// I've tried < 400ms and sometimes it's possible to see a flash
// and the camera snap.
}, 400)
}
context.videoRef.current.addEventListener('canplay', onCanPlay)
context.videoRef.current.addEventListener('play', onPlay)
// THIS ASSIGNMENT IS *EXTREMELY* EFFECTFUL! The amount of logic
// this triggers is quite far and wide. It drives the above events.
context.videoRef.current.srcObject = context.mediaStream
}
),
// Pause is also called when leaving the modeling scene. It's possible
// then videoRef and canvasRef are now null due to their DOM elements
// being destroyed.
[EngineStreamTransition.Pause]: fromPromise(
async ({
input: { context, rootContext },
}: {
input: {
context: EngineStreamContext
rootContext: AppMachineContext
}
}) => {
if (context.videoRef.current && context.canvasRef.current) {
await context.videoRef.current.pause()
await holdOntoVideoFrameInCanvas(
context.videoRef.current,
context.canvasRef.current
)
context.videoRef.current.style.display = 'none'
}
await rootContext.sceneInfra.camControls.saveRemoteCameraState()
// Make sure we're on the next frame for no flickering between canvas
// and the video elements.
window.requestAnimationFrame(
() =>
void (async () => {
// Destroy the media stream. We will re-establish it. We could
// leave everything at pausing, preventing video decoders from running
// but we can do even better by significantly reducing network
// cards also.
context.mediaStream?.getVideoTracks()[0].stop()
context.mediaStream = null
if (context.videoRef.current) {
context.videoRef.current.srcObject = null
}
rootContext.engineCommandManager.tearDown({ idleMode: true })
})()
)
}
),
},
}).createMachine({
initial: EngineStreamState.Off,
initial: EngineStreamState.WaitingForDependencies,
context: (initial) => initial.input,
states: {
[EngineStreamState.Off]: {
reenter: true,
[EngineStreamState.WaitingForDependencies]: {
on: {
[EngineStreamTransition.SetPool]: {
target: EngineStreamState.Off,
actions: [assign({ pool: ({ context, event }) => event.data.pool })],
target: EngineStreamState.WaitingForDependencies,
actions: [assign({ pool: ({ context, event }) => event.pool })],
},
[EngineStreamTransition.SetAuthToken]: {
target: EngineStreamState.Off,
target: EngineStreamState.WaitingForDependencies,
actions: [
assign({ authToken: ({ context, event }) => event.data.authToken }),
assign({ authToken: ({ context, event }) => event.authToken }),
],
},
[EngineStreamTransition.SetVideoRef]: {
target: EngineStreamState.WaitingForDependencies,
actions: [
assign({ videoRef: ({ context, event }) => event.videoRef }),
],
},
[EngineStreamTransition.SetCanvasRef]: {
target: EngineStreamState.WaitingForDependencies,
actions: [
assign({ canvasRef: ({ context, event }) => event.canvasRef }),
],
},
[EngineStreamTransition.StartOrReconfigureEngine]: {
target: EngineStreamState.On,
target: EngineStreamState.WaitingForMediaStream,
},
},
},
[EngineStreamState.On]: {
reenter: true,
[EngineStreamState.WaitingForMediaStream]: {
invoke: {
src: EngineStreamTransition.StartOrReconfigureEngine,
input: (args) => ({
context: args.context,
rootContext: args.self.system.get('root').getSnapshot().context,
params: { zoomToFit: args.context.zoomToFit },
event: args.event,
}),
onError: [
{
target: EngineStreamState.WaitingForDependencies,
reenter: true,
},
],
},
on: {
// Transition requested by engineConnection
[EngineStreamTransition.StartOrReconfigureEngine]: {
target: EngineStreamState.WaitingForMediaStream,
reenter: true,
},
[EngineStreamTransition.SetMediaStream]: {
target: EngineStreamState.On,
target: EngineStreamState.WaitingToPlay,
actions: [
assign({ mediaStream: ({ context, event }) => event.mediaStream }),
],
},
},
},
[EngineStreamState.WaitingToPlay]: {
on: {
[EngineStreamTransition.Play]: {
target: EngineStreamState.Playing,
actions: [assign({ zoomToFit: () => true })],
},
// We actually failed inbetween needing to play and sending commands.
[EngineStreamTransition.StartOrReconfigureEngine]: {
target: EngineStreamState.WaitingForMediaStream,
reenter: true,
},
},
},
@ -270,6 +340,9 @@ export const engineStreamMachine = setup({
[EngineStreamTransition.Pause]: {
target: EngineStreamState.Paused,
},
[EngineStreamTransition.Stop]: {
target: EngineStreamState.Stopped,
},
},
},
[EngineStreamState.Reconfiguring]: {
@ -280,9 +353,7 @@ export const engineStreamMachine = setup({
rootContext: args.self.system.get('root').getSnapshot().context,
event: args.event,
}),
onDone: {
target: EngineStreamState.Playing,
},
onDone: [{ target: EngineStreamState.Playing }],
},
},
[EngineStreamState.Paused]: {
@ -299,8 +370,27 @@ export const engineStreamMachine = setup({
},
},
},
[EngineStreamState.Stopped]: {
invoke: {
src: EngineStreamTransition.Pause,
input: (args) => ({
context: args.context,
rootContext: args.self.system.get('root').getSnapshot().context,
}),
onDone: [
{
target: EngineStreamState.WaitingForDependencies,
actions: [
assign({
videoRef: { current: null },
canvasRef: { current: null },
}),
],
},
],
},
},
[EngineStreamState.Resuming]: {
reenter: true,
invoke: {
src: EngineStreamTransition.StartOrReconfigureEngine,
input: (args) => ({
@ -315,14 +405,11 @@ export const engineStreamMachine = setup({
target: EngineStreamState.Paused,
},
[EngineStreamTransition.SetMediaStream]: {
target: EngineStreamState.Playing,
actions: [
assign({ mediaStream: ({ context, event }) => event.mediaStream }),
],
},
[EngineStreamTransition.Play]: {
target: EngineStreamState.Playing,
actions: [assign({ zoomToFit: () => false })],
},
},
},
},

File diff suppressed because it is too large Load Diff