Compare commits

...

18 Commits

Author SHA1 Message Date
0604da9c37 Update 2020 tests 2024-07-13 19:45:15 -05:00
29bf77bb82 Show descriptions for generated commands, make them look better and sort better (#3023) 2024-07-12 17:48:38 -04:00
e81b614523 Lf94/save settings between reconnects (#2997)
* Keep settings between reconnects

* Set idle timeout to 2 minutes

* Put idle behind flags

* Remove pauses

* Fix online->offline->online

* Revert "Remove pauses"

This reverts commit 267ef4ff4b86f2d8014bfb2a8e8a633adc8001dc.

* ci

* call correct setmediastream
2024-07-12 20:42:23 +00:00
5a5fe3bb95 Add sketch tools back to the command bar (#3008)
* Make machine command type names more explicit

* Prepare "change tool" event for command bar

* Make it so that state machine events can each map to multiple command configs

* Make commands with all skippable args possible

* Add back the tools to the command bar

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Update to use new `groupId` property name

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Oops didn't save this other instance of `ownerMachine`

* Add a playwright test

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-12 16:16:26 -04:00
0710f6e5f2 Add format code to the command palette (#3001)
* Add format code to the command palette

* Fix to use renamed groupId parameter

* Add icon to format code command

* Fix to remove commands during teardown

* Fix dependencies

* Change formatting
2024-07-12 17:08:01 +00:00
c9d5633647 Bump thiserror from 1.0.61 to 1.0.62 in /src/wasm-lib (#3016)
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.61 to 1.0.62.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.61...1.0.62)

---
updated-dependencies:
- dependency-name: thiserror
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-12 09:11:49 -07:00
f9419a98b5 Cut release v0.24.1 (#3014) 2024-07-11 21:51:58 -04:00
999f72bccf mediaStream (#3013)
* mediaStream

* make vitest happy

* fmt
2024-07-11 20:57:27 -04:00
9dbe74e008 cleaner hack (#3012) 2024-07-12 09:41:39 +10:00
88d9cdc52b Codemirror deferrers (#3006)
* Force document update requests when necessary

* fix up codemirror deferrers

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

* lock

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

* fixups kcl/index

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

* fix copilot

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

* updates

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

* updates

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

* docs

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Marijn Haverbeke <marijn@haverbeke.berlin>
2024-07-11 16:05:19 -07:00
2dd1f0f213 refactor: Rename ownerMachine to groupId in Command (#3010)
* refactor: Rename ownerMachine to groupId in Command

Commands don't need to be part of a state machine.

* Fix formatting
2024-07-11 18:10:47 -04:00
b971f3ecf4 Fix CUT_RELEASE_PR eval in ci.yml (#3003) 2024-07-11 08:19:33 -04:00
2198bd7580 Rename function to use standard abbreviation (#2965) 2024-07-11 11:52:26 +00:00
5fa1497b75 Don't navigate when Backspace/Delete is pressed on the home screen (#2987) 2024-07-11 07:50:59 -04:00
ff86e41283 Roll your own Playwright retries (#3002)
* roll you own playwright retries

* tweak

* tweak

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* add retries for ubuntu too

* Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)"

This reverts commit 327cc196cd.

* Revert "add retries for ubuntu too"

This reverts commit db877748e2.

* add retries for ubuntu too

* whoopsie

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-11 17:37:59 +10:00
08e4c03ca7 send failing test to axiom (#2996)
* send failing test to axiom (ubuntu)

* forgot always

* rename

* Update .github/workflows/playwright.yml

Co-authored-by: Adam Sunderland <adam@kittycad.io>

* update to indivdual lines of json

* another fix

* tweak output

* log macos too

---------

Co-authored-by: Adam Sunderland <adam@kittycad.io>
2024-07-11 14:32:36 +10:00
c654582137 Build tauri updater test bundles on 'Cut release' PRs (#2927)
* WIP: Automate tauri updater tests
Fixes #2926

* Same product name

* Tweak uploads

* Add cat

* Fix macos universal builds for updater

* New artifact name

* Revert "New artifact name"

This reverts commit 61defcab18.

* Final check

* Clean up
2024-07-10 18:41:07 -04:00
6c2fa95a32 Fix perspective camera toggle in debug pane to update immediately (#2969) 2024-07-10 17:50:25 -04:00
168 changed files with 1203 additions and 565 deletions

View File

@ -13,6 +13,7 @@ on:
# Will checkout the last commit from the default branch (main as of 2023-10-04) # Will checkout the last commit from the default branch (main as of 2023-10-04)
env: env:
CUT_RELEASE_PR: ${{ github.event_name == 'pull_request' && (contains(github.event.pull_request.title, 'Cut release v')) }}
BUILD_RELEASE: ${{ github.event_name == 'release' || github.event_name == 'schedule' || github.event_name == 'pull_request' && (contains(github.event.pull_request.title, 'Cut release v')) }} BUILD_RELEASE: ${{ github.event_name == 'release' || github.event_name == 'schedule' || github.event_name == 'pull_request' && (contains(github.event.pull_request.title, 'Cut release v')) }}
concurrency: concurrency:
@ -110,8 +111,14 @@ jobs:
echo "$(jq --arg name 'Zoo Modeling App (Nightly)' \ echo "$(jq --arg name 'Zoo Modeling App (Nightly)' \
'.productName=$name' src-tauri/tauri.release.conf.json --indent 2)" > src-tauri/tauri.release.conf.json '.productName=$name' src-tauri/tauri.release.conf.json --indent 2)" > src-tauri/tauri.release.conf.json
- name: Set updater test version
if: ${{ env.CUT_RELEASE_PR == 'true' }}
run: |
echo "$(jq --arg url 'https://dl.zoo.dev/releases/modeling-app/test/last_update.json' \
'.plugins.updater.endpoints[]=$url' src-tauri/tauri.release.conf.json --indent 2)" > src-tauri/tauri.release.conf.json
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
if: github.event_name == 'schedule' if: ${{ github.event_name == 'schedule' || env.CUT_RELEASE_PR == 'true' }}
with: with:
path: | path: |
package.json package.json
@ -377,6 +384,30 @@ jobs:
E2E_TAURI_ENABLED: true E2E_TAURI_ENABLED: true
TS_NODE_COMPILER_OPTIONS: '{"module": "commonjs"}' TS_NODE_COMPILER_OPTIONS: '{"module": "commonjs"}'
- uses: actions/download-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' }}
- name: Copy updated .json file for updater test
if: ${{ env.CUT_RELEASE_PR == 'true' }}
run: |
ls -l artifact
cp artifact/src-tauri/tauri.release.conf.json src-tauri/tauri.release.conf.json
cat src-tauri/tauri.release.conf.json
- name: Build the app (release, updater test)
if: ${{ env.CUT_RELEASE_PR == 'true' && matrix.os != 'ubuntu-latest' }}
env:
TAURI_CONF_ARGS: "-c ${{ matrix.os == 'windows-latest' && 'src-tauri\\tauri.release.conf.json' || 'src-tauri/tauri.release.conf.json' }}"
TAURI_BUNDLE_ARGS: "-b ${{ matrix.os == 'windows-latest' && 'msi' || 'dmg' }}"
run: "yarn tauri build ${{ env.TAURI_CONF_ARGS }} ${{ env.TAURI_BUNDLE_ARGS }} ${{ env.TAURI_ARGS_MACOS }}"
- uses: actions/upload-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' && matrix.os != 'ubuntu-latest' }}
with:
path: "${{ matrix.os == 'macos-14' && 'src-tauri/target/universal-apple-darwin/release/bundle/dmg/*.dmg' || 'src-tauri/target/release/bundle/msi/*.msi' }}"
name: updater-test
publish-apps-release: publish-apps-release:
permissions: permissions:
contents: write contents: write

View File

@ -83,6 +83,20 @@ jobs:
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
with: with:
workspaces: './src/wasm-lib' workspaces: './src/wasm-lib'
- name: Install vector
run: |
curl --proto '=https' --tlsv1.2 -sSfL https://sh.vector.dev > /tmp/vector.sh
chmod +x /tmp/vector.sh
/tmp/vector.sh -y -no-modify-path
mkdir -p /tmp/vector
cp .github/workflows/vector.toml /tmp/vector.toml
sed -i "s#GITHUB_WORKFLOW#${GITHUB_WORKFLOW}#g" /tmp/vector.toml
sed -i "s#GITHUB_REPOSITORY#${GITHUB_REPOSITORY}#g" /tmp/vector.toml
sed -i "s#GITHUB_SHA#${GITHUB_SHA}#g" /tmp/vector.toml
sed -i "s#GITHUB_REF_NAME#${GITHUB_REF_NAME}#g" /tmp/vector.toml
sed -i "s#GH_ACTIONS_AXIOM_TOKEN#${{secrets.GH_ACTIONS_AXIOM_TOKEN}}#g" /tmp/vector.toml
cat /tmp/vector.toml
${HOME}/.vector/bin/vector --config /tmp/vector.toml &
- name: Build Wasm (because rust diff) - name: Build Wasm (because rust diff)
if: needs.check-rust-changes.outputs.rust-changed == 'true' if: needs.check-rust-changes.outputs.rust-changed == 'true'
run: yarn build:wasm run: yarn build:wasm
@ -139,27 +153,60 @@ jobs:
with: with:
name: test-results-ubuntu-${{ github.sha }} name: test-results-ubuntu-${{ github.sha }}
path: test-results/ path: test-results/
- name: Run ubuntu/chrome flow retry failures - name: Run ubuntu/chrome flow (with retries)
id: retry id: retry
if: always() if: always()
run: | run: |
if [[ -d "test-results" ]]; if [[ ! -f "test-results/.last-run.json" ]]; then
then if [[ $(ls -1 "test-results" | wc -l) != "0" ]]; # if no last run artifact, than run plawright normally
then echo "retried=true" >> $GITHUB_OUTPUT; echo "run playwright normally"
else echo "retried=false" >> $GITHUB_OUTPUT; exit 0; yarn playwright test --project="Google Chrome" e2e/playwright/flow-tests.spec.ts || true
fi; # # send to axiom
else echo "retried=false" >> $GITHUB_OUTPUT; exit 0; node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
fi; fi
yarn playwright test --project="Google Chrome" --last-failed e2e/playwright/flow-tests.spec.ts
env: retry=1
CI: true max_retrys=4
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
- name: Run ubuntu/chrome flow # retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues
if: steps.retry.outputs.retried == 'false' while [[ $retry -le $max_retrys ]]; do
run: yarn playwright test --project="Google Chrome" e2e/playwright/flow-tests.spec.ts if [[ -f "test-results/.last-run.json" ]]; then
failed_tests=$(jq '.failedTests | length' test-results/.last-run.json)
if [[ $failed_tests -gt 0 ]]; then
echo "retried=true" >>$GITHUB_OUTPUT
echo "run playwright with last failed tests and retry $retry"
yarn playwright test --project="Google Chrome" --last-failed e2e/playwright/flow-tests.spec.ts || true
# send to axiom
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
retry=$((retry + 1))
else
echo "retried=false" >>$GITHUB_OUTPUT
exit 0
fi
else
echo "retried=false" >>$GITHUB_OUTPUT
exit 0
fi
done
echo "retried=false" >>$GITHUB_OUTPUT
if [[ -f "test-results/.last-run.json" ]]; then
failed_tests=$(jq '.failedTests | length' test-results/.last-run.json)
if [[ $failed_tests -gt 0 ]]; then
# if it still fails after 3 retrys, then fail the job
exit 1
fi
fi
exit 0
env: env:
CI: true CI: true
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
- name: send to axiom
if: always()
shell: bash
run: |
node playwrightProcess.mjs | tee /tmp/github-actions.log
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: always() if: always()
with: with:
@ -226,6 +273,20 @@ jobs:
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
with: with:
workspaces: './src/wasm-lib' workspaces: './src/wasm-lib'
- name: Install vector
run: |
curl --proto '=https' --tlsv1.2 -sSfL https://sh.vector.dev > /tmp/vector.sh
chmod +x /tmp/vector.sh
/tmp/vector.sh -y -no-modify-path
mkdir -p /tmp/vector
cp .github/workflows/vector.toml /tmp/vector.toml
sed -i "" "s#GITHUB_WORKFLOW#${GITHUB_WORKFLOW}#g" /tmp/vector.toml
sed -i "" "s#GITHUB_REPOSITORY#${GITHUB_REPOSITORY}#g" /tmp/vector.toml
sed -i "" "s#GITHUB_SHA#${GITHUB_SHA}#g" /tmp/vector.toml
sed -i "" "s#GITHUB_REF_NAME#${GITHUB_REF_NAME}#g" /tmp/vector.toml
sed -i "" "s#GH_ACTIONS_AXIOM_TOKEN#${{secrets.GH_ACTIONS_AXIOM_TOKEN}}#g" /tmp/vector.toml
cat /tmp/vector.toml
${HOME}/.vector/bin/vector --config /tmp/vector.toml &
- name: Build Wasm (because rust diff) - name: Build Wasm (because rust diff)
if: needs.check-rust-changes.outputs.rust-changed == 'true' if: needs.check-rust-changes.outputs.rust-changed == 'true'
run: yarn build:wasm run: yarn build:wasm
@ -241,26 +302,52 @@ jobs:
with: with:
name: test-results-macos-${{ github.sha }} name: test-results-macos-${{ github.sha }}
path: test-results/ path: test-results/
- name: Run macos/safari flow retry failures - name: Run macos/safari flow (with retries)
id: retry id: retry
if: always() if: always()
run: | run: |
if [[ -d "test-results" ]]; if [[ ! -f "test-results/.last-run.json" ]]; then
then if [[ $(ls -1 "test-results" | wc -l) != "0" ]]; # if no last run artifact, than run plawright normally
then echo "retried=true" >> $GITHUB_OUTPUT; echo "run playwright normally"
else echo "retried=false" >> $GITHUB_OUTPUT; exit 0; yarn playwright test --project="webkit" e2e/playwright/flow-tests.spec.ts || true
fi; # # send to axiom
else echo "retried=false" >> $GITHUB_OUTPUT; exit 0; node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
fi; fi
yarn playwright test --project="webkit" --last-failed e2e/playwright/flow-tests.spec.ts
env: retry=1
CI: true max_retrys=4
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
- name: Run macos/safari flow # retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues
if: steps.retry.outputs.retried == 'false' while [[ $retry -le $max_retrys ]]; do
# webkit doesn't work on Ubuntu because of the same reason tauri doesn't (webRTC issues) if [[ -f "test-results/.last-run.json" ]]; then
# TODO remove this and the matrix and run all tests on ubuntu when this is fixed failed_tests=$(jq '.failedTests | length' test-results/.last-run.json)
run: yarn playwright test --project="webkit" e2e/playwright/flow-tests.spec.ts if [[ $failed_tests -gt 0 ]]; then
echo "retried=true" >>$GITHUB_OUTPUT
echo "run playwright with last failed tests and retry $retry"
yarn playwright test --project="webkit" --last-failed e2e/playwright/flow-tests.spec.ts || true
# send to axiom
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
retry=$((retry + 1))
else
echo "retried=false" >>$GITHUB_OUTPUT
exit 0
fi
else
echo "retried=false" >>$GITHUB_OUTPUT
exit 0
fi
done
echo "retried=false" >>$GITHUB_OUTPUT
if [[ -f "test-results/.last-run.json" ]]; then
failed_tests=$(jq '.failedTests | length' test-results/.last-run.json)
if [[ $failed_tests -gt 0 ]]; then
# if it still fails after 3 retrys, then fail the job
exit 1
fi
fi
exit 0
env: env:
CI: true CI: true
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 171 KiB

View File

@ -3691,6 +3691,46 @@ const extrude001 = extrude(distance001, sketch001)`.replace(
) // remove newlines ) // remove newlines
) )
}) })
test('Can switch between sketch tools via command bar', async ({ page }) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
const sketchButton = page.getByRole('button', { name: 'Start Sketch' })
const cmdBarButton = page.getByRole('button', { name: 'Commands' })
const rectangleToolCommand = page.getByRole('option', {
name: 'Rectangle',
})
const rectangleToolButton = page.getByRole('button', { name: 'Rectangle' })
const lineToolCommand = page.getByRole('option', { name: 'Line' })
const lineToolButton = page.getByRole('button', { name: 'Line' })
const arcToolCommand = page.getByRole('option', { name: 'Tangential Arc' })
const arcToolButton = page.getByRole('button', { name: 'Tangential Arc' })
// Start a sketch
await sketchButton.click()
await page.mouse.click(700, 200)
// Switch between sketch tools via the command bar
await expect(lineToolButton).toHaveAttribute('aria-pressed', 'true')
await cmdBarButton.click()
await rectangleToolCommand.click()
await expect(rectangleToolButton).toHaveAttribute('aria-pressed', 'true')
await cmdBarButton.click()
await lineToolCommand.click()
await expect(lineToolButton).toHaveAttribute('aria-pressed', 'true')
// Click in the scene a couple times to draw a line
// so tangential arc is valid
await page.mouse.click(700, 200)
await page.mouse.click(700, 300)
// switch to tangential arc via command bar
await cmdBarButton.click()
await arcToolCommand.click()
await expect(arcToolButton).toHaveAttribute('aria-pressed', 'true')
})
}) })
test.describe('Regression tests', () => { test.describe('Regression tests', () => {
@ -7726,6 +7766,31 @@ test('Basic default modeling and sketch hotkeys work', async ({ page }) => {
await expect(page.locator('.cm-content')).toContainText('extrude(') await expect(page.locator('.cm-content')).toContainText('extrude(')
}) })
test('Delete key does not navigate back', async ({ page }) => {
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await page.waitForURL('**/file/**', { waitUntil: 'domcontentloaded' })
const settingsButton = page.getByRole('link', {
name: 'Settings',
exact: false,
})
const settingsCloseButton = page.getByTestId('settings-close-button')
await settingsButton.click()
await expect(page.url()).toContain('/settings')
// Make sure that delete doesn't go back from settings
await page.keyboard.press('Delete')
await expect(page.url()).toContain('/settings')
// Now close the settings and try delete again,
// make sure it doesn't go back to settings
await settingsCloseButton.click()
await page.keyboard.press('Delete')
await expect(page.url()).not.toContain('/settings')
})
test('Sketch on face', async ({ page }) => { test('Sketch on face', async ({ page }) => {
test.setTimeout(90_000) test.setTimeout(90_000)
const u = await getUtils(page) const u = await getUtils(page)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

@ -1,6 +1,6 @@
{ {
"name": "untitled-app", "name": "untitled-app",
"version": "0.24.0", "version": "0.24.1",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.17.0", "@codemirror/autocomplete": "^6.17.0",

View File

@ -93,23 +93,10 @@ export class LanguageServerPlugin implements PluginValue {
private doSemanticTokens: boolean = false private doSemanticTokens: boolean = false
private doFoldingRanges: boolean = false private doFoldingRanges: boolean = false
private _defferer = deferExecution((code: string) => { // When a doc update needs to be sent to the server, this holds the
try { // timeout handle for it. When null, the server has the up-to-date
// Update the state (not the editor) with the new code. // document.
this.client.textDocumentDidChange({ private sendScheduled: number | null = null
textDocument: {
uri: this.getDocUri(),
version: this.documentVersion++,
},
contentChanges: [{ text: code }],
})
this.requestSemanticTokens()
this.updateFoldingRanges()
} catch (e) {
console.error(e)
}
}, this.changesDelay)
constructor(options: LanguageServerOptions, private view: EditorView) { constructor(options: LanguageServerOptions, private view: EditorView) {
this.client = options.client this.client = options.client
@ -152,14 +139,9 @@ export class LanguageServerPlugin implements PluginValue {
} }
update(viewUpdate: ViewUpdate) { update(viewUpdate: ViewUpdate) {
// If the doc didn't change we can return early. if (viewUpdate.docChanged) {
if (!viewUpdate.docChanged) { this.scheduleSendDoc()
return
} }
this.sendChange({
documentText: viewUpdate.state.doc.toString(),
})
} }
destroy() { destroy() {
@ -184,16 +166,6 @@ export class LanguageServerPlugin implements PluginValue {
this.updateFoldingRanges() this.updateFoldingRanges()
} }
async sendChange({ documentText }: { documentText: string }) {
if (!this.client.ready) return
this._defferer(documentText)
}
requestDiagnostics() {
this.sendChange({ documentText: this.getDocText() })
}
async requestHoverTooltip( async requestHoverTooltip(
view: EditorView, view: EditorView,
{ line, character }: { line: number; character: number } { line, character }: { line: number; character: number }
@ -204,7 +176,7 @@ export class LanguageServerPlugin implements PluginValue {
) )
return null return null
this.sendChange({ documentText: this.getDocText() }) this.ensureDocSent()
const result = await this.client.textDocumentHover({ const result = await this.client.textDocumentHover({
textDocument: { uri: this.getDocUri() }, textDocument: { uri: this.getDocUri() },
position: { line, character }, position: { line, character },
@ -227,6 +199,42 @@ export class LanguageServerPlugin implements PluginValue {
return { pos, end, create: (view) => ({ dom }), above: true } return { pos, end, create: (view) => ({ dom }), above: true }
} }
scheduleSendDoc() {
if (this.sendScheduled != null) window.clearTimeout(this.sendScheduled)
this.sendScheduled = window.setTimeout(
() => this.sendDoc(),
this.changesDelay
)
}
sendDoc() {
if (this.sendScheduled != null) {
window.clearTimeout(this.sendScheduled)
this.sendScheduled = null
}
if (!this.client.ready) return
try {
// Update the state (not the editor) with the new code.
this.client.textDocumentDidChange({
textDocument: {
uri: this.getDocUri(),
version: this.documentVersion++,
},
contentChanges: [{ text: this.view.state.doc.toString() }],
})
this.requestSemanticTokens()
this.updateFoldingRanges()
} catch (e) {
console.error(e)
}
}
ensureDocSent() {
if (this.sendScheduled != null) this.sendDoc()
}
async getFoldingRanges(): Promise<LSP.FoldingRange[] | null> { async getFoldingRanges(): Promise<LSP.FoldingRange[] | null> {
if ( if (
!this.doFoldingRanges || !this.doFoldingRanges ||
@ -284,13 +292,7 @@ export class LanguageServerPlugin implements PluginValue {
) )
return null return null
this.client.textDocumentDidChange({ this.ensureDocSent()
textDocument: {
uri: this.getDocUri(),
version: this.documentVersion++,
},
contentChanges: [{ text: this.getDocText() }],
})
const result = await this.client.textDocumentFormatting({ const result = await this.client.textDocumentFormatting({
textDocument: { uri: this.getDocUri() }, textDocument: { uri: this.getDocUri() },
@ -330,9 +332,7 @@ export class LanguageServerPlugin implements PluginValue {
) )
return null return null
this.sendChange({ this.ensureDocSent()
documentText: context.state.doc.toString(),
})
const result = await this.client.textDocumentCompletion({ const result = await this.client.textDocumentCompletion({
textDocument: { uri: this.getDocUri() }, textDocument: { uri: this.getDocUri() },

View File

@ -20,7 +20,10 @@ export default defineConfig({
/* Different amount of parallelism on CI and local. */ /* Different amount of parallelism on CI and local. */
workers: process.env.CI ? 4 : 4, workers: process.env.CI ? 4 : 4,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html', reporter: [
[process.env.CI ? 'dot' : 'list'],
['json', { outputFile: './test-results/report.json' }],
],
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: { use: {
/* Base URL to use in actions like `await page.goto('/')`. */ /* Base URL to use in actions like `await page.goto('/')`. */

65
playwrightProcess.mjs Normal file
View File

@ -0,0 +1,65 @@
import { readFileSync } from 'fs'
const data = readFileSync('./test-results/report.json', 'utf8')
// types, but was easier to store and run as normal js
// interface FailedTest {
// name: string;
// projectName: string;
// error: string;
// }
// interface Spec {
// title: string;
// tests: Test[];
// }
// interface Test {
// expectedStatus: 'passed' | 'failed' | 'pending';
// projectName: string;
// title: string;
// results: {
// status: 'passed' | 'failed' | 'pending';
// error: {stack: string}
// }[]
// }
// interface Suite {
// title: string
// suites: Suite[];
// specs: Spec[];
// }
// const processReport = (suites: Suite[]): FailedTest[] => {
// const failedTests: FailedTest[] = []
// const loopSuites = (suites: Suite[], previousName = '') => {
const processReport = (suites) => {
const failedTests = []
const loopSuites = (suites, previousName = '') => {
if (!suites) return
for (const suite of suites) {
const name = (previousName ? `${previousName} -- ` : '') + suite.title
for (const spec of suite.specs) {
for (const test of spec.tests) {
for (const result of test.results) {
if ((result.status !== 'passed') && test.expectedStatus === 'passed') {
failedTests.push({
name: (name + ' -- ' + spec.title) + (test.title ? ` -- ${test.title}` : ''),
status: result.status,
projectName: test.projectName,
error: result.error?.stack,
})
}
}
}
}
loopSuites(suite.suites, name)
}
}
loopSuites(suites)
return failedTests.map(line => JSON.stringify(line)).join('\n')
}
const failedTests = processReport(JSON.parse(data).suites)
// log to stdout to be piped to axiom
console.log(failedTests)

130
src-tauri/Cargo.lock generated
View File

@ -332,7 +332,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -361,13 +361,13 @@ checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799"
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.80" version = "0.1.81"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -407,7 +407,7 @@ checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -550,7 +550,7 @@ dependencies = [
"proc-macro-crate 3.1.0", "proc-macro-crate 3.1.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
"syn_derive", "syn_derive",
] ]
@ -792,9 +792,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.7" version = "4.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f" checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@ -802,9 +802,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.7" version = "4.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f" checksum = "6fb8393d67ba2e7bfaf28a23458e4e2b543cc73a99595511eb207fdb8aede942"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@ -816,14 +816,14 @@ dependencies = [
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.5.5" version = "4.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6" checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -1073,7 +1073,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
dependencies = [ dependencies = [
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -1083,7 +1083,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f"
dependencies = [ dependencies = [
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -1107,7 +1107,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"strsim 0.10.0", "strsim 0.10.0",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -1118,7 +1118,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
dependencies = [ dependencies = [
"darling_core", "darling_core",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -1179,7 +1179,7 @@ checksum = "4078275de501a61ceb9e759d37bdd3d7210e654dbc167ac1a3678ef4435ed57b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
"synstructure", "synstructure",
] ]
@ -1216,7 +1216,7 @@ dependencies = [
"regex", "regex",
"serde", "serde",
"serde_tokenstream", "serde_tokenstream",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -1227,7 +1227,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -1288,7 +1288,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -1320,7 +1320,7 @@ checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -1427,7 +1427,7 @@ checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -1588,7 +1588,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -1704,7 +1704,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -1980,7 +1980,7 @@ dependencies = [
"proc-macro-error", "proc-macro-error",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -2008,7 +2008,7 @@ dependencies = [
"inflections", "inflections",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -2083,7 +2083,7 @@ dependencies = [
"proc-macro-error", "proc-macro-error",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -2571,7 +2571,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-lib" name = "kcl-lib"
version = "0.1.70" version = "0.1.72"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"approx", "approx",
@ -3377,7 +3377,7 @@ dependencies = [
"regex", "regex",
"regex-syntax 0.8.3", "regex-syntax 0.8.3",
"structmeta", "structmeta",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -3496,7 +3496,7 @@ dependencies = [
"phf_shared 0.11.2", "phf_shared 0.11.2",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -3564,7 +3564,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -4438,7 +4438,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"serde_derive_internals", "serde_derive_internals",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -4523,9 +4523,9 @@ dependencies = [
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.203" version = "1.0.204"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
@ -4552,13 +4552,13 @@ dependencies = [
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.203" version = "1.0.204"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -4569,7 +4569,7 @@ checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -4602,7 +4602,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -4623,7 +4623,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"serde", "serde",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -4665,7 +4665,7 @@ dependencies = [
"darling", "darling",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -4933,7 +4933,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"structmeta-derive", "structmeta-derive",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -4944,7 +4944,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -4966,7 +4966,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"rustversion", "rustversion",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -4999,9 +4999,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.68" version = "2.0.70"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" checksum = "2f0209b68b3613b093e0ec905354eccaedcfe83b8cb37cbdeae64026c3064c16"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -5017,7 +5017,7 @@ dependencies = [
"proc-macro-error", "proc-macro-error",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -5034,7 +5034,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -5251,7 +5251,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"sha2", "sha2",
"syn 2.0.68", "syn 2.0.70",
"tauri-utils", "tauri-utils",
"thiserror", "thiserror",
"time", "time",
@ -5269,7 +5269,7 @@ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
"tauri-codegen", "tauri-codegen",
"tauri-utils", "tauri-utils",
] ]
@ -5642,7 +5642,7 @@ checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -5740,7 +5740,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -5940,7 +5940,7 @@ checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -5969,7 +5969,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -6099,7 +6099,7 @@ checksum = "c88cc88fd23b5a04528f3a8436024f20010a16ec18eb23c164b1242f65860130"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
"termcolor", "termcolor",
] ]
@ -6280,9 +6280,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.9.1" version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439" checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
dependencies = [ dependencies = [
"getrandom 0.2.14", "getrandom 0.2.14",
"serde", "serde",
@ -6316,7 +6316,7 @@ dependencies = [
"proc-macro-error", "proc-macro-error",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -6415,7 +6415,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -6449,7 +6449,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -6590,7 +6590,7 @@ checksum = "ac1345798ecd8122468840bcdf1b95e5dc6d2206c5e4b0eafa078d061f59c9bc"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -6696,7 +6696,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -6707,7 +6707,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]
@ -7159,7 +7159,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.68", "syn 2.0.70",
] ]
[[package]] [[package]]

View File

@ -80,5 +80,5 @@
} }
}, },
"productName": "Zoo Modeling App", "productName": "Zoo Modeling App",
"version": "0.24.0" "version": "0.24.1"
} }

View File

@ -39,3 +39,32 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
</AppStateContext.Provider> </AppStateContext.Provider>
) )
} }
interface AppStream {
mediaStream: MediaStream
setMediaStream: (mediaStream: MediaStream) => void
}
const AppStreamContext = createContext<AppStream>({
mediaStream: undefined as unknown as MediaStream,
setMediaStream: () => {},
})
export const useAppStream = () => useContext(AppStreamContext)
export const AppStreamProvider = ({ children }: { children: ReactNode }) => {
const [mediaStream, setMediaStream] = useState<MediaStream>(
undefined as unknown as MediaStream
)
return (
<AppStreamContext.Provider
value={{
mediaStream,
setMediaStream,
}}
>
{children}
</AppStreamContext.Provider>
)
}

View File

@ -60,7 +60,7 @@ export function Toolbar({
? send('CancelSketch') ? send('CancelSketch')
: send({ : send({
type: 'change tool', type: 'change tool',
data: 'line', data: { tool: 'line' },
}), }),
{ enabled: !disableLineButton, scopes: ['sketch'] } { enabled: !disableLineButton, scopes: ['sketch'] }
) )
@ -75,7 +75,7 @@ export function Toolbar({
? send('CancelSketch') ? send('CancelSketch')
: send({ : send({
type: 'change tool', type: 'change tool',
data: 'tangentialArc', data: { tool: 'tangentialArc' },
}), }),
{ enabled: !disableTangentialArc, scopes: ['sketch'] } { enabled: !disableTangentialArc, scopes: ['sketch'] }
) )
@ -89,7 +89,7 @@ export function Toolbar({
? send('CancelSketch') ? send('CancelSketch')
: send({ : send({
type: 'change tool', type: 'change tool',
data: 'rectangle', data: { tool: 'rectangle' },
}), }),
{ enabled: !disableRectangle, scopes: ['sketch'] } { enabled: !disableRectangle, scopes: ['sketch'] }
) )
@ -114,7 +114,7 @@ export function Toolbar({
() => () =>
commandBarSend({ commandBarSend({
type: 'Find and select command', type: 'Find and select command',
data: { name: 'Extrude', ownerMachine: 'modeling' }, data: { name: 'Extrude', groupId: 'modeling' },
}), }),
{ enabled: !disableAllButtons, scopes: ['modeling'] } { enabled: !disableAllButtons, scopes: ['modeling'] }
) )
@ -263,7 +263,7 @@ export function Toolbar({
? send('CancelSketch') ? send('CancelSketch')
: send({ : send({
type: 'change tool', type: 'change tool',
data: 'line', data: { tool: 'line' },
}) })
} }
aria-pressed={state?.matches('Sketch.Line tool')} aria-pressed={state?.matches('Sketch.Line tool')}
@ -293,7 +293,7 @@ export function Toolbar({
? send('CancelSketch') ? send('CancelSketch')
: send({ : send({
type: 'change tool', type: 'change tool',
data: 'tangentialArc', data: { tool: 'tangentialArc' },
}) })
} }
aria-pressed={state.matches('Sketch.Tangential arc to')} aria-pressed={state.matches('Sketch.Tangential arc to')}
@ -323,7 +323,7 @@ export function Toolbar({
? send('CancelSketch') ? send('CancelSketch')
: send({ : send({
type: 'change tool', type: 'change tool',
data: 'rectangle', data: { tool: 'rectangle' },
}) })
} }
aria-pressed={state.matches('Sketch.Rectangle tool')} aria-pressed={state.matches('Sketch.Rectangle tool')}
@ -378,7 +378,7 @@ export function Toolbar({
onClick={() => onClick={() =>
commandBarSend({ commandBarSend({
type: 'Find and select command', type: 'Find and select command',
data: { name: 'Extrude', ownerMachine: 'modeling' }, data: { name: 'Extrude', groupId: 'modeling' },
}) })
} }
disabled={!state.can('Extrude') || disableAllButtons} disabled={!state.can('Extrude') || disableAllButtons}

View File

@ -518,9 +518,9 @@ export class CameraControls {
direction.normalize() direction.normalize()
this.camera.position.copy(this.target).addScaledVector(direction, distance) this.camera.position.copy(this.target).addScaledVector(direction, distance)
} }
usePerspectiveCamera = async () => { usePerspectiveCamera = async (forceSend = false) => {
this._usePerspectiveCamera() this._usePerspectiveCamera()
if (this.syncDirection === 'clientToEngine') { if (forceSend || this.syncDirection === 'clientToEngine') {
await this.engineCommandManager.sendSceneCommand({ await this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd_id: uuidv4(), cmd_id: uuidv4(),

View File

@ -717,7 +717,7 @@ export const CamDebugSettings = () => {
if (camSettings.type === 'perspective') { if (camSettings.type === 'perspective') {
sceneInfra.camControls.useOrthographicCamera() sceneInfra.camControls.useOrthographicCamera()
} else { } else {
sceneInfra.camControls.usePerspectiveCamera() sceneInfra.camControls.usePerspectiveCamera(true)
} }
}} }}
/> />

View File

@ -28,6 +28,11 @@ export const CommandBarProvider = ({
Object.keys(context.selectedCommand?.args).length === 0 Object.keys(context.selectedCommand?.args).length === 0
) )
}, },
'All arguments are skippable': (context, _event) => {
return Object.values(context.selectedCommand!.args!).every(
(argConfig) => argConfig.skip
)
},
}, },
}) })

View File

@ -70,19 +70,23 @@ function CommandComboBox({
> >
{filteredOptions?.map((option) => ( {filteredOptions?.map((option) => (
<Combobox.Option <Combobox.Option
key={option.name} key={option.groupId + option.name + (option.displayName || '')}
value={option} value={option}
className="flex items-center gap-2 px-4 py-1 first:mt-2 last:mb-2 ui-active:bg-primary/10 dark:ui-active:bg-chalkboard-90" className="flex items-center gap-4 px-4 py-1.5 first:mt-2 last:mb-2 ui-active:bg-primary/10 dark:ui-active:bg-chalkboard-90"
> >
{'icon' in option && option.icon && ( {'icon' in option && option.icon && (
<CustomIcon name={option.icon} className="w-5 h-5" /> <CustomIcon name={option.icon} className="w-5 h-5" />
)} )}
<p className="flex-grow">{option.displayName || option.name} </p> <div className="flex-grow flex flex-col">
{option.description && ( <p className="my-0 leading-tight">
<p className="text-xs text-chalkboard-60 dark:text-chalkboard-40"> {option.displayName || option.name}{' '}
{option.description}
</p> </p>
)} {option.description && (
<p className="my-0 text-xs text-chalkboard-60 dark:text-chalkboard-50">
{option.description}
</p>
)}
</div>
</Combobox.Option> </Combobox.Option>
))} ))}
</Combobox.Options> </Combobox.Options>

View File

@ -131,6 +131,16 @@ const CustomIconMap = {
/> />
</svg> </svg>
), ),
code: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.7071 5L7.77734 14.7794L8.73527 15.0663L11.665 5.28698L10.7071 5ZM2.35356 9.64644L5.85362 6.14644L6.56072 6.85355L3.41423 10L6.56072 13.1464L5.85362 13.8536L2.35356 10.3536L2 10L2.35356 9.64644ZM17.0607 9.64644L13.5607 6.14644L12.8536 6.85355L16 10L12.8536 13.1464L13.5607 13.8535L17.0607 10.3536L17.4142 10L17.0607 9.64644Z"
fill="currentColor"
/>
</svg>
),
dimension: ( dimension: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path <path

View File

@ -42,7 +42,7 @@ import {
import { applyConstraintIntersect } from './Toolbar/Intersect' import { applyConstraintIntersect } from './Toolbar/Intersect'
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance' import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
import useStateMachineCommands from 'hooks/useStateMachineCommands' import useStateMachineCommands from 'hooks/useStateMachineCommands'
import { modelingMachineConfig } from 'lib/commandBarConfigs/modelingCommandConfig' import { modelingMachineCommandConfig } from 'lib/commandBarConfigs/modelingCommandConfig'
import { import {
STRAIGHT_SEGMENT, STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT, TANGENTIAL_ARC_TO_SEGMENT,
@ -920,7 +920,7 @@ export const ModelingMachineProvider = ({
state: modelingState, state: modelingState,
send: modelingSend, send: modelingSend,
actor: modelingActor, actor: modelingActor,
commandBarConfig: modelingMachineConfig, commandBarConfig: modelingMachineCommandConfig,
allCommandsRequireNetwork: true, allCommandsRequireNetwork: true,
// TODO for when sketch tools are in the toolbar: This was added when we used one "Cancel" event, // TODO for when sketch tools are in the toolbar: This was added when we used one "Cancel" event,
// but we need to support "SketchCancel" and basically // but we need to support "SketchCancel" and basically

View File

@ -82,11 +82,11 @@ function ProjectMenuPopover({
}) { }) {
const { commandBarState, commandBarSend } = useCommandsContext() const { commandBarState, commandBarSend } = useCommandsContext()
const { onProjectClose } = useLspContext() const { onProjectClose } = useLspContext()
const exportCommandInfo = { name: 'Export', ownerMachine: 'modeling' } const exportCommandInfo = { name: 'Export', groupId: 'modeling' }
const findCommand = (obj: { name: string; ownerMachine: string }) => const findCommand = (obj: { name: string; groupId: string }) =>
Boolean( Boolean(
commandBarState.context.commands.find( commandBarState.context.commands.find(
(c) => c.name === obj.name && c.ownerMachine === obj.ownerMachine (c) => c.name === obj.name && c.groupId === obj.groupId
) )
) )

View File

@ -1,3 +1,4 @@
import { DEV } from 'env'
import { MouseEventHandler, useEffect, useRef, useState } from 'react' import { MouseEventHandler, useEffect, useRef, useState } from 'react'
import { getNormalisedCoordinates } from '../lib/utils' import { getNormalisedCoordinates } from '../lib/utils'
import Loading from './Loading' import Loading from './Loading'
@ -6,9 +7,10 @@ import { useModelingContext } from 'hooks/useModelingContext'
import { useNetworkContext } from 'hooks/useNetworkContext' import { useNetworkContext } from 'hooks/useNetworkContext'
import { NetworkHealthState } from 'hooks/useNetworkStatus' import { NetworkHealthState } from 'hooks/useNetworkStatus'
import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp' import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp'
import { butName } from 'lib/cameraControls' import { btnName } from 'lib/cameraControls'
import { sendSelectEventToEngine } from 'lib/selections' import { sendSelectEventToEngine } from 'lib/selections'
import { kclManager, engineCommandManager, sceneInfra } from 'lib/singletons' import { kclManager, engineCommandManager, sceneInfra } from 'lib/singletons'
import { useAppStream } from 'AppState'
export const Stream = () => { export const Stream = () => {
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
@ -17,9 +19,12 @@ export const Stream = () => {
const videoRef = useRef<HTMLVideoElement>(null) const videoRef = useRef<HTMLVideoElement>(null)
const { settings } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
const { state, send, context } = useModelingContext() const { state, send, context } = useModelingContext()
const { mediaStream } = useAppStream()
const { overallState } = useNetworkContext() const { overallState } = useNetworkContext()
const [isFreezeFrame, setIsFreezeFrame] = useState(false) const [isFreezeFrame, setIsFreezeFrame] = useState(false)
const IDLE = true
const isNetworkOkay = const isNetworkOkay =
overallState === NetworkHealthState.Ok || overallState === NetworkHealthState.Ok ||
overallState === NetworkHealthState.Weak overallState === NetworkHealthState.Weak
@ -51,7 +56,7 @@ export const Stream = () => {
capture: true, capture: true,
}) })
const IDLE_TIME_MS = 1000 * 20 const IDLE_TIME_MS = 1000 * 60 * 2
let timeoutIdIdleA: ReturnType<typeof setTimeout> | undefined = undefined let timeoutIdIdleA: ReturnType<typeof setTimeout> | undefined = undefined
const teardown = () => { const teardown = () => {
@ -60,19 +65,21 @@ export const Stream = () => {
sceneInfra.modelingSend({ type: 'Cancel' }) sceneInfra.modelingSend({ type: 'Cancel' })
// Give video time to pause // Give video time to pause
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
engineCommandManager.engineConnection?.tearDown({ freeze: true }) engineCommandManager.tearDown()
}) })
} }
// Teardown everything if we go hidden or reconnect // Teardown everything if we go hidden or reconnect
if (globalThis?.window?.document) { if (IDLE && DEV) {
globalThis.window.document.onvisibilitychange = () => { if (globalThis?.window?.document) {
if (globalThis.window.document.visibilityState === 'hidden') { globalThis.window.document.onvisibilitychange = () => {
clearTimeout(timeoutIdIdleA) if (globalThis.window.document.visibilityState === 'hidden') {
timeoutIdIdleA = setTimeout(teardown, IDLE_TIME_MS) clearTimeout(timeoutIdIdleA)
} else if (!engineCommandManager.engineConnection?.isReady()) { timeoutIdIdleA = setTimeout(teardown, IDLE_TIME_MS)
clearTimeout(timeoutIdIdleA) } else if (!engineCommandManager.engineConnection?.isReady()) {
engineCommandManager.engineConnection?.connect(true) clearTimeout(timeoutIdIdleA)
engineCommandManager.engineConnection?.connect(true)
}
} }
} }
} }
@ -80,35 +87,44 @@ export const Stream = () => {
let timeoutIdIdleB: ReturnType<typeof setTimeout> | undefined = undefined let timeoutIdIdleB: ReturnType<typeof setTimeout> | undefined = undefined
const onAnyInput = () => { const onAnyInput = () => {
if (!engineCommandManager.engineConnection?.isReady()) {
engineCommandManager.engineConnection?.connect(true)
}
// Clear both timers // Clear both timers
clearTimeout(timeoutIdIdleA) clearTimeout(timeoutIdIdleA)
clearTimeout(timeoutIdIdleB) clearTimeout(timeoutIdIdleB)
timeoutIdIdleB = setTimeout(teardown, IDLE_TIME_MS) timeoutIdIdleB = setTimeout(teardown, IDLE_TIME_MS)
} }
globalThis?.window?.document?.addEventListener('keydown', onAnyInput) if (IDLE && DEV) {
globalThis?.window?.document?.addEventListener('mousemove', onAnyInput) globalThis?.window?.document?.addEventListener('keydown', onAnyInput)
globalThis?.window?.document?.addEventListener('mousedown', onAnyInput) globalThis?.window?.document?.addEventListener('mousemove', onAnyInput)
globalThis?.window?.document?.addEventListener('scroll', onAnyInput) globalThis?.window?.document?.addEventListener('mousedown', onAnyInput)
globalThis?.window?.document?.addEventListener('touchstart', onAnyInput) globalThis?.window?.document?.addEventListener('scroll', onAnyInput)
globalThis?.window?.document?.addEventListener('touchstart', onAnyInput)
}
timeoutIdIdleB = setTimeout(teardown, IDLE_TIME_MS) if (IDLE && DEV) {
timeoutIdIdleB = setTimeout(teardown, IDLE_TIME_MS)
}
return () => { return () => {
globalThis?.window?.document?.removeEventListener('paste', handlePaste, { globalThis?.window?.document?.removeEventListener('paste', handlePaste, {
capture: true, capture: true,
}) })
globalThis?.window?.document?.removeEventListener('keydown', onAnyInput) if (IDLE && DEV) {
globalThis?.window?.document?.removeEventListener('mousemove', onAnyInput) globalThis?.window?.document?.removeEventListener('keydown', onAnyInput)
globalThis?.window?.document?.removeEventListener('mousedown', onAnyInput) globalThis?.window?.document?.removeEventListener(
globalThis?.window?.document?.removeEventListener('scroll', onAnyInput) 'mousemove',
globalThis?.window?.document?.removeEventListener( onAnyInput
'touchstart', )
onAnyInput globalThis?.window?.document?.removeEventListener(
) 'mousedown',
onAnyInput
)
globalThis?.window?.document?.removeEventListener('scroll', onAnyInput)
globalThis?.window?.document?.removeEventListener(
'touchstart',
onAnyInput
)
}
} }
}, []) }, [])
@ -124,10 +140,10 @@ export const Stream = () => {
) )
return return
if (!videoRef.current) return if (!videoRef.current) return
if (!context.store?.mediaStream) return if (!mediaStream) return
// Do not immediately play the stream! // Do not immediately play the stream!
videoRef.current.srcObject = context.store.mediaStream videoRef.current.srcObject = mediaStream
videoRef.current.pause() videoRef.current.pause()
send({ send({
@ -136,7 +152,7 @@ export const Stream = () => {
videoElement: videoRef.current, videoElement: videoRef.current,
}, },
}) })
}, [context.store?.mediaStream]) }, [mediaStream])
const handleMouseDown: MouseEventHandler<HTMLDivElement> = (e) => { const handleMouseDown: MouseEventHandler<HTMLDivElement> = (e) => {
if (!isNetworkOkay) return if (!isNetworkOkay) return
@ -172,7 +188,7 @@ export const Stream = () => {
if (state.matches('Sketch')) return if (state.matches('Sketch')) return
if (state.matches('Sketch no face')) return if (state.matches('Sketch no face')) return
if (!context.store?.didDragInStream && butName(e).left) { if (!context.store?.didDragInStream && btnName(e).left) {
sendSelectEventToEngine( sendSelectEventToEngine(
e, e,
videoRef.current, videoRef.current,

View File

@ -195,14 +195,15 @@ export class CompletionRequester implements PluginValue {
private queuedUids: string[] = [] private queuedUids: string[] = []
private _deffererCodeUpdate = deferExecution(() => {
this.requestCompletions()
}, changesDelay)
private _deffererUserSelect = deferExecution(() => { private _deffererUserSelect = deferExecution(() => {
this.rejectSuggestionCommand() this.rejectSuggestionCommand()
}, changesDelay) }, changesDelay)
// When a doc update needs to be sent to the server, this holds the
// timeout handle for it. When null, the server has the up-to-date
// document.
private sendScheduledInput: number | null = null
constructor(readonly view: EditorView, client: LanguageServerClient) { constructor(readonly view: EditorView, client: LanguageServerClient) {
this.client = client this.client = client
} }
@ -245,7 +246,34 @@ export class CompletionRequester implements PluginValue {
} }
this.lastPos = this.view.state.selection.main.head this.lastPos = this.view.state.selection.main.head
if (viewUpdate.docChanged) this._deffererCodeUpdate(true) if (viewUpdate.docChanged) this.scheduleUpdateDoc()
}
scheduleUpdateDoc() {
if (this.sendScheduledInput != null)
window.clearTimeout(this.sendScheduledInput)
this.sendScheduledInput = window.setTimeout(
() => this.updateDoc(),
changesDelay
)
}
updateDoc() {
if (this.sendScheduledInput != null) {
window.clearTimeout(this.sendScheduledInput)
this.sendScheduledInput = null
}
if (!this.client.ready) return
try {
this.requestCompletions()
} catch (e) {
console.error(e)
}
}
ensureDocUpdated() {
if (this.sendScheduledInput != null) this.updateDoc()
} }
ghostText(): GhostText | null { ghostText(): GhostText | null {

View File

@ -27,13 +27,10 @@ export class KclPlugin implements PluginValue {
this.client = client this.client = client
} }
private _deffererCodeUpdate = deferExecution(() => { // When a doc update needs to be sent to the server, this holds the
if (this.viewUpdate === null) { // timeout handle for it. When null, the server has the up-to-date
return // document.
} private sendScheduledInput: number | null = null
kclManager.executeCode()
}, changesDelay)
private _deffererUserSelect = deferExecution(() => { private _deffererUserSelect = deferExecution(() => {
if (this.viewUpdate === null) { if (this.viewUpdate === null) {
@ -101,7 +98,34 @@ export class KclPlugin implements PluginValue {
codeManager.code = newCode codeManager.code = newCode
codeManager.writeToFile() codeManager.writeToFile()
this._deffererCodeUpdate(true) this.scheduleUpdateDoc()
}
scheduleUpdateDoc() {
if (this.sendScheduledInput != null)
window.clearTimeout(this.sendScheduledInput)
this.sendScheduledInput = window.setTimeout(
() => this.updateDoc(),
changesDelay
)
}
updateDoc() {
if (this.sendScheduledInput != null) {
window.clearTimeout(this.sendScheduledInput)
this.sendScheduledInput = null
}
if (!this.client.ready) return
try {
kclManager.executeCode()
} catch (e) {
console.error(e)
}
}
ensureDocUpdated() {
if (this.sendScheduledInput != null) this.updateDoc()
} }
async updateUnits( async updateUnits(

View File

@ -1,10 +1,10 @@
import { useLayoutEffect, useEffect, useRef } from 'react' import { useLayoutEffect, useEffect, useRef, useState } from 'react'
import { engineCommandManager, kclManager } from 'lib/singletons' import { engineCommandManager, kclManager } from 'lib/singletons'
import { deferExecution } from 'lib/utils' import { deferExecution } from 'lib/utils'
import { Themes } from 'lib/theme' import { Themes } from 'lib/theme'
import { makeDefaultPlanes, modifyGrid } from 'lang/wasm' import { makeDefaultPlanes, modifyGrid } from 'lang/wasm'
import { useModelingContext } from './useModelingContext' import { useModelingContext } from './useModelingContext'
import { useAppState } from 'AppState' import { useAppState, useAppStream } from 'AppState'
export function useSetupEngineManager( export function useSetupEngineManager(
streamRef: React.RefObject<HTMLDivElement>, streamRef: React.RefObject<HTMLDivElement>,
@ -28,9 +28,7 @@ export function useSetupEngineManager(
} }
) { ) {
const { setAppState } = useAppState() const { setAppState } = useAppState()
const { setMediaStream } = useAppStream()
const streamWidth = streamRef?.current?.offsetWidth
const streamHeight = streamRef?.current?.offsetHeight
const hasSetNonZeroDimensions = useRef<boolean>(false) const hasSetNonZeroDimensions = useRef<boolean>(false)
@ -40,59 +38,60 @@ export function useSetupEngineManager(
engineCommandManager.pool = settings.pool engineCommandManager.pool = settings.pool
} }
const startEngineInstance = () => { const startEngineInstance = (restart: boolean = false) => {
// Load the engine command manager once with the initial width and height, // Load the engine command manager once with the initial width and height,
// then we do not want to reload it. // then we do not want to reload it.
const { width: quadWidth, height: quadHeight } = getDimensions( const { width: quadWidth, height: quadHeight } = getDimensions(
streamWidth, streamRef?.current?.offsetWidth ?? 0,
streamHeight streamRef?.current?.offsetHeight ?? 0
) )
if ( if (restart) {
!hasSetNonZeroDimensions.current && kclManager.isFirstRender = false
quadHeight &&
quadWidth &&
settings.modelingSend
) {
engineCommandManager.start({
setMediaStream: (mediaStream) =>
settings.modelingSend({
type: 'Set context',
data: { mediaStream },
}),
setIsStreamReady: (isStreamReady) => setAppState({ isStreamReady }),
width: quadWidth,
height: quadHeight,
executeCode: () => {
// We only want to execute the code here that we already have set.
// Nothing else.
kclManager.isFirstRender = true
return kclManager.executeCode(true, true).then(() => {
kclManager.isFirstRender = false
})
},
token,
settings,
makeDefaultPlanes: () => {
return makeDefaultPlanes(kclManager.engineCommandManager)
},
modifyGrid: (hidden: boolean) => {
return modifyGrid(kclManager.engineCommandManager, hidden)
},
})
settings.modelingSend({
type: 'Set context',
data: {
streamDimensions: {
streamWidth: quadWidth,
streamHeight: quadHeight,
},
},
})
hasSetNonZeroDimensions.current = true
} }
engineCommandManager.start({
restart,
setMediaStream: (mediaStream) => setMediaStream(mediaStream),
setIsStreamReady: (isStreamReady) => setAppState({ isStreamReady }),
width: quadWidth,
height: quadHeight,
executeCode: () => {
// We only want to execute the code here that we already have set.
// Nothing else.
kclManager.isFirstRender = true
return kclManager.executeCode(true, true).then(() => {
kclManager.isFirstRender = false
})
},
token,
settings,
makeDefaultPlanes: () => {
return makeDefaultPlanes(kclManager.engineCommandManager)
},
modifyGrid: (hidden: boolean) => {
return modifyGrid(kclManager.engineCommandManager, hidden)
},
})
settings.modelingSend({
type: 'Set context',
data: {
streamDimensions: {
streamWidth: quadWidth,
streamHeight: quadHeight,
},
},
})
hasSetNonZeroDimensions.current = true
} }
useLayoutEffect(startEngineInstance, [ useLayoutEffect(() => {
const { width: quadWidth, height: quadHeight } = getDimensions(
streamRef?.current?.offsetWidth ?? 0,
streamRef?.current?.offsetHeight ?? 0
)
if (!hasSetNonZeroDimensions.current && quadHeight && quadWidth) {
startEngineInstance()
}
}, [
streamRef?.current?.offsetWidth, streamRef?.current?.offsetWidth,
streamRef?.current?.offsetHeight, streamRef?.current?.offsetHeight,
settings.modelingSend, settings.modelingSend,
@ -101,8 +100,8 @@ export function useSetupEngineManager(
useEffect(() => { useEffect(() => {
const handleResize = deferExecution(() => { const handleResize = deferExecution(() => {
const { width, height } = getDimensions( const { width, height } = getDimensions(
streamRef?.current?.offsetWidth, streamRef?.current?.offsetWidth ?? 0,
streamRef?.current?.offsetHeight streamRef?.current?.offsetHeight ?? 0
) )
if ( if (
settings.modelingContext.store.streamDimensions.streamWidth !== width || settings.modelingContext.store.streamDimensions.streamWidth !== width ||
@ -125,10 +124,37 @@ export function useSetupEngineManager(
}, 500) }, 500)
const onOnline = () => { const onOnline = () => {
startEngineInstance() startEngineInstance(true)
} }
const onVisibilityChange = () => {
if (window.document.visibilityState === 'visible') {
if (
!engineCommandManager.engineConnection?.isReady() &&
!engineCommandManager.engineConnection?.isConnecting()
) {
startEngineInstance()
}
}
}
window.document.addEventListener('visibilitychange', onVisibilityChange)
const onAnyInput = () => {
if (
!engineCommandManager.engineConnection?.isReady() &&
!engineCommandManager.engineConnection?.isConnecting()
) {
startEngineInstance()
}
}
window.document.addEventListener('keydown', onAnyInput)
window.document.addEventListener('mousemove', onAnyInput)
window.document.addEventListener('mousedown', onAnyInput)
window.document.addEventListener('scroll', onAnyInput)
window.document.addEventListener('touchstart', onAnyInput)
const onOffline = () => { const onOffline = () => {
kclManager.isFirstRender = true
engineCommandManager.tearDown() engineCommandManager.tearDown()
} }
@ -136,11 +162,30 @@ export function useSetupEngineManager(
window.addEventListener('offline', onOffline) window.addEventListener('offline', onOffline)
window.addEventListener('resize', handleResize) window.addEventListener('resize', handleResize)
return () => { return () => {
window.document.removeEventListener(
'visibilitychange',
onVisibilityChange
)
window.document.removeEventListener('keydown', onAnyInput)
window.document.removeEventListener('mousemove', onAnyInput)
window.document.removeEventListener('mousedown', onAnyInput)
window.document.removeEventListener('scroll', onAnyInput)
window.document.removeEventListener('touchstart', onAnyInput)
window.removeEventListener('online', onOnline) window.removeEventListener('online', onOnline)
window.removeEventListener('offline', onOffline) window.removeEventListener('offline', onOffline)
window.removeEventListener('resize', handleResize) window.removeEventListener('resize', handleResize)
} }
}, [])
// Engine relies on many settings so we should rebind events when it changes
// We have to list out the ones we care about because the settings object holds
// non-settings too...
}, [
settings.enableSSAO,
settings.highlightEdges,
settings.showScaleGrid,
settings.theme,
settings.pool,
])
} }
function getDimensions(streamWidth?: number, streamHeight?: number) { function getDimensions(streamWidth?: number, streamHeight?: number) {

View File

@ -6,7 +6,11 @@ import { modelingMachine } from 'machines/modelingMachine'
import { authMachine } from 'machines/authMachine' import { authMachine } from 'machines/authMachine'
import { settingsMachine } from 'machines/settingsMachine' import { settingsMachine } from 'machines/settingsMachine'
import { homeMachine } from 'machines/homeMachine' import { homeMachine } from 'machines/homeMachine'
import { Command, CommandSetConfig, CommandSetSchema } from 'lib/commandTypes' import {
Command,
StateMachineCommandSetConfig,
StateMachineCommandSetSchema,
} from 'lib/commandTypes'
import { useKclContext } from 'lang/KclProvider' import { useKclContext } from 'lang/KclProvider'
import { useNetworkContext } from 'hooks/useNetworkContext' import { useNetworkContext } from 'hooks/useNetworkContext'
import { NetworkHealthState } from 'hooks/useNetworkStatus' import { NetworkHealthState } from 'hooks/useNetworkStatus'
@ -21,20 +25,20 @@ export type AllMachines =
interface UseStateMachineCommandsArgs< interface UseStateMachineCommandsArgs<
T extends AllMachines, T extends AllMachines,
S extends CommandSetSchema<T> S extends StateMachineCommandSetSchema<T>
> { > {
machineId: T['id'] machineId: T['id']
state: StateFrom<T> state: StateFrom<T>
send: Function send: Function
actor: InterpreterFrom<T> actor: InterpreterFrom<T>
commandBarConfig?: CommandSetConfig<T, S> commandBarConfig?: StateMachineCommandSetConfig<T, S>
allCommandsRequireNetwork?: boolean allCommandsRequireNetwork?: boolean
onCancel?: () => void onCancel?: () => void
} }
export default function useStateMachineCommands< export default function useStateMachineCommands<
T extends AnyStateMachine, T extends AnyStateMachine,
S extends CommandSetSchema<T> S extends StateMachineCommandSetSchema<T>
>({ >({
machineId, machineId,
state, state,
@ -58,9 +62,10 @@ export default function useStateMachineCommands<
const newCommands = state.nextEvents const newCommands = state.nextEvents
.filter((_) => !allCommandsRequireNetwork || !disableAllButtons) .filter((_) => !allCommandsRequireNetwork || !disableAllButtons)
.filter((e) => !['done.', 'error.'].some((n) => e.includes(n))) .filter((e) => !['done.', 'error.'].some((n) => e.includes(n)))
.map((type) => .flatMap((type) =>
createMachineCommand<T, S>({ createMachineCommand<T, S>({
ownerMachine: machineId, // The group is the owner machine's ID.
groupId: machineId,
type, type,
state, state,
send, send,

View File

@ -13,6 +13,7 @@ import {
UpdaterRestartModal, UpdaterRestartModal,
createUpdaterRestartModal, createUpdaterRestartModal,
} from 'components/UpdaterRestartModal' } from 'components/UpdaterRestartModal'
import { AppStreamProvider } from 'AppState'
// uncomment for xstate inspector // uncomment for xstate inspector
// import { DEV } from 'env' // import { DEV } from 'env'
@ -26,28 +27,30 @@ const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
root.render( root.render(
<HotkeysProvider> <HotkeysProvider>
<Router /> <AppStreamProvider>
<Toaster <Router />
position="bottom-center" <Toaster
toastOptions={{ position="bottom-center"
style: { toastOptions={{
borderRadius: '3px', style: {
}, borderRadius: '3px',
className:
'bg-chalkboard-10 dark:bg-chalkboard-90 text-chalkboard-110 dark:text-chalkboard-10 rounded-sm border-chalkboard-20/50 dark:border-chalkboard-80/50',
success: {
iconTheme: {
primary: 'oklch(89% 0.16 143.4deg)',
secondary: 'oklch(48.62% 0.1654 142.5deg)',
}, },
duration: className:
window?.localStorage.getItem('playwright') === 'true' 'bg-chalkboard-10 dark:bg-chalkboard-90 text-chalkboard-110 dark:text-chalkboard-10 rounded-sm border-chalkboard-20/50 dark:border-chalkboard-80/50',
? 10 // speed up e2e tests success: {
: 1500, iconTheme: {
}, primary: 'oklch(89% 0.16 143.4deg)',
}} secondary: 'oklch(48.62% 0.1654 142.5deg)',
/> },
<ModalContainer /> duration:
window?.localStorage.getItem('playwright') === 'true'
? 10 // speed up e2e tests
: 1500,
},
}}
/>
<ModalContainer />
</AppStreamProvider>
</HotkeysProvider> </HotkeysProvider>
) )

View File

@ -3,6 +3,8 @@ import { createContext, useContext, useEffect, useState } from 'react'
import { type IndexLoaderData } from 'lib/types' import { type IndexLoaderData } from 'lib/types'
import { useLoaderData } from 'react-router-dom' import { useLoaderData } from 'react-router-dom'
import { codeManager, kclManager } from 'lib/singletons' import { codeManager, kclManager } from 'lib/singletons'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { Command } from 'lib/commandTypes'
const KclContext = createContext({ const KclContext = createContext({
code: codeManager?.code || '', code: codeManager?.code || '',
@ -35,6 +37,7 @@ export function KclContextProvider({
const [errors, setErrors] = useState<KCLError[]>([]) const [errors, setErrors] = useState<KCLError[]>([])
const [logs, setLogs] = useState<string[]>([]) const [logs, setLogs] = useState<string[]>([])
const [wasmInitFailed, setWasmInitFailed] = useState(false) const [wasmInitFailed, setWasmInitFailed] = useState(false)
const { commandBarSend } = useCommandsContext()
useEffect(() => { useEffect(() => {
codeManager.registerCallBacks({ codeManager.registerCallBacks({
@ -50,6 +53,28 @@ export function KclContextProvider({
}) })
}, []) }, [])
// Add format code to command palette.
useEffect(() => {
const commands: Command[] = [
{
name: 'format-code',
displayName: 'Format Code',
description: 'Nicely formats the KCL code in the editor.',
needsReview: false,
groupId: 'code',
icon: 'code',
onSubmit: (data) => {
kclManager.format()
},
},
]
commandBarSend({ type: 'Add commands', data: { commands } })
return () => {
commandBarSend({ type: 'Remove commands', data: { commands } })
}
}, [kclManager, commandBarSend])
return ( return (
<KclContext.Provider <KclContext.Provider
value={{ value={{

View File

@ -302,6 +302,30 @@ class EngineConnection extends EventTarget {
mediaStream?: MediaStream mediaStream?: MediaStream
freezeFrame: boolean = false freezeFrame: boolean = false
onIceCandidate = function (
this: RTCPeerConnection,
event: RTCPeerConnectionIceEvent
) {}
onIceCandidateError = function (
this: RTCPeerConnection,
event: RTCPeerConnectionIceErrorEvent
) {}
onConnectionStateChange = function (this: RTCPeerConnection, event: Event) {}
onDataChannelOpen = function (this: RTCDataChannel, event: Event) {}
onDataChannelClose = function (this: RTCDataChannel, event: Event) {}
onDataChannelError = function (this: RTCDataChannel, event: Event) {}
onDataChannelMessage = function (this: RTCDataChannel, event: MessageEvent) {}
onDataChannel = function (
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 = { private _state: EngineConnectionState = {
type: EngineConnectionStateType.Fresh, type: EngineConnectionStateType.Fresh,
} }
@ -346,6 +370,7 @@ class EngineConnection extends EventTarget {
private engineCommandManager: EngineCommandManager private engineCommandManager: EngineCommandManager
private pingPongSpan: { ping?: Date; pong?: Date } private pingPongSpan: { ping?: Date; pong?: Date }
private pingIntervalId: ReturnType<typeof setInterval>
constructor({ constructor({
engineCommandManager, engineCommandManager,
@ -368,7 +393,7 @@ class EngineConnection extends EventTarget {
// Without an interval ping, our connection will timeout. // Without an interval ping, our connection will timeout.
// If this.freezeFrame is true we skip this logic so only reconnect // If this.freezeFrame is true we skip this logic so only reconnect
// happens on mouse move // happens on mouse move
setInterval(() => { this.pingIntervalId = setInterval(() => {
if (this.freezeFrame) return if (this.freezeFrame) return
switch (this.state.type as EngineConnectionStateType) { switch (this.state.type as EngineConnectionStateType) {
@ -434,6 +459,44 @@ class EngineConnection extends EventTarget {
tearDown(opts?: { freeze: boolean }) { tearDown(opts?: { freeze: boolean }) {
this.freezeFrame = opts?.freeze ?? false this.freezeFrame = opts?.freeze ?? false
this.disconnectAll() this.disconnectAll()
clearInterval(this.pingIntervalId)
this.pc?.removeEventListener('icecandidate', this.onIceCandidate)
this.pc?.removeEventListener('icecandidateerror', this.onIceCandidateError)
this.pc?.removeEventListener(
'connectionstatechange',
this.onConnectionStateChange
)
this.pc?.removeEventListener('track', this.onTrack)
this.unreliableDataChannel?.removeEventListener(
'open',
this.onDataChannelOpen
)
this.unreliableDataChannel?.removeEventListener(
'close',
this.onDataChannelClose
)
this.unreliableDataChannel?.removeEventListener(
'error',
this.onDataChannelError
)
this.unreliableDataChannel?.removeEventListener(
'message',
this.onDataChannelMessage
)
this.pc?.removeEventListener('datachannel', this.onDataChannel)
this.websocket?.removeEventListener('open', this.onWebSocketOpen)
this.websocket?.removeEventListener('close', this.onWebSocketClose)
this.websocket?.removeEventListener('error', this.onWebSocketError)
this.websocket?.removeEventListener('message', this.onWebSocketMessage)
window.removeEventListener(
'use-network-status-ready',
this.onNetworkStatusReady
)
this.state = { this.state = {
type: EngineConnectionStateType.Disconnecting, type: EngineConnectionStateType.Disconnecting,
value: { type: DisconnectingType.Quit }, value: { type: DisconnectingType.Quit },
@ -477,7 +540,7 @@ class EngineConnection extends EventTarget {
}, },
} }
this.pc.addEventListener('icecandidate', (event) => { this.onIceCandidate = (event: RTCPeerConnectionIceEvent) => {
if (event.candidate === null) { if (event.candidate === null) {
return return
} }
@ -499,18 +562,20 @@ class EngineConnection extends EventTarget {
usernameFragment: event.candidate.usernameFragment || undefined, usernameFragment: event.candidate.usernameFragment || undefined,
}, },
}) })
}) }
this.pc.addEventListener('icecandidate', this.onIceCandidate)
this.pc.addEventListener('icecandidateerror', (_event: Event) => { this.onIceCandidateError = (_event: Event) => {
const event = _event as RTCPeerConnectionIceErrorEvent const event = _event as RTCPeerConnectionIceErrorEvent
console.warn( console.warn(
`ICE candidate returned an error: ${event.errorCode}: ${event.errorText} for ${event.url}` `ICE candidate returned an error: ${event.errorCode}: ${event.errorText} for ${event.url}`
) )
}) }
this.pc.addEventListener('icecandidateerror', this.onIceCandidateError)
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/connectionstatechange_event // https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/connectionstatechange_event
// Event type: generic Event type... // Event type: generic Event type...
this.pc.addEventListener('connectionstatechange', (event: any) => { this.onConnectionStateChange = (event: any) => {
console.log('connectionstatechange: ' + event.target?.connectionState) console.log('connectionstatechange: ' + event.target?.connectionState)
switch (event.target?.connectionState) { switch (event.target?.connectionState) {
// From what I understand, only after have we done the ICE song and // From what I understand, only after have we done the ICE song and
@ -539,9 +604,13 @@ class EngineConnection extends EventTarget {
default: default:
break break
} }
}) }
this.pc.addEventListener(
'connectionstatechange',
this.onConnectionStateChange
)
this.pc.addEventListener('track', (event) => { this.onTrack = (event) => {
const mediaStream = event.streams[0] const mediaStream = event.streams[0]
this.state = { this.state = {
@ -625,9 +694,10 @@ class EngineConnection extends EventTarget {
// to pass it to the rest of the application. // to pass it to the rest of the application.
this.mediaStream = mediaStream this.mediaStream = mediaStream
}) }
this.pc.addEventListener('track', this.onTrack)
this.pc.addEventListener('datachannel', (event) => { this.onDataChannel = (event) => {
this.unreliableDataChannel = event.channel this.unreliableDataChannel = event.channel
this.state = { this.state = {
@ -638,7 +708,7 @@ class EngineConnection extends EventTarget {
}, },
} }
this.unreliableDataChannel.addEventListener('open', (event) => { this.onDataChannelOpen = (event) => {
this.state = { this.state = {
type: EngineConnectionStateType.Connecting, type: EngineConnectionStateType.Connecting,
value: { value: {
@ -654,14 +724,22 @@ class EngineConnection extends EventTarget {
this.dispatchEvent( this.dispatchEvent(
new CustomEvent(EngineConnectionEvents.Opened, { detail: this }) new CustomEvent(EngineConnectionEvents.Opened, { detail: this })
) )
}) }
this.unreliableDataChannel?.addEventListener(
'open',
this.onDataChannelOpen
)
this.unreliableDataChannel.addEventListener('close', (event) => { this.onDataChannelClose = (event) => {
this.disconnectAll() this.disconnectAll()
this.finalizeIfAllConnectionsClosed() this.finalizeIfAllConnectionsClosed()
}) }
this.unreliableDataChannel?.addEventListener(
'close',
this.onDataChannelClose
)
this.unreliableDataChannel.addEventListener('error', (event) => { this.onDataChannelError = (event) => {
this.disconnectAll() this.disconnectAll()
this.state = { this.state = {
@ -674,8 +752,13 @@ class EngineConnection extends EventTarget {
}, },
}, },
} }
}) }
this.unreliableDataChannel.addEventListener('message', (event) => { this.unreliableDataChannel?.addEventListener(
'error',
this.onDataChannelError
)
this.onDataChannelMessage = (event) => {
const result: UnreliableResponses = JSON.parse(event.data) const result: UnreliableResponses = JSON.parse(event.data)
Object.values( Object.values(
this.engineCommandManager.unreliableSubscriptions[result.type] || {} this.engineCommandManager.unreliableSubscriptions[result.type] || {}
@ -697,8 +780,13 @@ class EngineConnection extends EventTarget {
} }
} }
) )
}) }
}) this.unreliableDataChannel.addEventListener(
'message',
this.onDataChannelMessage
)
}
this.pc.addEventListener('datachannel', this.onDataChannel)
} }
const createWebSocketConnection = () => { const createWebSocketConnection = () => {
@ -712,7 +800,7 @@ class EngineConnection extends EventTarget {
this.websocket = new WebSocket(this.url, []) this.websocket = new WebSocket(this.url, [])
this.websocket.binaryType = 'arraybuffer' this.websocket.binaryType = 'arraybuffer'
this.websocket.addEventListener('open', (event) => { this.onWebSocketOpen = (event) => {
this.state = { this.state = {
type: EngineConnectionStateType.Connecting, type: EngineConnectionStateType.Connecting,
value: { value: {
@ -733,14 +821,16 @@ class EngineConnection extends EventTarget {
// Send an initial ping // Send an initial ping
this.send({ type: 'ping' }) this.send({ type: 'ping' })
this.pingPongSpan.ping = new Date() this.pingPongSpan.ping = new Date()
}) }
this.websocket.addEventListener('open', this.onWebSocketOpen)
this.websocket.addEventListener('close', (event) => { this.onWebSocketClose = (event) => {
this.disconnectAll() this.disconnectAll()
this.finalizeIfAllConnectionsClosed() this.finalizeIfAllConnectionsClosed()
}) }
this.websocket.addEventListener('close', this.onWebSocketClose)
this.websocket.addEventListener('error', (event) => { this.onWebSocketError = (event) => {
this.disconnectAll() this.disconnectAll()
this.state = { this.state = {
@ -753,9 +843,10 @@ class EngineConnection extends EventTarget {
}, },
}, },
} }
}) }
this.websocket.addEventListener('error', this.onWebSocketError)
this.websocket.addEventListener('message', (event) => { this.onWebSocketMessage = (event) => {
// In the EngineConnection, we're looking for messages to/from // In the EngineConnection, we're looking for messages to/from
// the server that relate to the ICE handshake, or WebRTC // the server that relate to the ICE handshake, or WebRTC
// negotiation. There may be other messages (including ArrayBuffer // negotiation. There may be other messages (including ArrayBuffer
@ -960,15 +1051,20 @@ class EngineConnection extends EventTarget {
}) })
break break
} }
}) }
this.websocket.addEventListener('message', this.onWebSocketMessage)
} }
if (reconnecting) { if (reconnecting) {
createWebSocketConnection() createWebSocketConnection()
} else { } else {
window.addEventListener('use-network-status-ready', () => { this.onNetworkStatusReady = () => {
createWebSocketConnection() createWebSocketConnection()
}) }
window.addEventListener(
'use-network-status-ready',
this.onNetworkStatusReady
)
} }
} }
// Do not change this back to an object or any, we should only be sending the // Do not change this back to an object or any, we should only be sending the
@ -1154,7 +1250,15 @@ export class EngineCommandManager extends EventTarget {
private makeDefaultPlanes: () => Promise<DefaultPlanes> | null = () => null private makeDefaultPlanes: () => Promise<DefaultPlanes> | null = () => null
private modifyGrid: (hidden: boolean) => Promise<void> | null = () => null private modifyGrid: (hidden: boolean) => Promise<void> | null = () => null
private onEngineConnectionOpened = () => {}
private onEngineConnectionClosed = () => {}
private onEngineConnectionStarted = ({ detail: engineConnection }: any) => {}
private onEngineConnectionNewTrack = ({
detail,
}: CustomEvent<NewTrackArgs>) => {}
start({ start({
restart,
setMediaStream, setMediaStream,
setIsStreamReady, setIsStreamReady,
width, width,
@ -1170,6 +1274,7 @@ export class EngineCommandManager extends EventTarget {
showScaleGrid: false, showScaleGrid: false,
}, },
}: { }: {
restart?: boolean
setMediaStream: (stream: MediaStream) => void setMediaStream: (stream: MediaStream) => void
setIsStreamReady: (isStreamReady: boolean) => void setIsStreamReady: (isStreamReady: boolean) => void
width: number width: number
@ -1215,162 +1320,168 @@ export class EngineCommandManager extends EventTarget {
}) })
) )
this.onEngineConnectionOpened = () => {
// Set the stream background color
// This takes RGBA values from 0-1
// So we convert from the conventional 0-255 found in Figma
void this.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'set_background_color',
color: getThemeColorForEngine(settings.theme),
},
})
// Sets the default line colors
const opposingTheme = getOppositeTheme(settings.theme)
this.sendSceneCommand({
cmd_id: uuidv4(),
type: 'modeling_cmd_req',
cmd: {
type: 'set_default_system_properties',
color: getThemeColorForEngine(opposingTheme),
},
})
// Set the edge lines visibility
this.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'edge_lines_visible' as any, // TODO: update kittycad.ts to use the correct type
hidden: !settings.highlightEdges,
},
})
this._camControlsCameraChange()
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 want modify the grid first because we don't want it to flash.
// Ideally these would already be default hidden in engine (TODO do
// that) https://github.com/KittyCAD/engine/issues/2282
this.modifyGrid(!settings.showScaleGrid)?.then(async () => {
await this.initPlanes()
this.resolveReady()
setIsStreamReady(true)
await executeCode()
})
}
this.engineConnection.addEventListener( this.engineConnection.addEventListener(
EngineConnectionEvents.Opened, EngineConnectionEvents.Opened,
() => { this.onEngineConnectionOpened
// Set the stream background color
// This takes RGBA values from 0-1
// So we convert from the conventional 0-255 found in Figma
void this.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'set_background_color',
color: getThemeColorForEngine(settings.theme),
},
})
// Sets the default line colors
const opposingTheme = getOppositeTheme(settings.theme)
this.sendSceneCommand({
cmd_id: uuidv4(),
type: 'modeling_cmd_req',
cmd: {
type: 'set_default_system_properties',
color: getThemeColorForEngine(opposingTheme),
},
})
// Set the edge lines visibility
this.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'edge_lines_visible' as any, // TODO: update kittycad.ts to use the correct type
hidden: !settings.highlightEdges,
},
})
this._camControlsCameraChange()
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 want modify the grid first because we don't want it to flash.
// Ideally these would already be default hidden in engine (TODO do
// that) https://github.com/KittyCAD/engine/issues/2282
this.modifyGrid(!settings.showScaleGrid)?.then(async () => {
await this.initPlanes()
this.resolveReady()
setIsStreamReady(true)
await executeCode()
})
}
) )
this.onEngineConnectionClosed = () => {
setIsStreamReady(false)
}
this.engineConnection.addEventListener( this.engineConnection.addEventListener(
EngineConnectionEvents.Closed, EngineConnectionEvents.Closed,
() => { this.onEngineConnectionClosed
setIsStreamReady(false)
}
) )
this.engineConnection.addEventListener( this.onEngineConnectionStarted = ({ detail: engineConnection }: any) => {
EngineConnectionEvents.ConnectionStarted, engineConnection?.pc?.addEventListener(
({ detail: engineConnection }: any) => { 'datachannel',
engineConnection?.pc?.addEventListener( (event: RTCDataChannelEvent) => {
'datachannel', let unreliableDataChannel = event.channel
(event: RTCDataChannelEvent) => {
let unreliableDataChannel = event.channel
unreliableDataChannel.addEventListener( unreliableDataChannel.addEventListener(
'message', 'message',
(event: MessageEvent) => { (event: MessageEvent) => {
const result: UnreliableResponses = JSON.parse(event.data) const result: UnreliableResponses = JSON.parse(event.data)
Object.values( Object.values(
this.unreliableSubscriptions[result.type] || {} this.unreliableSubscriptions[result.type] || {}
).forEach( ).forEach(
// TODO: There is only one response that uses the unreliable channel atm, // TODO: There is only one response that uses the unreliable channel atm,
// highlight_set_entity, if there are more it's likely they will all have the same // highlight_set_entity, if there are more it's likely they will all have the same
// sequence logic, but I'm not sure if we use a single global sequence or a sequence // sequence logic, but I'm not sure if we use a single global sequence or a sequence
// per unreliable subscription. // per unreliable subscription.
(callback) => { (callback) => {
let data = result?.data let data = result?.data
if (isHighlightSetEntity_type(data)) { if (isHighlightSetEntity_type(data)) {
if ( if (
data.sequence !== undefined && data.sequence !== undefined &&
data.sequence > this.inSequence data.sequence > this.inSequence
) { ) {
this.inSequence = data.sequence this.inSequence = data.sequence
callback(result) callback(result)
}
} }
} }
) }
}
)
}
)
// When the EngineConnection starts a connection, we want to register
// callbacks into the WebSocket/PeerConnection.
engineConnection.websocket?.addEventListener('message', ((
event: MessageEvent
) => {
if (event.data instanceof ArrayBuffer) {
// If the data is an ArrayBuffer, it's the result of an export command,
// because in all other cases we send JSON strings. But in the case of
// export we send a binary blob.
// Pass this to our export function.
exportSave(event.data).then(() => {
this.pendingExport?.resolve()
}, this.pendingExport?.reject)
} else {
const message: Models['WebSocketResponse_type'] = JSON.parse(
event.data
)
if (
message.success &&
(message.resp.type === 'modeling' ||
message.resp.type === 'modeling_batch') &&
message.request_id
) {
this.handleModelingCommand(
message.resp,
message.request_id,
message
) )
} else if (
!message.success &&
message.request_id &&
this.artifactMap[message.request_id]
) {
this.handleFailedModelingCommand(message.request_id, message)
} }
)
}
)
// When the EngineConnection starts a connection, we want to register
// callbacks into the WebSocket/PeerConnection.
engineConnection.websocket?.addEventListener('message', ((
event: MessageEvent
) => {
if (event.data instanceof ArrayBuffer) {
// If the data is an ArrayBuffer, it's the result of an export command,
// because in all other cases we send JSON strings. But in the case of
// export we send a binary blob.
// Pass this to our export function.
exportSave(event.data).then(() => {
this.pendingExport?.resolve()
}, this.pendingExport?.reject)
} else {
const message: Models['WebSocketResponse_type'] = JSON.parse(
event.data
)
if (
message.success &&
(message.resp.type === 'modeling' ||
message.resp.type === 'modeling_batch') &&
message.request_id
) {
this.handleModelingCommand(
message.resp,
message.request_id,
message
)
} else if (
!message.success &&
message.request_id &&
this.artifactMap[message.request_id]
) {
this.handleFailedModelingCommand(message.request_id, message)
} }
}) as EventListener) }
}) as EventListener)
this.engineConnection?.addEventListener( this.onEngineConnectionNewTrack = ({
EngineConnectionEvents.NewTrack, detail: { mediaStream },
(({ detail: { mediaStream } }: CustomEvent<NewTrackArgs>) => { }: CustomEvent<NewTrackArgs>) => {
mediaStream.getVideoTracks()[0].addEventListener('mute', () => { mediaStream.getVideoTracks()[0].addEventListener('mute', () => {
console.error( console.error(
'video track mute: check webrtc internals -> inbound rtp' 'video track mute: check webrtc internals -> inbound rtp'
) )
}) })
setMediaStream(mediaStream) setMediaStream(mediaStream)
}) as EventListener
)
this.engineConnection?.connect()
} }
this.engineConnection?.addEventListener(
EngineConnectionEvents.NewTrack,
this.onEngineConnectionNewTrack as EventListener
)
this.engineConnection?.connect()
}
this.engineConnection.addEventListener(
EngineConnectionEvents.ConnectionStarted,
this.onEngineConnectionStarted
) )
} }
@ -1629,7 +1740,26 @@ export class EngineCommandManager extends EventTarget {
} }
tearDown() { tearDown() {
if (this.engineConnection) { if (this.engineConnection) {
this.engineConnection.removeEventListener(
EngineConnectionEvents.Opened,
this.onEngineConnectionOpened
)
this.engineConnection.removeEventListener(
EngineConnectionEvents.Closed,
this.onEngineConnectionClosed
)
this.engineConnection.removeEventListener(
EngineConnectionEvents.ConnectionStarted,
this.onEngineConnectionStarted
)
this.engineConnection.removeEventListener(
EngineConnectionEvents.NewTrack,
this.onEngineConnectionNewTrack as EventListener
)
this.engineConnection?.tearDown() this.engineConnection?.tearDown()
this.engineConnection = undefined
// Our window.tearDown assignment causes this case to happen which is // Our window.tearDown assignment causes this case to happen which is
// only really for tests. // only really for tests.
// @ts-ignore // @ts-ignore

View File

@ -64,7 +64,7 @@ export interface MouseGuard {
rotate: MouseGuardHandler rotate: MouseGuardHandler
} }
export const butName = (e: React.MouseEvent) => ({ export const btnName = (e: React.MouseEvent) => ({
middle: !!(e.buttons & 4) || e.button === 1, middle: !!(e.buttons & 4) || e.button === 1,
right: !!(e.buttons & 2) || e.button === 2, right: !!(e.buttons & 2) || e.button === 2,
left: !!(e.buttons & 1) || e.button === 0, left: !!(e.buttons & 1) || e.button === 0,
@ -75,8 +75,8 @@ export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
pan: { pan: {
description: 'Right click + Shift + drag or middle click + drag', description: 'Right click + Shift + drag or middle click + drag',
callback: (e) => callback: (e) =>
(butName(e).middle && noModifiersPressed(e)) || (btnName(e).middle && noModifiersPressed(e)) ||
(butName(e).right && e.shiftKey), (btnName(e).right && e.shiftKey),
}, },
zoom: { zoom: {
description: 'Scroll wheel or Right click + Ctrl + drag', description: 'Scroll wheel or Right click + Ctrl + drag',
@ -85,15 +85,15 @@ export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
}, },
rotate: { rotate: {
description: 'Right click + drag', description: 'Right click + drag',
callback: (e) => butName(e).right && noModifiersPressed(e), callback: (e) => btnName(e).right && noModifiersPressed(e),
}, },
}, },
OnShape: { OnShape: {
pan: { pan: {
description: 'Right click + Ctrl + drag or middle click + drag', description: 'Right click + Ctrl + drag or middle click + drag',
callback: (e) => callback: (e) =>
(butName(e).right && e.ctrlKey) || (btnName(e).right && e.ctrlKey) ||
(butName(e).middle && noModifiersPressed(e)), (btnName(e).middle && noModifiersPressed(e)),
}, },
zoom: { zoom: {
description: 'Scroll wheel', description: 'Scroll wheel',
@ -102,72 +102,72 @@ export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
}, },
rotate: { rotate: {
description: 'Right click + drag', description: 'Right click + drag',
callback: (e) => butName(e).right && noModifiersPressed(e), callback: (e) => btnName(e).right && noModifiersPressed(e),
}, },
}, },
'Trackpad Friendly': { 'Trackpad Friendly': {
pan: { pan: {
description: 'Left click + Alt + Shift + drag or middle click + drag', description: 'Left click + Alt + Shift + drag or middle click + drag',
callback: (e) => callback: (e) =>
(butName(e).left && e.altKey && e.shiftKey && !e.metaKey) || (btnName(e).left && e.altKey && e.shiftKey && !e.metaKey) ||
(butName(e).middle && noModifiersPressed(e)), (btnName(e).middle && noModifiersPressed(e)),
}, },
zoom: { zoom: {
description: 'Scroll wheel or Left click + Alt + OS + drag', description: 'Scroll wheel or Left click + Alt + OS + drag',
dragCallback: (e) => butName(e).left && e.altKey && e.metaKey, dragCallback: (e) => btnName(e).left && e.altKey && e.metaKey,
scrollCallback: () => true, scrollCallback: () => true,
}, },
rotate: { rotate: {
description: 'Left click + Alt + drag', description: 'Left click + Alt + drag',
callback: (e) => butName(e).left && e.altKey && !e.shiftKey && !e.metaKey, callback: (e) => btnName(e).left && e.altKey && !e.shiftKey && !e.metaKey,
lenientDragStartButton: 0, lenientDragStartButton: 0,
}, },
}, },
Solidworks: { Solidworks: {
pan: { pan: {
description: 'Right click + Ctrl + drag', description: 'Right click + Ctrl + drag',
callback: (e) => butName(e).right && e.ctrlKey, callback: (e) => btnName(e).right && e.ctrlKey,
lenientDragStartButton: 2, lenientDragStartButton: 2,
}, },
zoom: { zoom: {
description: 'Scroll wheel or Middle click + Shift + drag', description: 'Scroll wheel or Middle click + Shift + drag',
dragCallback: (e) => butName(e).middle && e.shiftKey, dragCallback: (e) => btnName(e).middle && e.shiftKey,
scrollCallback: () => true, scrollCallback: () => true,
}, },
rotate: { rotate: {
description: 'Middle click + drag', description: 'Middle click + drag',
callback: (e) => butName(e).middle && noModifiersPressed(e), callback: (e) => btnName(e).middle && noModifiersPressed(e),
}, },
}, },
NX: { NX: {
pan: { pan: {
description: 'Middle click + Shift + drag', description: 'Middle click + Shift + drag',
callback: (e) => butName(e).middle && e.shiftKey, callback: (e) => btnName(e).middle && e.shiftKey,
}, },
zoom: { zoom: {
description: 'Scroll wheel or Middle click + Ctrl + drag', description: 'Scroll wheel or Middle click + Ctrl + drag',
dragCallback: (e) => butName(e).middle && e.ctrlKey, dragCallback: (e) => btnName(e).middle && e.ctrlKey,
scrollCallback: () => true, scrollCallback: () => true,
}, },
rotate: { rotate: {
description: 'Middle click + drag', description: 'Middle click + drag',
callback: (e) => butName(e).middle && noModifiersPressed(e), callback: (e) => btnName(e).middle && noModifiersPressed(e),
}, },
}, },
Creo: { Creo: {
pan: { pan: {
description: 'Left click + Ctrl + drag', description: 'Left click + Ctrl + drag',
callback: (e) => butName(e).left && !butName(e).right && e.ctrlKey, callback: (e) => btnName(e).left && !btnName(e).right && e.ctrlKey,
}, },
zoom: { zoom: {
description: 'Scroll wheel or Right click + Ctrl + drag', description: 'Scroll wheel or Right click + Ctrl + drag',
dragCallback: (e) => butName(e).right && !butName(e).left && e.ctrlKey, dragCallback: (e) => btnName(e).right && !btnName(e).left && e.ctrlKey,
scrollCallback: () => true, scrollCallback: () => true,
}, },
rotate: { rotate: {
description: 'Middle (or Left + Right) click + Ctrl + drag', description: 'Middle (or Left + Right) click + Ctrl + drag',
callback: (e) => { callback: (e) => {
const b = butName(e) const b = btnName(e)
return (b.middle || (b.left && b.right)) && e.ctrlKey return (b.middle || (b.left && b.right)) && e.ctrlKey
}, },
}, },
@ -175,7 +175,7 @@ export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
AutoCAD: { AutoCAD: {
pan: { pan: {
description: 'Middle click + drag', description: 'Middle click + drag',
callback: (e) => butName(e).middle && noModifiersPressed(e), callback: (e) => btnName(e).middle && noModifiersPressed(e),
}, },
zoom: { zoom: {
description: 'Scroll wheel', description: 'Scroll wheel',
@ -184,7 +184,7 @@ export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
}, },
rotate: { rotate: {
description: 'Middle click + Shift + drag', description: 'Middle click + Shift + drag',
callback: (e) => butName(e).middle && e.shiftKey, callback: (e) => btnName(e).middle && e.shiftKey,
}, },
}, },
} }

View File

@ -1,9 +1,9 @@
import { CommandSetConfig } from 'lib/commandTypes' import { StateMachineCommandSetConfig } from 'lib/commandTypes'
import { authMachine } from 'machines/authMachine' import { authMachine } from 'machines/authMachine'
type AuthCommandSchema = {} type AuthCommandSchema = {}
export const authCommandBarConfig: CommandSetConfig< export const authCommandBarConfig: StateMachineCommandSetConfig<
typeof authMachine, typeof authMachine,
AuthCommandSchema AuthCommandSchema
> = { > = {

View File

@ -1,4 +1,4 @@
import { CommandSetConfig } from 'lib/commandTypes' import { StateMachineCommandSetConfig } from 'lib/commandTypes'
import { homeMachine } from 'machines/homeMachine' import { homeMachine } from 'machines/homeMachine'
export type HomeCommandSchema = { export type HomeCommandSchema = {
@ -17,7 +17,7 @@ export type HomeCommandSchema = {
} }
} }
export const homeCommandBarConfig: CommandSetConfig< export const homeCommandBarConfig: StateMachineCommandSetConfig<
typeof homeMachine, typeof homeMachine,
HomeCommandSchema HomeCommandSchema
> = { > = {

View File

@ -1,8 +1,8 @@
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { CommandSetConfig, KclCommandValue } from 'lib/commandTypes' import { StateMachineCommandSetConfig, KclCommandValue } from 'lib/commandTypes'
import { KCL_DEFAULT_LENGTH } from 'lib/constants' import { KCL_DEFAULT_LENGTH } from 'lib/constants'
import { Selections } from 'lib/selections' import { Selections } from 'lib/selections'
import { modelingMachine } from 'machines/modelingMachine' import { modelingMachine, SketchTool } from 'machines/modelingMachine'
type OutputFormat = Models['OutputFormat_type'] type OutputFormat = Models['OutputFormat_type']
type OutputTypeKey = OutputFormat['type'] type OutputTypeKey = OutputFormat['type']
@ -27,9 +27,12 @@ export type ModelingCommandSchema = {
// result: (typeof EXTRUSION_RESULTS)[number] // result: (typeof EXTRUSION_RESULTS)[number]
distance: KclCommandValue distance: KclCommandValue
} }
'change tool': {
tool: SketchTool
}
} }
export const modelingMachineConfig: CommandSetConfig< export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
typeof modelingMachine, typeof modelingMachine,
ModelingCommandSchema ModelingCommandSchema
> = { > = {
@ -37,22 +40,47 @@ export const modelingMachineConfig: CommandSetConfig<
description: 'Enter sketch mode.', description: 'Enter sketch mode.',
icon: 'sketch', icon: 'sketch',
}, },
// TODO the event is no 'change tool' with data: 'line', 'rectangle' etc 'change tool': [
// 'Equip Line tool': { {
// description: 'Start drawing straight lines.', description: 'Start drawing straight lines.',
// icon: 'line', icon: 'line',
// displayName: 'Line', displayName: 'Line',
// }, args: {
// 'Equip tangential arc to': { tool: {
// description: 'Start drawing an arc tangent to the current segment.', defaultValue: 'line',
// icon: 'arc', required: true,
// displayName: 'Tangential Arc', skip: true,
// }, inputType: 'string',
// 'Equip rectangle tool': { },
// description: 'Start drawing a rectangle.', },
// icon: 'rectangle', },
// displayName: 'Rectangle', {
// }, description: 'Start drawing an arc tangent to the current segment.',
icon: 'arc',
displayName: 'Tangential Arc',
args: {
tool: {
defaultValue: 'tangentialArc',
required: true,
skip: true,
inputType: 'string',
},
},
},
{
description: 'Start drawing a rectangle.',
icon: 'rectangle',
displayName: 'Rectangle',
args: {
tool: {
defaultValue: 'rectangle',
required: true,
skip: true,
inputType: 'string',
},
},
},
],
Export: { Export: {
description: 'Export the current model.', description: 'Export the current model.',
icon: 'exportFile', icon: 'exportFile',

View File

@ -124,7 +124,8 @@ export function createSettingsCommand({
displayName: `Settings · ${decamelize(type.replaceAll('.', ' · '), { displayName: `Settings · ${decamelize(type.replaceAll('.', ' · '), {
separator: ' ', separator: ' ',
})}`, })}`,
ownerMachine: 'settings', description: settingConfig.description,
groupId: 'settings',
icon: 'settings', icon: 'settings',
needsReview: false, needsReview: false,
onSubmit: (data) => { onSubmit: (data) => {

View File

@ -33,13 +33,13 @@ export interface KclExpressionWithVariable extends KclExpression {
export type KclCommandValue = KclExpression | KclExpressionWithVariable export type KclCommandValue = KclExpression | KclExpressionWithVariable
export type CommandInputType = (typeof INPUT_TYPES)[number] export type CommandInputType = (typeof INPUT_TYPES)[number]
export type CommandSetSchema<T extends AnyStateMachine> = Partial<{ export type StateMachineCommandSetSchema<T extends AnyStateMachine> = Partial<{
[EventType in EventFrom<T>['type']]: Record<string, any> [EventType in EventFrom<T>['type']]: Record<string, any>
}> }>
export type CommandSet< export type StateMachineCommandSet<
T extends AllMachines, T extends AllMachines,
Schema extends CommandSetSchema<T> Schema extends StateMachineCommandSetSchema<T>
> = Partial<{ > = Partial<{
[EventType in EventFrom<T>['type']]: Command< [EventType in EventFrom<T>['type']]: Command<
T, T,
@ -48,24 +48,28 @@ export type CommandSet<
> >
}> }>
export type CommandSetConfig< /**
* A configuration object for a set of commands tied to a state machine.
* Each event type can have one or more commands associated with it.
* @param T The state machine type.
* @param Schema The schema for the command set, defined by the developer.
*/
export type StateMachineCommandSetConfig<
T extends AllMachines, T extends AllMachines,
Schema extends CommandSetSchema<T> Schema extends StateMachineCommandSetSchema<T>
> = Partial<{ > = Partial<{
[EventType in EventFrom<T>['type']]: CommandConfig< [EventType in EventFrom<T>['type']]:
T, | CommandConfig<T, EventFrom<T>['type'], Schema[EventType]>
EventFrom<T>['type'], | CommandConfig<T, EventFrom<T>['type'], Schema[EventType]>[]
Schema[EventType]
>
}> }>
export type Command< export type Command<
T extends AnyStateMachine = AnyStateMachine, T extends AnyStateMachine = AnyStateMachine,
CommandName extends EventFrom<T>['type'] = EventFrom<T>['type'], CommandName extends EventFrom<T>['type'] = EventFrom<T>['type'],
CommandSchema extends CommandSetSchema<T>[CommandName] = CommandSetSchema<T>[CommandName] CommandSchema extends StateMachineCommandSetSchema<T>[CommandName] = StateMachineCommandSetSchema<T>[CommandName]
> = { > = {
name: CommandName name: CommandName
ownerMachine: T['id'] groupId: T['id']
needsReview: boolean needsReview: boolean
onSubmit: (data?: CommandSchema) => void onSubmit: (data?: CommandSchema) => void
onCancel?: () => void onCancel?: () => void
@ -81,10 +85,10 @@ export type Command<
export type CommandConfig< export type CommandConfig<
T extends AnyStateMachine = AnyStateMachine, T extends AnyStateMachine = AnyStateMachine,
CommandName extends EventFrom<T>['type'] = EventFrom<T>['type'], CommandName extends EventFrom<T>['type'] = EventFrom<T>['type'],
CommandSchema extends CommandSetSchema<T>[CommandName] = CommandSetSchema<T>[CommandName] CommandSchema extends StateMachineCommandSetSchema<T>[CommandName] = StateMachineCommandSetSchema<T>[CommandName]
> = Omit< > = Omit<
Command<T, CommandName, CommandSchema>, Command<T, CommandName, CommandSchema>,
'name' | 'ownerMachine' | 'onSubmit' | 'onCancel' | 'args' | 'needsReview' 'name' | 'groupId' | 'onSubmit' | 'onCancel' | 'args' | 'needsReview'
> & { > & {
needsReview?: true needsReview?: true
args?: { args?: {

View File

@ -11,20 +11,20 @@ import {
CommandArgument, CommandArgument,
CommandArgumentConfig, CommandArgumentConfig,
CommandConfig, CommandConfig,
CommandSetConfig, StateMachineCommandSetConfig,
CommandSetSchema, StateMachineCommandSetSchema,
} from './commandTypes' } from './commandTypes'
interface CreateMachineCommandProps< interface CreateMachineCommandProps<
T extends AnyStateMachine, T extends AnyStateMachine,
S extends CommandSetSchema<T> S extends StateMachineCommandSetSchema<T>
> { > {
type: EventFrom<T>['type'] type: EventFrom<T>['type']
ownerMachine: T['id'] groupId: T['id']
state: StateFrom<T> state: StateFrom<T>
send: Function send: Function
actor: InterpreterFrom<T> actor: InterpreterFrom<T>
commandBarConfig?: CommandSetConfig<T, S> commandBarConfig?: StateMachineCommandSetConfig<T, S>
onCancel?: () => void onCancel?: () => void
} }
@ -32,22 +32,39 @@ interface CreateMachineCommandProps<
// from a more terse Command Bar Meta definition. // from a more terse Command Bar Meta definition.
export function createMachineCommand< export function createMachineCommand<
T extends AnyStateMachine, T extends AnyStateMachine,
S extends CommandSetSchema<T> S extends StateMachineCommandSetSchema<T>
>({ >({
ownerMachine, groupId,
type, type,
state, state,
send, send,
actor, actor,
commandBarConfig, commandBarConfig,
onCancel, onCancel,
}: CreateMachineCommandProps<T, S>): Command< }: CreateMachineCommandProps<T, S>):
T, | Command<T, typeof type, S[typeof type]>
typeof type, | Command<T, typeof type, S[typeof type]>[]
S[typeof type] | null {
> | null {
const commandConfig = commandBarConfig && commandBarConfig[type] const commandConfig = commandBarConfig && commandBarConfig[type]
if (!commandConfig) return null // There may be no command config for this event type,
// or there may be multiple commands to create.
if (!commandConfig) {
return null
} else if (commandConfig instanceof Array) {
return commandConfig
.map((config) =>
createMachineCommand({
groupId,
type,
state,
send,
actor,
commandBarConfig: { [type]: config },
onCancel,
})
)
.filter((c) => c !== null) as Command<T, typeof type, S[typeof type]>[]
}
// Hide commands based on platform by returning `null` // Hide commands based on platform by returning `null`
// so the consumer can filter them out // so the consumer can filter them out
@ -62,8 +79,9 @@ export function createMachineCommand<
const command: Command<T, typeof type, S[typeof type]> = { const command: Command<T, typeof type, S[typeof type]> = {
name: type, name: type,
ownerMachine: ownerMachine, groupId,
icon, icon,
description: commandConfig.description,
needsReview: commandConfig.needsReview || false, needsReview: commandConfig.needsReview || false,
onSubmit: (data?: S[typeof type]) => { onSubmit: (data?: S[typeof type]) => {
if (data !== undefined && data !== null) { if (data !== undefined && data !== null) {
@ -84,6 +102,10 @@ export function createMachineCommand<
command.onCancel = onCancel command.onCancel = onCancel
} }
if ('displayName' in commandConfig) {
command.displayName = commandConfig.displayName
}
return command return command
} }
@ -92,7 +114,7 @@ export function createMachineCommand<
// bundled together into the args for a Command. // bundled together into the args for a Command.
function buildCommandArguments< function buildCommandArguments<
T extends AnyStateMachine, T extends AnyStateMachine,
S extends CommandSetSchema<T>, S extends StateMachineCommandSetSchema<T>,
CommandName extends EventFrom<T>['type'] = EventFrom<T>['type'] CommandName extends EventFrom<T>['type'] = EventFrom<T>['type']
>( >(
state: StateFrom<T>, state: StateFrom<T>,
@ -112,7 +134,7 @@ function buildCommandArguments<
export function buildCommandArgument< export function buildCommandArgument<
T extends AnyStateMachine, T extends AnyStateMachine,
O extends CommandSetSchema<T> = CommandSetSchema<T> O extends StateMachineCommandSetSchema<T> = StateMachineCommandSetSchema<T>
>( >(
arg: CommandArgumentConfig<O, T>, arg: CommandArgumentConfig<O, T>,
context: ContextFrom<T>, context: ContextFrom<T>,

View File

@ -57,7 +57,7 @@ export type CommandBarMachineEvent =
} }
| { | {
type: 'Find and select command' type: 'Find and select command'
data: { name: string; ownerMachine: string } data: { name: string; groupId: string }
} }
| { | {
type: 'Change current argument' type: 'Change current argument'
@ -66,7 +66,7 @@ export type CommandBarMachineEvent =
export const commandBarMachine = createMachine( export const commandBarMachine = createMachine(
{ {
/** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAJwA6AGwAmAKwBmBoukAWafIAcDcSoA0IAJ6JZDaZIDs8hgzV6AjA61a1DWQF8PhtJlwFiciowUkYWJBAOWB4+AQiRBFF5CwdpcVkHS1lpVyU5QxNEh1lFGTUsrUUtOQd5SwZLLx8MbDwiUkkqGkgyAHk2MBwwwSiY-kEEswZJbPltM0U3eXLZAsRFeQcrerUHRbTFvfkmkF9WgI6u2ggyADFONv98WkowAGNufDeW-2GI0d443iiHESkktUUilkskqdiUWjWCAcDC0kjUqnElkc6lkoK0JzOT0CnWo1zIAEEIARvn48LA-uwuIC4qAEqIHJjJPJcYtxFoYdJFFjVsZEG5pqCquJxJoGvJxOUCT82sSrj0AEpgdCoABuYC+yog9OYIyZsQmYmhWzMmTqLnU0kyikRGVRbj5lg2SmUam5StpFxIkgAymBXh8HlADQGyKHw58aecGZEzUDWYgchY7CisWoSlpQQ5EVDLJJxKkdIpyypUop-ed2kHCW0Xu9uD1kwDzcCEJCtlUNgWhZZ1OlnaKEBpZJItHVzDZ5xlpPWiZdDc8w22Ow5wozosyLUiVNMSg5ytVKmfrIipzO564z2otPVpI1vKd18SAOJYbgACzAEhI3wUgoAAV3QQZuFgCg-1wGAvjAkgSCgkCSHAyCcG4TtUxZYRED2TQZGSOw80qaQ7ARCc9mmZYSksextGkNJBRXFUOh-f9AOA0CIKgmCABE4E3D5oyTE1-lww8clKBjZF2Bh5SFZwXUUyRVDzJwNE2LQzzYwNJE4gCgJwKNeMw6DJHJAB3LAYlM-AHjYMDuFjMCACN0B4NCMKgnD927dMkWSUs8yY8RISfWQshvcsrDlapshULJPHfZsDKM7iHPM-jJAANSwShOAgX9IzICB+DASQHh1VAAGsqp1Qrit-MByXQvisP8sY8ISZwbCsDllBLSwz1qRFBQsRZ5WRIVkui-TG0M39jJ4jqLNgfLmpK3hTLIQCSFQIM2EoX8ADMjvQSQmqKna2vWvyJL3HrD1SEoyzSdJUkUm9dnUtQn10aK6hkxbiU1HVODAay3M87zE1+J6UwCtN8IQUauR0OZ0ksFwUUqRFPWmUdouPXR3TBjoIahmHKQIHKuqRrtUYSaVShhLF+TkeTzBFQosVKDRM2RVInEdSmg2p6GyE1bU9R8zrsKZqSexFqQHWlHNXHzCbFhnX1+UydFkVxiXJClmGAFEIG8hmld3ZGXp7TR5C5RSCxdjlQV1tR9bqaVdhsSFxDN5AALeOrgPa3ysJgiqcCqmr6sa7bWujxXjQd5nesQZYthsblIQYJx6hUic9AsdEFT2HQzByVLmgDJaw-eSOHPTjbysq6qcFqhrrtT9sO-4ugd1NFGc4QXEPo5eVLGsFxlnKREXGnZI9HcT1mOikO0s-S5w7bqNh9j-bkKOyQTvOy6B9utOHtj7qD1V8pSyrHJZ3STY4QmjQ0R2Ww0oSjuF3u+HAqAIBwEEOlRs48nZBVENkKQNprC42qBoJ0LothaXMJ9aElRXDLj3k3VUpJIBwOfgg4oMx8y41SPUOY8p5DFk2GiKoxsoRVmhGoM2cY2zAQRngChgU0a7HZtFH0ilRp1D5usVh6JXCKD2CUdI8lQ7rlbB8chkkJ6HkxFsBeulbQZAUCwrYCiqzIhyE+fqZtMomTMg-aCwiWYEV2NOBUCghQ6EFGzYsvpwQUT5MxawFECx2JWllRxMdLI2TsrtKMTkXIuMnmeCiZYwrePMFWCKv1XZKVGroWwmxNARK4g4hWG0tp3wSSk16lQpC41ULjV8uxUjSBvFCNEDTdCOkWCY44xCGzgzAJDaGdTnaZHBADYcqg5DpCovzeRno5izA0BeUOh8o5OPgDo+BaNvqV0xNFaE7gdYTnnvkmUChdKEMyF4LwQA */ /** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAJwA6AGwAmAKwBmBoukAWafIAcDcSoA0IAJ6JZDaZIDs8hgzV6AjA61a1DWQF8PhtJlwFiciowUkYWJBAOWB4+AQiRBFF5CwdpcVkHS1lpVyU5QxNEh1lFGTUsrUUtOQd5SwZLLx8MbDwiUkkqGkgyAHk2MBwwwSiY-kEEswZJbPltM0U3eXLZAsRFeQcrerUHRbTFvfkmkF9WgI6u2ggyADFONv98WkowAGNufDeW-2GI0d443iiHESkktUUilkskqdiUWjWCAcDC0kjUqnElkc6lkoK0JzOT0CnWo1zIAEEIARvn48LA-uwuIC4qAEqIHJjJPJcYtxFoYdJFFjVsZEG5pqCquJxJoGvJxOUCT82sSrj0AEpgdCoABuYC+yog9OYIyZsQmYmhWzMmTqLnU0kyikRGVRbj5lg2SmUam5StpFxIkgAymBXh8HlADQGyKHw58aecGZEzUDWYgchY7CisWoSlpQQ5EVDLJJxKkdIpyypUop-ed2kHCW0Xu9uD1kwDzcCEJCtlUNgWhZZ1OlnaKEBpZJItHVzDZ5xlpPWiZdDc8w22O7JwozosyLUj3KiZZY85YtMUNgx5Ii81JuS5xBtPVCCyuVR0AOJYbgACzAEhI3wUgoAAV3QQZuFgCg-1wGAvjAkgSCgkCSHAyCcG4TtUxZYRED2TQZGSOw80qaQ7ARCc9mmZYSjPPFpDSQUP0DSQf3-QDgNAiCoJggAROBNw+aMkxNf5cMPHJSjPWRdhvZ9LGcF0b0kVQ8ycDRNkvNRWMbdjfwAoCcCjHjMOgyRyQAdywGITPwB42DA7hYzAgAjdAeDQjCoJw-du3TJE6mnWwoT0NIUTUAs71sMtdGsRYCyUtI9OJDijO49DeKw2BJAANSwShOAgX9IzICB+DASQHh1VAAGsqp1Qrit-MBySy8y-LGPCElSeoy2lZJcwcNRfXHQpBWmWQsRsPYdDPZJUu-QyuPssy+Py5qSt4EyyEAkhUCDNhKF-AAzQ70EkJqiu2tqOt88S926w9UhhLlykWXFHXcTJixsd60hHGxNj2Jag01HVODAKzXI8rzE1+R6U38tN8IQJSuR0OZ0gvHQXHGxBPWmUdppUWwFV0MHJAhqGYcpAh1qwrqDx7ZFajUmVpulEbqlqREsfBTQ0jcPM5P5KmaehshNW1PVvOy7Cka7VHeoYDkyjSPQtAvPMqkRQU1BnX1+UydFkQvCWwEhqWAFEIC8xnFd3ZHntZuTDZG4ob1nGxPTUfXrBnS8nDmEpT2ObxTnXVUALeOrgPanycvKyrqpwWqGqurbWsThXjWd5WesQZYtmBkplCceplIncK0SrXYqnccxxcj5s2OQWP4-s3PzJgiqcCqmr6sa7P2x7vi6AcAvJJ7XEy0dUFMWsFxlnKfn+S5CL3E9Jiuapjv3i7qNx+T-bDskY6zourObpz+6cuZgK0Y5Ua0TqEvXDkJR-YnR0s1xjkIMnDln3uuVsHwOxT1NCjIuR5nCSBUExJi1huRqyooUTYpYnzeyUFkKsy4Tg4FQBAOAgg26Nmga7QKohshSBtNYXGDonQumtPYaQfsSjLBHDefepJICUJZtQ4oMx8wXj6jebGt4JwbC2OiVwig9hh2hLpVu0cOhxjbMBBGeABFPwSA3ERRMlhKWCn9WRVQzZQirMo0BAYNzxn4RJGBh5MRbGXsHawGQFBmLrpUasOQorOAjs0OxaUVrGVMvfaCuiVYEV2KWTYg1yxyA0i6ZYaIPSL3lOkS8wSo6hOWpxCJ8te6WRsnZKMjlnIxNgSNIiiTF6vVSdI9JM1taXlxLzTwqiClBnSqtSJScLIFVvjtKANSXquENqoJwtgyZzRFIUQ4MhqgNACdNTQCpLbWyshMnsjpSwjXRJYHecgcmImsLIz0odNAaGqPiHpDYY6HwTlE+ATiqHPwohYewWQshmGhHrCcJz5Bck5toZwGhMheC8EAA */
predictableActionArguments: true, predictableActionArguments: true,
tsTypes: {} as import('./commandBarMachine.typegen').Typegen0, tsTypes: {} as import('./commandBarMachine.typegen').Typegen0,
context: { context: {
@ -120,9 +120,7 @@ export const commandBarMachine = createMachine(
context.commands.filter( context.commands.filter(
(c) => (c) =>
!event.data.commands.some( !event.data.commands.some(
(c2) => (c2) => c2.name === c.name && c2.groupId === c.groupId
c2.name === c.name &&
c2.ownerMachine === c.ownerMachine
) )
), ),
}), }),
@ -149,6 +147,10 @@ export const commandBarMachine = createMachine(
cond: 'Command has no arguments', cond: 'Command has no arguments',
actions: ['Execute command'], actions: ['Execute command'],
}, },
{
target: 'Checking Arguments',
cond: 'All arguments are skippable',
},
{ {
target: 'Gathering arguments', target: 'Gathering arguments',
actions: ['Set current argument to first non-skippable'], actions: ['Set current argument to first non-skippable'],
@ -393,9 +395,7 @@ export const commandBarMachine = createMachine(
selectedCommand: (c, e) => { selectedCommand: (c, e) => {
if (e.type !== 'Find and select command') return c.selectedCommand if (e.type !== 'Find and select command') return c.selectedCommand
const found = c.commands.find( const found = c.commands.find(
(cmd) => (cmd) => cmd.name === e.data.name && cmd.groupId === e.data.groupId
cmd.name === e.data.name &&
cmd.ownerMachine === e.data.ownerMachine
) )
return !!found ? found : c.selectedCommand return !!found ? found : c.selectedCommand
@ -514,7 +514,9 @@ export const commandBarMachine = createMachine(
) )
function sortCommands(a: Command, b: Command) { function sortCommands(a: Command, b: Command) {
if (b.ownerMachine === 'auth') return -1 if (b.groupId === 'auth' && !(a.groupId === 'auth')) return -2
if (a.ownerMachine === 'auth') return 1 if (a.groupId === 'auth' && !(b.groupId === 'auth')) return 2
if (b.groupId === 'settings' && !(a.groupId === 'settings')) return -1
if (a.groupId === 'settings' && !(b.groupId === 'settings')) return 1
return a.name.localeCompare(b.name) return a.name.localeCompare(b.name)
} }

File diff suppressed because one or more lines are too long

View File

@ -57,6 +57,9 @@ const Home = () => {
kclManager.cancelAllExecutions() kclManager.cancelAllExecutions()
}, []) }, [])
useHotkeys('backspace', (e) => {
e.preventDefault()
})
useHotkeys( useHotkeys(
isTauri() ? 'mod+,' : 'shift+mod+,', isTauri() ? 'mod+,' : 'shift+mod+,',
() => navigate(paths.HOME + paths.SETTINGS), () => navigate(paths.HOME + paths.SETTINGS),

View File

@ -1842,7 +1842,7 @@ dependencies = [
"bincode", "bincode",
"either", "either",
"fnv", "fnv",
"itertools 0.10.5", "itertools 0.12.1",
"lazy_static", "lazy_static",
"nom", "nom",
"quick-xml", "quick-xml",
@ -2928,18 +2928,18 @@ dependencies = [
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.61" version = "1.0.62"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.61" version = "1.0.62"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View File

@ -35,7 +35,7 @@ schemars = { version = "0.8.17", features = ["impl_json_schema", "url", "uuid1"]
serde = { version = "1.0.204", features = ["derive"] } serde = { version = "1.0.204", features = ["derive"] }
serde_json = "1.0.120" serde_json = "1.0.120"
sha2 = "0.10.8" sha2 = "0.10.8"
thiserror = "1.0.61" thiserror = "1.0.62"
toml = "0.8.14" toml = "0.8.14"
ts-rs = { version = "9.0.1", features = ["uuid-impl", "url-impl", "chrono-impl", "no-serde-warnings", "serde-json-impl"] } ts-rs = { version = "9.0.1", features = ["uuid-impl", "url-impl", "chrono-impl", "no-serde-warnings", "serde-json-impl"] }
url = { version = "2.5.2", features = ["serde"] } url = { version = "2.5.2", features = ["serde"] }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 333 KiB

After

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 195 KiB

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