Compare commits

...

18 Commits

Author SHA1 Message Date
01eedd7f71 Fix CI action for checking if Cargo.lock is stale
It was falsely passing GH Action CI even when it shouldn't have.
2024-07-13 15:12:36 -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
61 changed files with 1212 additions and 567 deletions

View File

@ -63,5 +63,4 @@ jobs:
# If this fails, run "cargo check" to update Cargo.lock,
# then add Cargo.lock to the PR.
- name: Check Cargo.lock doesn't need updating
run: |
cargo check --locked || echo "Pls run cargo check and commit the changed Cargo.lock"
run: "./scripts/check-tauri-lockfile.sh"

View File

@ -13,6 +13,7 @@ on:
# Will checkout the last commit from the default branch (main as of 2023-10-04)
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')) }}
concurrency:
@ -110,8 +111,14 @@ jobs:
echo "$(jq --arg name 'Zoo Modeling App (Nightly)' \
'.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
if: github.event_name == 'schedule'
if: ${{ github.event_name == 'schedule' || env.CUT_RELEASE_PR == 'true' }}
with:
path: |
package.json
@ -377,6 +384,30 @@ jobs:
E2E_TAURI_ENABLED: true
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:
permissions:
contents: write

View File

@ -83,6 +83,20 @@ jobs:
uses: Swatinem/rust-cache@v2
with:
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)
if: needs.check-rust-changes.outputs.rust-changed == 'true'
run: yarn build:wasm
@ -139,27 +153,60 @@ jobs:
with:
name: test-results-ubuntu-${{ github.sha }}
path: test-results/
- name: Run ubuntu/chrome flow retry failures
- name: Run ubuntu/chrome flow (with retries)
id: retry
if: always()
run: |
if [[ -d "test-results" ]];
then if [[ $(ls -1 "test-results" | wc -l) != "0" ]];
then echo "retried=true" >> $GITHUB_OUTPUT;
else echo "retried=false" >> $GITHUB_OUTPUT; exit 0;
fi;
else echo "retried=false" >> $GITHUB_OUTPUT; exit 0;
fi;
yarn playwright test --project="Google Chrome" --last-failed e2e/playwright/flow-tests.spec.ts
env:
CI: true
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
- name: Run ubuntu/chrome flow
if: steps.retry.outputs.retried == 'false'
run: yarn playwright test --project="Google Chrome" e2e/playwright/flow-tests.spec.ts
if [[ ! -f "test-results/.last-run.json" ]]; then
# if no last run artifact, than run plawright normally
echo "run playwright normally"
yarn playwright test --project="Google Chrome" e2e/playwright/flow-tests.spec.ts || true
# # send to axiom
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
fi
retry=1
max_retrys=4
# retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues
while [[ $retry -le $max_retrys ]]; do
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:
CI: true
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
if: always()
with:
@ -226,6 +273,20 @@ jobs:
uses: Swatinem/rust-cache@v2
with:
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)
if: needs.check-rust-changes.outputs.rust-changed == 'true'
run: yarn build:wasm
@ -241,26 +302,52 @@ jobs:
with:
name: test-results-macos-${{ github.sha }}
path: test-results/
- name: Run macos/safari flow retry failures
- name: Run macos/safari flow (with retries)
id: retry
if: always()
run: |
if [[ -d "test-results" ]];
then if [[ $(ls -1 "test-results" | wc -l) != "0" ]];
then echo "retried=true" >> $GITHUB_OUTPUT;
else echo "retried=false" >> $GITHUB_OUTPUT; exit 0;
fi;
else echo "retried=false" >> $GITHUB_OUTPUT; exit 0;
fi;
yarn playwright test --project="webkit" --last-failed e2e/playwright/flow-tests.spec.ts
env:
CI: true
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
- name: Run macos/safari flow
if: steps.retry.outputs.retried == 'false'
# webkit doesn't work on Ubuntu because of the same reason tauri doesn't (webRTC issues)
# TODO remove this and the matrix and run all tests on ubuntu when this is fixed
run: yarn playwright test --project="webkit" e2e/playwright/flow-tests.spec.ts
if [[ ! -f "test-results/.last-run.json" ]]; then
# if no last run artifact, than run plawright normally
echo "run playwright normally"
yarn playwright test --project="webkit" e2e/playwright/flow-tests.spec.ts || true
# # send to axiom
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
fi
retry=1
max_retrys=4
# retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues
while [[ $retry -le $max_retrys ]]; do
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="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:
CI: true
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
)
})
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', () => {
@ -7726,6 +7766,31 @@ test('Basic default modeling and sketch hotkeys work', async ({ page }) => {
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.setTimeout(90_000)
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",
"version": "0.24.0",
"version": "0.24.1",
"private": true,
"dependencies": {
"@codemirror/autocomplete": "^6.17.0",

View File

@ -93,23 +93,10 @@ export class LanguageServerPlugin implements PluginValue {
private doSemanticTokens: boolean = false
private doFoldingRanges: boolean = false
private _defferer = deferExecution((code: string) => {
try {
// Update the state (not the editor) with the new code.
this.client.textDocumentDidChange({
textDocument: {
uri: this.getDocUri(),
version: this.documentVersion++,
},
contentChanges: [{ text: code }],
})
this.requestSemanticTokens()
this.updateFoldingRanges()
} catch (e) {
console.error(e)
}
}, this.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 sendScheduled: number | null = null
constructor(options: LanguageServerOptions, private view: EditorView) {
this.client = options.client
@ -152,14 +139,9 @@ export class LanguageServerPlugin implements PluginValue {
}
update(viewUpdate: ViewUpdate) {
// If the doc didn't change we can return early.
if (!viewUpdate.docChanged) {
return
if (viewUpdate.docChanged) {
this.scheduleSendDoc()
}
this.sendChange({
documentText: viewUpdate.state.doc.toString(),
})
}
destroy() {
@ -184,16 +166,6 @@ export class LanguageServerPlugin implements PluginValue {
this.updateFoldingRanges()
}
async sendChange({ documentText }: { documentText: string }) {
if (!this.client.ready) return
this._defferer(documentText)
}
requestDiagnostics() {
this.sendChange({ documentText: this.getDocText() })
}
async requestHoverTooltip(
view: EditorView,
{ line, character }: { line: number; character: number }
@ -204,7 +176,7 @@ export class LanguageServerPlugin implements PluginValue {
)
return null
this.sendChange({ documentText: this.getDocText() })
this.ensureDocSent()
const result = await this.client.textDocumentHover({
textDocument: { uri: this.getDocUri() },
position: { line, character },
@ -227,6 +199,42 @@ export class LanguageServerPlugin implements PluginValue {
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> {
if (
!this.doFoldingRanges ||
@ -284,13 +292,7 @@ export class LanguageServerPlugin implements PluginValue {
)
return null
this.client.textDocumentDidChange({
textDocument: {
uri: this.getDocUri(),
version: this.documentVersion++,
},
contentChanges: [{ text: this.getDocText() }],
})
this.ensureDocSent()
const result = await this.client.textDocumentFormatting({
textDocument: { uri: this.getDocUri() },
@ -330,9 +332,7 @@ export class LanguageServerPlugin implements PluginValue {
)
return null
this.sendChange({
documentText: context.state.doc.toString(),
})
this.ensureDocSent()
const result = await this.client.textDocumentCompletion({
textDocument: { uri: this.getDocUri() },

View File

@ -20,7 +20,10 @@ export default defineConfig({
/* Different amount of parallelism on CI and local. */
workers: process.env.CI ? 4 : 4,
/* 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. */
use: {
/* 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)

View File

@ -0,0 +1,8 @@
DIR=src-tauri
cd $DIR
if cargo check --locked ; then
echo "Seems $DIR/Cargo.lock is up to date."
else
echo "Pls run cargo check and commit the changed $DIR/Cargo.lock, because it's out of date."
fi
cd -

130
src-tauri/Cargo.lock generated
View File

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

View File

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

View File

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

View File

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

View File

@ -28,6 +28,11 @@ export const CommandBarProvider = ({
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) => (
<Combobox.Option
key={option.name}
key={option.groupId + option.name + (option.displayName || '')}
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 && (
<CustomIcon name={option.icon} className="w-5 h-5" />
)}
<p className="flex-grow">{option.displayName || option.name} </p>
{option.description && (
<p className="text-xs text-chalkboard-60 dark:text-chalkboard-40">
{option.description}
<div className="flex-grow flex flex-col">
<p className="my-0 leading-tight">
{option.displayName || option.name}{' '}
</p>
)}
{option.description && (
<p className="my-0 text-xs text-chalkboard-60 dark:text-chalkboard-50">
{option.description}
</p>
)}
</div>
</Combobox.Option>
))}
</Combobox.Options>

View File

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

View File

@ -42,7 +42,7 @@ import {
import { applyConstraintIntersect } from './Toolbar/Intersect'
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
import useStateMachineCommands from 'hooks/useStateMachineCommands'
import { modelingMachineConfig } from 'lib/commandBarConfigs/modelingCommandConfig'
import { modelingMachineCommandConfig } from 'lib/commandBarConfigs/modelingCommandConfig'
import {
STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT,
@ -920,7 +920,7 @@ export const ModelingMachineProvider = ({
state: modelingState,
send: modelingSend,
actor: modelingActor,
commandBarConfig: modelingMachineConfig,
commandBarConfig: modelingMachineCommandConfig,
allCommandsRequireNetwork: true,
// 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

View File

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

View File

@ -195,14 +195,15 @@ export class CompletionRequester implements PluginValue {
private queuedUids: string[] = []
private _deffererCodeUpdate = deferExecution(() => {
this.requestCompletions()
}, changesDelay)
private _deffererUserSelect = deferExecution(() => {
this.rejectSuggestionCommand()
}, 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) {
this.client = client
}
@ -245,7 +246,34 @@ export class CompletionRequester implements PluginValue {
}
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 {

View File

@ -27,13 +27,10 @@ export class KclPlugin implements PluginValue {
this.client = client
}
private _deffererCodeUpdate = deferExecution(() => {
if (this.viewUpdate === null) {
return
}
kclManager.executeCode()
}, 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
private _deffererUserSelect = deferExecution(() => {
if (this.viewUpdate === null) {
@ -101,7 +98,34 @@ export class KclPlugin implements PluginValue {
codeManager.code = newCode
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(

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 { deferExecution } from 'lib/utils'
import { Themes } from 'lib/theme'
import { makeDefaultPlanes, modifyGrid } from 'lang/wasm'
import { useModelingContext } from './useModelingContext'
import { useAppState } from 'AppState'
import { useAppState, useAppStream } from 'AppState'
export function useSetupEngineManager(
streamRef: React.RefObject<HTMLDivElement>,
@ -28,9 +28,7 @@ export function useSetupEngineManager(
}
) {
const { setAppState } = useAppState()
const streamWidth = streamRef?.current?.offsetWidth
const streamHeight = streamRef?.current?.offsetHeight
const { setMediaStream } = useAppStream()
const hasSetNonZeroDimensions = useRef<boolean>(false)
@ -40,59 +38,60 @@ export function useSetupEngineManager(
engineCommandManager.pool = settings.pool
}
const startEngineInstance = () => {
const startEngineInstance = (restart: boolean = false) => {
// Load the engine command manager once with the initial width and height,
// then we do not want to reload it.
const { width: quadWidth, height: quadHeight } = getDimensions(
streamWidth,
streamHeight
streamRef?.current?.offsetWidth ?? 0,
streamRef?.current?.offsetHeight ?? 0
)
if (
!hasSetNonZeroDimensions.current &&
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
if (restart) {
kclManager.isFirstRender = false
}
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?.offsetHeight,
settings.modelingSend,
@ -101,8 +100,8 @@ export function useSetupEngineManager(
useEffect(() => {
const handleResize = deferExecution(() => {
const { width, height } = getDimensions(
streamRef?.current?.offsetWidth,
streamRef?.current?.offsetHeight
streamRef?.current?.offsetWidth ?? 0,
streamRef?.current?.offsetHeight ?? 0
)
if (
settings.modelingContext.store.streamDimensions.streamWidth !== width ||
@ -125,10 +124,37 @@ export function useSetupEngineManager(
}, 500)
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 = () => {
kclManager.isFirstRender = true
engineCommandManager.tearDown()
}
@ -136,11 +162,30 @@ export function useSetupEngineManager(
window.addEventListener('offline', onOffline)
window.addEventListener('resize', handleResize)
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('offline', onOffline)
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) {

View File

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

View File

@ -13,6 +13,7 @@ import {
UpdaterRestartModal,
createUpdaterRestartModal,
} from 'components/UpdaterRestartModal'
import { AppStreamProvider } from 'AppState'
// uncomment for xstate inspector
// import { DEV } from 'env'
@ -26,28 +27,30 @@ const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
root.render(
<HotkeysProvider>
<Router />
<Toaster
position="bottom-center"
toastOptions={{
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)',
<AppStreamProvider>
<Router />
<Toaster
position="bottom-center"
toastOptions={{
style: {
borderRadius: '3px',
},
duration:
window?.localStorage.getItem('playwright') === 'true'
? 10 // speed up e2e tests
: 1500,
},
}}
/>
<ModalContainer />
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:
window?.localStorage.getItem('playwright') === 'true'
? 10 // speed up e2e tests
: 1500,
},
}}
/>
<ModalContainer />
</AppStreamProvider>
</HotkeysProvider>
)

View File

@ -3,6 +3,8 @@ import { createContext, useContext, useEffect, useState } from 'react'
import { type IndexLoaderData } from 'lib/types'
import { useLoaderData } from 'react-router-dom'
import { codeManager, kclManager } from 'lib/singletons'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { Command } from 'lib/commandTypes'
const KclContext = createContext({
code: codeManager?.code || '',
@ -35,6 +37,7 @@ export function KclContextProvider({
const [errors, setErrors] = useState<KCLError[]>([])
const [logs, setLogs] = useState<string[]>([])
const [wasmInitFailed, setWasmInitFailed] = useState(false)
const { commandBarSend } = useCommandsContext()
useEffect(() => {
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 (
<KclContext.Provider
value={{

View File

@ -302,6 +302,30 @@ class EngineConnection extends EventTarget {
mediaStream?: MediaStream
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 = {
type: EngineConnectionStateType.Fresh,
}
@ -346,6 +370,7 @@ class EngineConnection extends EventTarget {
private engineCommandManager: EngineCommandManager
private pingPongSpan: { ping?: Date; pong?: Date }
private pingIntervalId: ReturnType<typeof setInterval>
constructor({
engineCommandManager,
@ -368,7 +393,7 @@ class EngineConnection extends EventTarget {
// Without an interval ping, our connection will timeout.
// If this.freezeFrame is true we skip this logic so only reconnect
// happens on mouse move
setInterval(() => {
this.pingIntervalId = setInterval(() => {
if (this.freezeFrame) return
switch (this.state.type as EngineConnectionStateType) {
@ -434,6 +459,44 @@ class EngineConnection extends EventTarget {
tearDown(opts?: { freeze: boolean }) {
this.freezeFrame = opts?.freeze ?? false
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 = {
type: EngineConnectionStateType.Disconnecting,
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) {
return
}
@ -499,18 +562,20 @@ class EngineConnection extends EventTarget {
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
console.warn(
`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
// Event type: generic Event type...
this.pc.addEventListener('connectionstatechange', (event: any) => {
this.onConnectionStateChange = (event: any) => {
console.log('connectionstatechange: ' + event.target?.connectionState)
switch (event.target?.connectionState) {
// From what I understand, only after have we done the ICE song and
@ -539,9 +604,13 @@ class EngineConnection extends EventTarget {
default:
break
}
})
}
this.pc.addEventListener(
'connectionstatechange',
this.onConnectionStateChange
)
this.pc.addEventListener('track', (event) => {
this.onTrack = (event) => {
const mediaStream = event.streams[0]
this.state = {
@ -625,9 +694,10 @@ class EngineConnection extends EventTarget {
// to pass it to the rest of the application.
this.mediaStream = mediaStream
})
}
this.pc.addEventListener('track', this.onTrack)
this.pc.addEventListener('datachannel', (event) => {
this.onDataChannel = (event) => {
this.unreliableDataChannel = event.channel
this.state = {
@ -638,7 +708,7 @@ class EngineConnection extends EventTarget {
},
}
this.unreliableDataChannel.addEventListener('open', (event) => {
this.onDataChannelOpen = (event) => {
this.state = {
type: EngineConnectionStateType.Connecting,
value: {
@ -654,14 +724,22 @@ class EngineConnection extends EventTarget {
this.dispatchEvent(
new CustomEvent(EngineConnectionEvents.Opened, { detail: this })
)
})
}
this.unreliableDataChannel?.addEventListener(
'open',
this.onDataChannelOpen
)
this.unreliableDataChannel.addEventListener('close', (event) => {
this.onDataChannelClose = (event) => {
this.disconnectAll()
this.finalizeIfAllConnectionsClosed()
})
}
this.unreliableDataChannel?.addEventListener(
'close',
this.onDataChannelClose
)
this.unreliableDataChannel.addEventListener('error', (event) => {
this.onDataChannelError = (event) => {
this.disconnectAll()
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)
Object.values(
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 = () => {
@ -712,7 +800,7 @@ class EngineConnection extends EventTarget {
this.websocket = new WebSocket(this.url, [])
this.websocket.binaryType = 'arraybuffer'
this.websocket.addEventListener('open', (event) => {
this.onWebSocketOpen = (event) => {
this.state = {
type: EngineConnectionStateType.Connecting,
value: {
@ -733,14 +821,16 @@ class EngineConnection extends EventTarget {
// Send an initial ping
this.send({ type: 'ping' })
this.pingPongSpan.ping = new Date()
})
}
this.websocket.addEventListener('open', this.onWebSocketOpen)
this.websocket.addEventListener('close', (event) => {
this.onWebSocketClose = (event) => {
this.disconnectAll()
this.finalizeIfAllConnectionsClosed()
})
}
this.websocket.addEventListener('close', this.onWebSocketClose)
this.websocket.addEventListener('error', (event) => {
this.onWebSocketError = (event) => {
this.disconnectAll()
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
// the server that relate to the ICE handshake, or WebRTC
// negotiation. There may be other messages (including ArrayBuffer
@ -960,15 +1051,20 @@ class EngineConnection extends EventTarget {
})
break
}
})
}
this.websocket.addEventListener('message', this.onWebSocketMessage)
}
if (reconnecting) {
createWebSocketConnection()
} else {
window.addEventListener('use-network-status-ready', () => {
this.onNetworkStatusReady = () => {
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
@ -1154,7 +1250,15 @@ export class EngineCommandManager extends EventTarget {
private makeDefaultPlanes: () => Promise<DefaultPlanes> | 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({
restart,
setMediaStream,
setIsStreamReady,
width,
@ -1170,6 +1274,7 @@ export class EngineCommandManager extends EventTarget {
showScaleGrid: false,
},
}: {
restart?: boolean
setMediaStream: (stream: MediaStream) => void
setIsStreamReady: (isStreamReady: boolean) => void
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(
EngineConnectionEvents.Opened,
() => {
// 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.onEngineConnectionOpened
)
this.onEngineConnectionClosed = () => {
setIsStreamReady(false)
}
this.engineConnection.addEventListener(
EngineConnectionEvents.Closed,
() => {
setIsStreamReady(false)
}
this.onEngineConnectionClosed
)
this.engineConnection.addEventListener(
EngineConnectionEvents.ConnectionStarted,
({ detail: engineConnection }: any) => {
engineConnection?.pc?.addEventListener(
'datachannel',
(event: RTCDataChannelEvent) => {
let unreliableDataChannel = event.channel
this.onEngineConnectionStarted = ({ detail: engineConnection }: any) => {
engineConnection?.pc?.addEventListener(
'datachannel',
(event: RTCDataChannelEvent) => {
let unreliableDataChannel = event.channel
unreliableDataChannel.addEventListener(
'message',
(event: MessageEvent) => {
const result: UnreliableResponses = JSON.parse(event.data)
Object.values(
this.unreliableSubscriptions[result.type] || {}
).forEach(
// 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
// sequence logic, but I'm not sure if we use a single global sequence or a sequence
// per unreliable subscription.
(callback) => {
let data = result?.data
if (isHighlightSetEntity_type(data)) {
if (
data.sequence !== undefined &&
data.sequence > this.inSequence
) {
this.inSequence = data.sequence
callback(result)
}
unreliableDataChannel.addEventListener(
'message',
(event: MessageEvent) => {
const result: UnreliableResponses = JSON.parse(event.data)
Object.values(
this.unreliableSubscriptions[result.type] || {}
).forEach(
// 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
// sequence logic, but I'm not sure if we use a single global sequence or a sequence
// per unreliable subscription.
(callback) => {
let data = result?.data
if (isHighlightSetEntity_type(data)) {
if (
data.sequence !== undefined &&
data.sequence > this.inSequence
) {
this.inSequence = data.sequence
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(
EngineConnectionEvents.NewTrack,
(({ detail: { mediaStream } }: CustomEvent<NewTrackArgs>) => {
mediaStream.getVideoTracks()[0].addEventListener('mute', () => {
console.error(
'video track mute: check webrtc internals -> inbound rtp'
)
})
this.onEngineConnectionNewTrack = ({
detail: { mediaStream },
}: CustomEvent<NewTrackArgs>) => {
mediaStream.getVideoTracks()[0].addEventListener('mute', () => {
console.error(
'video track mute: check webrtc internals -> inbound rtp'
)
})
setMediaStream(mediaStream)
}) as EventListener
)
this.engineConnection?.connect()
setMediaStream(mediaStream)
}
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() {
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 = undefined
// Our window.tearDown assignment causes this case to happen which is
// only really for tests.
// @ts-ignore

View File

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

View File

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

View File

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

View File

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

View File

@ -33,13 +33,13 @@ export interface KclExpressionWithVariable extends KclExpression {
export type KclCommandValue = KclExpression | KclExpressionWithVariable
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>
}>
export type CommandSet<
export type StateMachineCommandSet<
T extends AllMachines,
Schema extends CommandSetSchema<T>
Schema extends StateMachineCommandSetSchema<T>
> = Partial<{
[EventType in EventFrom<T>['type']]: Command<
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,
Schema extends CommandSetSchema<T>
Schema extends StateMachineCommandSetSchema<T>
> = Partial<{
[EventType in EventFrom<T>['type']]: CommandConfig<
T,
EventFrom<T>['type'],
Schema[EventType]
>
[EventType in EventFrom<T>['type']]:
| CommandConfig<T, EventFrom<T>['type'], Schema[EventType]>
| CommandConfig<T, EventFrom<T>['type'], Schema[EventType]>[]
}>
export type Command<
T extends AnyStateMachine = AnyStateMachine,
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
ownerMachine: T['id']
groupId: T['id']
needsReview: boolean
onSubmit: (data?: CommandSchema) => void
onCancel?: () => void
@ -81,10 +85,10 @@ export type Command<
export type CommandConfig<
T extends AnyStateMachine = AnyStateMachine,
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<
Command<T, CommandName, CommandSchema>,
'name' | 'ownerMachine' | 'onSubmit' | 'onCancel' | 'args' | 'needsReview'
'name' | 'groupId' | 'onSubmit' | 'onCancel' | 'args' | 'needsReview'
> & {
needsReview?: true
args?: {

View File

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

View File

@ -57,7 +57,7 @@ export type CommandBarMachineEvent =
}
| {
type: 'Find and select command'
data: { name: string; ownerMachine: string }
data: { name: string; groupId: string }
}
| {
type: 'Change current argument'
@ -66,7 +66,7 @@ export type CommandBarMachineEvent =
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,
tsTypes: {} as import('./commandBarMachine.typegen').Typegen0,
context: {
@ -120,9 +120,7 @@ export const commandBarMachine = createMachine(
context.commands.filter(
(c) =>
!event.data.commands.some(
(c2) =>
c2.name === c.name &&
c2.ownerMachine === c.ownerMachine
(c2) => c2.name === c.name && c2.groupId === c.groupId
)
),
}),
@ -149,6 +147,10 @@ export const commandBarMachine = createMachine(
cond: 'Command has no arguments',
actions: ['Execute command'],
},
{
target: 'Checking Arguments',
cond: 'All arguments are skippable',
},
{
target: 'Gathering arguments',
actions: ['Set current argument to first non-skippable'],
@ -393,9 +395,7 @@ export const commandBarMachine = createMachine(
selectedCommand: (c, e) => {
if (e.type !== 'Find and select command') return c.selectedCommand
const found = c.commands.find(
(cmd) =>
cmd.name === e.data.name &&
cmd.ownerMachine === e.data.ownerMachine
(cmd) => cmd.name === e.data.name && cmd.groupId === e.data.groupId
)
return !!found ? found : c.selectedCommand
@ -514,7 +514,9 @@ export const commandBarMachine = createMachine(
)
function sortCommands(a: Command, b: Command) {
if (b.ownerMachine === 'auth') return -1
if (a.ownerMachine === 'auth') return 1
if (b.groupId === 'auth' && !(a.groupId === 'auth')) return -2
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)
}

File diff suppressed because one or more lines are too long

View File

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

View File

@ -1842,7 +1842,7 @@ dependencies = [
"bincode",
"either",
"fnv",
"itertools 0.10.5",
"itertools 0.12.1",
"lazy_static",
"nom",
"quick-xml",
@ -2928,18 +2928,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.61"
version = "1.0.62"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.61"
version = "1.0.62"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c"
dependencies = [
"proc-macro2",
"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_json = "1.0.120"
sha2 = "0.10.8"
thiserror = "1.0.61"
thiserror = "1.0.62"
toml = "0.8.14"
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"] }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 333 KiB

After

Width:  |  Height:  |  Size: 333 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 333 KiB

After

Width:  |  Height:  |  Size: 333 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: 95 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 117 KiB