Merge branch 'main' into pierremtb/issue2610

This commit is contained in:
Pierre Jacquier
2024-07-16 05:07:21 -04:00
committed by GitHub
96 changed files with 2377 additions and 749 deletions

View File

@ -112,13 +112,13 @@ jobs:
'.productName=$name' src-tauri/tauri.release.conf.json --indent 2)" > src-tauri/tauri.release.conf.json '.productName=$name' src-tauri/tauri.release.conf.json --indent 2)" > src-tauri/tauri.release.conf.json
- name: Set updater test version - name: Set updater test version
if: env.CUT_RELEASE_PR if: ${{ env.CUT_RELEASE_PR == 'true' }}
run: | run: |
echo "$(jq --arg url 'https://dl.zoo.dev/releases/modeling-app/test/last_update.json' \ 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 '.plugins.updater.endpoints[]=$url' src-tauri/tauri.release.conf.json --indent 2)" > src-tauri/tauri.release.conf.json
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
if: ${{ github.event_name == 'schedule' || env.CUT_RELEASE_PR }} if: ${{ github.event_name == 'schedule' || env.CUT_RELEASE_PR == 'true' }}
with: with:
path: | path: |
package.json package.json
@ -387,24 +387,24 @@ jobs:
TS_NODE_COMPILER_OPTIONS: '{"module": "commonjs"}' TS_NODE_COMPILER_OPTIONS: '{"module": "commonjs"}'
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v3
if: env.CUT_RELEASE_PR if: ${{ env.CUT_RELEASE_PR == 'true' }}
- name: Copy updated .json file for updater test - name: Copy updated .json file for updater test
if: env.CUT_RELEASE_PR if: ${{ env.CUT_RELEASE_PR == 'true' }}
run: | run: |
ls -l artifact ls -l artifact
cp artifact/src-tauri/tauri.release.conf.json src-tauri/tauri.release.conf.json cp artifact/src-tauri/tauri.release.conf.json src-tauri/tauri.release.conf.json
cat src-tauri/tauri.release.conf.json cat src-tauri/tauri.release.conf.json
- name: Build the app (release, updater test) - name: Build the app (release, updater test)
if: ${{ env.CUT_RELEASE_PR && matrix.os != 'ubuntu-latest' }} if: ${{ env.CUT_RELEASE_PR == 'true' && matrix.os != 'ubuntu-latest' }}
env: env:
TAURI_CONF_ARGS: "-c ${{ matrix.os == 'windows-latest' && 'src-tauri\\tauri.release.conf.json' || 'src-tauri/tauri.release.conf.json' }}" 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' }}" 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 }}" run: "yarn tauri build ${{ env.TAURI_CONF_ARGS }} ${{ env.TAURI_BUNDLE_ARGS }} ${{ env.TAURI_ARGS_MACOS }}"
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
if: ${{ env.CUT_RELEASE_PR && matrix.os != 'ubuntu-latest' }} if: ${{ env.CUT_RELEASE_PR == 'true' && matrix.os != 'ubuntu-latest' }}
with: with:
path: "${{ matrix.os == 'macos-14' && 'src-tauri/target/universal-apple-darwin/release/bundle/dmg/*.dmg' || 'src-tauri/target/release/bundle/msi/*.msi' }}" 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 name: updater-test

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

@ -3099,6 +3099,49 @@ const sketch002 = startSketchOn(extrude001, $seg01)
).not.toBeDisabled() ).not.toBeDisabled()
}) })
test('Fillet button states test', async ({ page }) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const sketch001 = startSketchOn('XZ')
|> startProfileAt([-5, -5], %)
|> line([0, 10], %)
|> line([10, 0], %)
|> line([0, -10], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`
)
})
await page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
const selectSegment = () => page.getByText(`line([10, 0], %)`).click()
const selectClose = () => page.getByText(`close(%)`).click()
const clickEmpty = () => page.mouse.click(950, 100)
// expect fillet button without any bodies in the scene
await selectSegment()
await expect(page.getByRole('button', { name: 'Fillet' })).toBeDisabled()
await clickEmpty()
await expect(page.getByRole('button', { name: 'Fillet' })).toBeDisabled()
// test fillet button with the body in the scene
const codeToAdd = `${await u.codeLocator.allInnerTexts()}
const extrude001 = extrude(10, sketch001)`
await u.codeLocator.fill(codeToAdd)
await selectSegment()
await expect(page.getByRole('button', { name: 'Fillet' })).toBeEnabled()
await selectClose()
await expect(page.getByRole('button', { name: 'Fillet' })).toBeDisabled()
await clickEmpty()
await expect(page.getByRole('button', { name: 'Fillet' })).toBeEnabled()
})
const removeAfterFirstParenthesis = (inputString: string) => { const removeAfterFirstParenthesis = (inputString: string) => {
const index = inputString.indexOf('(') const index = inputString.indexOf('(')
if (index !== -1) { if (index !== -1) {
@ -3500,11 +3543,62 @@ test.describe('Command bar tests', () => {
`const extrude001 = extrude(${KCL_DEFAULT_LENGTH}, sketch001)` `const extrude001 = extrude(${KCL_DEFAULT_LENGTH}, sketch001)`
) )
}) })
test('Command bar works and can change a setting', async ({ page }) => {
test('Fillet from command bar', async ({ page }) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const sketch001 = startSketchOn('XY')
|> startProfileAt([-5, -5], %)
|> line([0, 10], %)
|> line([10, 0], %)
|> line([0, -10], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(-10, sketch001)`
)
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
const selectSegment = () => page.getByText(`line([0, -10], %)`).click()
await selectSegment()
await page.waitForTimeout(100)
await page.getByRole('button', { name: 'Fillet' }).click()
await page.waitForTimeout(100)
await page.keyboard.press('Enter')
await page.waitForTimeout(100)
await page.keyboard.press('Enter')
await page.waitForTimeout(100)
await expect(page.locator('.cm-activeLine')).toContainText(
`fillet({ radius: ${KCL_DEFAULT_LENGTH}, tags: [seg01] }, %)`
)
})
test('Command bar can change a setting, and switch back and forth between arguments', async ({
page,
}) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
const commandBarButton = page.getByRole('button', { name: 'Commands' })
const cmdSearchBar = page.getByPlaceholder('Search commands')
const themeOption = page.getByRole('option', {
name: 'theme',
exact: false,
})
const commandLevelArgButton = page.getByRole('button', { name: 'level' })
const commandThemeArgButton = page.getByRole('button', { name: 'value' })
// This selector changes after we set the setting
let commandOptionInput = page.getByPlaceholder('Select an option')
await expect( await expect(
page.getByRole('button', { name: 'Start Sketch' }) page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled() ).not.toBeDisabled()
@ -3515,23 +3609,17 @@ test.describe('Command bar tests', () => {
.or(page.getByRole('button', { name: '⌘K' })) .or(page.getByRole('button', { name: '⌘K' }))
.click() .click()
let cmdSearchBar = page.getByPlaceholder('Search commands')
await expect(cmdSearchBar).toBeVisible() await expect(cmdSearchBar).toBeVisible()
await page.keyboard.press('Escape') await page.keyboard.press('Escape')
cmdSearchBar = page.getByPlaceholder('Search commands')
await expect(cmdSearchBar).not.toBeVisible() await expect(cmdSearchBar).not.toBeVisible()
// Now try the same, but with the keyboard shortcut, check focus // Now try the same, but with the keyboard shortcut, check focus
await page.keyboard.press('Meta+K') await page.keyboard.press('Meta+K')
cmdSearchBar = page.getByPlaceholder('Search commands')
await expect(cmdSearchBar).toBeVisible() await expect(cmdSearchBar).toBeVisible()
await expect(cmdSearchBar).toBeFocused() await expect(cmdSearchBar).toBeFocused()
// Try typing in the command bar // Try typing in the command bar
await page.keyboard.type('theme') await cmdSearchBar.fill('theme')
const themeOption = page.getByRole('option', {
name: 'Settings · app · theme',
})
await expect(themeOption).toBeVisible() await expect(themeOption).toBeVisible()
await themeOption.click() await themeOption.click()
const themeInput = page.getByPlaceholder('Select an option') const themeInput = page.getByPlaceholder('Select an option')
@ -3553,6 +3641,24 @@ test.describe('Command bar tests', () => {
).toBeVisible() ).toBeVisible()
// Check that the theme changed // Check that the theme changed
await expect(page.locator('body')).not.toHaveClass(`body-bg dark`) await expect(page.locator('body')).not.toHaveClass(`body-bg dark`)
commandOptionInput = page.getByPlaceholder('system')
// Test case for https://github.com/KittyCAD/modeling-app/issues/2882
await commandBarButton.click()
await cmdSearchBar.focus()
await cmdSearchBar.fill('theme')
await themeOption.click()
await expect(commandThemeArgButton).toBeDisabled()
await commandOptionInput.focus()
await commandOptionInput.fill('lig')
await commandLevelArgButton.click()
await expect(commandLevelArgButton).toBeDisabled()
// Test case for https://github.com/KittyCAD/modeling-app/issues/2881
await commandThemeArgButton.click()
await expect(commandThemeArgButton).toBeDisabled()
await expect(commandLevelArgButton).toHaveText('level: project')
}) })
test('Command bar keybinding works from code editor and can change a setting', async ({ test('Command bar keybinding works from code editor and can change a setting', async ({
@ -3577,7 +3683,7 @@ test.describe('Command bar tests', () => {
await expect(cmdSearchBar).toBeFocused() await expect(cmdSearchBar).toBeFocused()
// Try typing in the command bar // Try typing in the command bar
await page.keyboard.type('theme') await cmdSearchBar.fill('theme')
const themeOption = page.getByRole('option', { const themeOption = page.getByRole('option', {
name: 'Settings · app · theme', name: 'Settings · app · theme',
}) })
@ -3648,7 +3754,9 @@ test.describe('Command bar tests', () => {
await page.mouse.click(700, 200) await page.mouse.click(700, 200)
// Assert that we're on the distance step // Assert that we're on the distance step
await expect(page.getByRole('button', { name: 'distance' })).toBeDisabled() await expect(
page.getByRole('button', { name: 'distance', exact: false })
).toBeDisabled()
// Assert that the an alternative variable name is chosen, // Assert that the an alternative variable name is chosen,
// since the default variable name is already in use (distance) // since the default variable name is already in use (distance)
@ -3663,11 +3771,12 @@ test.describe('Command bar tests', () => {
// Review step and argument hotkeys // Review step and argument hotkeys
await expect(submitButton).toBeEnabled() await expect(submitButton).toBeEnabled()
await page.keyboard.press('Backspace') await expect(submitButton).toBeFocused()
await submitButton.press('Backspace')
// Assert we're back on the distance step // Assert we're back on the distance step
await expect( await expect(
page.getByRole('button', { name: 'Distance 5', exact: false }) page.getByRole('button', { name: 'distance', exact: false })
).toBeDisabled() ).toBeDisabled()
await continueButton.click() await continueButton.click()
@ -3691,6 +3800,47 @@ const extrude001 = extrude(distance001, sketch001)`.replace(
) // remove newlines ) // remove newlines
) )
}) })
test('Can switch between sketch tools via command bar', async ({ page }) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
const sketchButton = page.getByRole('button', { name: 'Start Sketch' })
const cmdBarButton = page.getByRole('button', { name: 'Commands' })
const rectangleToolCommand = page.getByRole('option', {
name: 'Rectangle',
})
const rectangleToolButton = page.getByRole('button', { name: 'Rectangle' })
const lineToolCommand = page.getByRole('option', { name: 'Line' })
const lineToolButton = page.getByRole('button', { name: 'Line' })
const arcToolCommand = page.getByRole('option', { name: 'Tangential Arc' })
const arcToolButton = page.getByRole('button', { name: 'Tangential Arc' })
// Start a sketch
await sketchButton.click()
await page.mouse.click(700, 200)
// Switch between sketch tools via the command bar
await expect(lineToolButton).toHaveAttribute('aria-pressed', 'true')
await cmdBarButton.click()
await rectangleToolCommand.click()
await expect(rectangleToolButton).toHaveAttribute('aria-pressed', 'true')
await cmdBarButton.click()
await lineToolCommand.click()
await expect(lineToolButton).toHaveAttribute('aria-pressed', 'true')
// Click in the scene a couple times to draw a line
// so tangential arc is valid
await page.mouse.click(700, 200)
await page.mouse.move(700, 300, { steps: 5 })
await page.mouse.click(700, 300)
// switch to tangential arc via command bar
await cmdBarButton.click()
await arcToolCommand.click()
await expect(arcToolButton).toHaveAttribute('aria-pressed', 'true')
})
}) })
test.describe('Regression tests', () => { test.describe('Regression tests', () => {
@ -4642,10 +4792,10 @@ test.describe('Sketch tests', () => {
// click extrude // click extrude
await page.getByRole('button', { name: 'Extrude' }).click() await page.getByRole('button', { name: 'Extrude' }).click()
// sketch selection should already have been made. "Selection 1 face" only show up when the selection has been made already // sketch selection should already have been made. "Selection: 1 face" only show up when the selection has been made already
// otherwise the cmdbar would be waiting for a selection. // otherwise the cmdbar would be waiting for a selection.
await expect( await expect(
page.getByRole('button', { name: 'Selection 1 face' }) page.getByRole('button', { name: 'selection : 1 face', exact: false })
).toBeVisible() ).toBeVisible()
}) })
test("Existing sketch with bad code delete user's code", async ({ page }) => { test("Existing sketch with bad code delete user's code", async ({ page }) => {
@ -7726,6 +7876,31 @@ test('Basic default modeling and sketch hotkeys work', async ({ page }) => {
await expect(page.locator('.cm-content')).toContainText('extrude(') await expect(page.locator('.cm-content')).toContainText('extrude(')
}) })
test('Delete key does not navigate back', async ({ page }) => {
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await page.waitForURL('**/file/**', { waitUntil: 'domcontentloaded' })
const settingsButton = page.getByRole('link', {
name: 'Settings',
exact: false,
})
const settingsCloseButton = page.getByTestId('settings-close-button')
await settingsButton.click()
await expect(page.url()).toContain('/settings')
// Make sure that delete doesn't go back from settings
await page.keyboard.press('Delete')
await expect(page.url()).toContain('/settings')
// Now close the settings and try delete again,
// make sure it doesn't go back to settings
await settingsCloseButton.click()
await page.keyboard.press('Delete')
await expect(page.url()).not.toContain('/settings')
})
test('Sketch on face', async ({ page }) => { test('Sketch on face', async ({ page }) => {
test.setTimeout(90_000) test.setTimeout(90_000)
const u = await getUtils(page) const u = await getUtils(page)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

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

View File

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

130
src-tauri/Cargo.lock generated
View File

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

View File

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

View File

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

View File

@ -16,6 +16,7 @@ import {
canRectangleTool, canRectangleTool,
isEditingExistingSketch, isEditingExistingSketch,
} from 'machines/modelingMachine' } from 'machines/modelingMachine'
import { DEV } from 'env'
export function Toolbar({ export function Toolbar({
className = '', className = '',
@ -60,7 +61,7 @@ export function Toolbar({
? send('CancelSketch') ? send('CancelSketch')
: send({ : send({
type: 'change tool', type: 'change tool',
data: 'line', data: { tool: 'line' },
}), }),
{ enabled: !disableLineButton, scopes: ['sketch'] } { enabled: !disableLineButton, scopes: ['sketch'] }
) )
@ -75,7 +76,7 @@ export function Toolbar({
? send('CancelSketch') ? send('CancelSketch')
: send({ : send({
type: 'change tool', type: 'change tool',
data: 'tangentialArc', data: { tool: 'tangentialArc' },
}), }),
{ enabled: !disableTangentialArc, scopes: ['sketch'] } { enabled: !disableTangentialArc, scopes: ['sketch'] }
) )
@ -89,7 +90,7 @@ export function Toolbar({
? send('CancelSketch') ? send('CancelSketch')
: send({ : send({
type: 'change tool', type: 'change tool',
data: 'rectangle', data: { tool: 'rectangle' },
}), }),
{ enabled: !disableRectangle, scopes: ['sketch'] } { enabled: !disableRectangle, scopes: ['sketch'] }
) )
@ -114,10 +115,20 @@ export function Toolbar({
() => () =>
commandBarSend({ commandBarSend({
type: 'Find and select command', type: 'Find and select command',
data: { name: 'Extrude', ownerMachine: 'modeling' }, data: { name: 'Extrude', groupId: 'modeling' },
}), }),
{ enabled: !disableAllButtons, scopes: ['modeling'] } { enabled: !disableAllButtons, scopes: ['modeling'] }
) )
const disableFillet = !state.can('Fillet') || disableAllButtons
useHotkeys(
'f',
() =>
commandBarSend({
type: 'Find and select command',
data: { name: 'Fillet', groupId: 'modeling' },
}),
{ enabled: !disableFillet, scopes: ['modeling'] }
)
function handleToolbarButtonsWheelEvent(ev: WheelEvent<HTMLSpanElement>) { function handleToolbarButtonsWheelEvent(ev: WheelEvent<HTMLSpanElement>) {
const span = toolbarButtonsRef.current const span = toolbarButtonsRef.current
@ -263,7 +274,7 @@ export function Toolbar({
? send('CancelSketch') ? send('CancelSketch')
: send({ : send({
type: 'change tool', type: 'change tool',
data: 'line', data: { tool: 'line' },
}) })
} }
aria-pressed={state?.matches('Sketch.Line tool')} aria-pressed={state?.matches('Sketch.Line tool')}
@ -293,7 +304,7 @@ export function Toolbar({
? send('CancelSketch') ? send('CancelSketch')
: send({ : send({
type: 'change tool', type: 'change tool',
data: 'tangentialArc', data: { tool: 'tangentialArc' },
}) })
} }
aria-pressed={state.matches('Sketch.Tangential arc to')} aria-pressed={state.matches('Sketch.Tangential arc to')}
@ -323,7 +334,7 @@ export function Toolbar({
? send('CancelSketch') ? send('CancelSketch')
: send({ : send({
type: 'change tool', type: 'change tool',
data: 'rectangle', data: { tool: 'rectangle' },
}) })
} }
aria-pressed={state.matches('Sketch.Rectangle tool')} aria-pressed={state.matches('Sketch.Rectangle tool')}
@ -378,7 +389,7 @@ export function Toolbar({
onClick={() => onClick={() =>
commandBarSend({ commandBarSend({
type: 'Find and select command', type: 'Find and select command',
data: { name: 'Extrude', ownerMachine: 'modeling' }, data: { name: 'Extrude', groupId: 'modeling' },
}) })
} }
disabled={!state.can('Extrude') || disableAllButtons} disabled={!state.can('Extrude') || disableAllButtons}
@ -404,6 +415,36 @@ export function Toolbar({
</ActionButton> </ActionButton>
</li> </li>
)} )}
{state.matches('idle') && (DEV || (window as any)._enableFillet) && (
<li className="contents">
<ActionButton
className={buttonClassName}
Element="button"
onClick={() =>
commandBarSend({
type: 'Find and select command',
data: { name: 'Fillet', groupId: 'modeling' },
})
}
disabled={disableFillet}
title={disableFillet ? 'fillet' : "edge can't be filleted"}
iconStart={{
icon: 'fillet', // todo: add fillet icon
iconClassName,
bgClassName,
}}
>
Fillet
<Tooltip
delay={1250}
position="bottom"
className="!px-2 !text-xs"
>
Shortcut: F
</Tooltip>
</ActionButton>
</li>
)}
</ul> </ul>
</menu> </menu>
) )

View File

@ -41,6 +41,7 @@ function CommandArgOptionInput({
) )
const inputRef = useRef<HTMLInputElement>(null) const inputRef = useRef<HTMLInputElement>(null)
const formRef = useRef<HTMLFormElement>(null) const formRef = useRef<HTMLFormElement>(null)
const [shouldSubmitOnChange, setShouldSubmitOnChange] = useState(false)
const [selectedOption, setSelectedOption] = useState< const [selectedOption, setSelectedOption] = useState<
CommandArgumentOption<unknown> CommandArgumentOption<unknown>
>(currentOption || resolvedOptions[0]) >(currentOption || resolvedOptions[0])
@ -82,8 +83,10 @@ function CommandArgOptionInput({
// We deal with the whole option object internally // We deal with the whole option object internally
setSelectedOption(option) setSelectedOption(option)
// But we only submit the value // But we only submit the value itself
onSubmit(option.value) if (shouldSubmitOnChange) {
onSubmit(option.value)
}
} }
function handleSubmit(e: React.FormEvent<HTMLFormElement>) { function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
@ -94,7 +97,18 @@ function CommandArgOptionInput({
} }
return ( return (
<form id="arg-form" onSubmit={handleSubmit} ref={formRef}> <form
id="arg-form"
onSubmit={handleSubmit}
ref={formRef}
onKeyDownCapture={(e) => {
if (e.key === 'Enter') {
setShouldSubmitOnChange(true)
} else {
setShouldSubmitOnChange(false)
}
}}
>
<Combobox <Combobox
value={selectedOption} value={selectedOption}
onChange={handleSelectOption} onChange={handleSelectOption}
@ -118,6 +132,12 @@ function CommandArgOptionInput({
if (event.key === 'Backspace' && !event.currentTarget.value) { if (event.key === 'Backspace' && !event.currentTarget.value) {
stepBack() stepBack()
} }
if (event.key === 'Enter') {
setShouldSubmitOnChange(true)
} else {
setShouldSubmitOnChange(false)
}
}} }}
value={query} value={query}
placeholder={ placeholder={
@ -136,6 +156,9 @@ function CommandArgOptionInput({
<Combobox.Options <Combobox.Options
static static
className="overflow-y-auto max-h-96 cursor-pointer" className="overflow-y-auto max-h-96 cursor-pointer"
onMouseDown={() => {
setShouldSubmitOnChange(true)
}}
> >
{filteredOptions?.map((option) => ( {filteredOptions?.map((option) => (
<Combobox.Option <Combobox.Option

View File

@ -114,6 +114,7 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
> >
{argName} {argName}
</span> </span>
<span className="sr-only">:&nbsp;</span>
{argValue ? ( {argValue ? (
arg.inputType === 'selection' ? ( arg.inputType === 'selection' ? (
getSelectionTypeDisplayText(argValue as Selections) getSelectionTypeDisplayText(argValue as Selections)

View File

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

View File

@ -11,6 +11,25 @@ import { modelingMachine } from 'machines/modelingMachine'
import { useCallback, useEffect, useRef, useState } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
import { StateFrom } from 'xstate' import { StateFrom } from 'xstate'
const semanticEntityNames = {
face: ['extrude-wall', 'start-cap', 'end-cap'],
edge: ['edge', 'line', 'arc'],
point: ['point', 'line-end', 'line-mid'],
}
function getSemanticSelectionType(selectionType: string[]) {
const semanticSelectionType = new Set()
selectionType.forEach((type) => {
Object.entries(semanticEntityNames).forEach(([entity, entityTypes]) => {
if (entityTypes.includes(type)) {
semanticSelectionType.add(entity)
}
})
})
return Array.from(semanticSelectionType)
}
const selectionSelector = (snapshot: StateFrom<typeof modelingMachine>) => const selectionSelector = (snapshot: StateFrom<typeof modelingMachine>) =>
snapshot.context.selectionRanges snapshot.context.selectionRanges
@ -85,7 +104,9 @@ function CommandBarSelectionInput({
> >
{canSubmitSelection {canSubmitSelection
? getSelectionTypeDisplayText(selection) + ' selected' ? getSelectionTypeDisplayText(selection) + ' selected'
: `Please select ${arg.multiple ? 'one or more faces' : 'one face'}`} : `Please select ${
arg.multiple ? 'one or more ' : 'one '
}${getSemanticSelectionType(arg.selectionTypes).join(' or ')}`}
<input <input
id="selection" id="selection"
name="selection" name="selection"

View File

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

View File

@ -131,6 +131,16 @@ const CustomIconMap = {
/> />
</svg> </svg>
), ),
code: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.7071 5L7.77734 14.7794L8.73527 15.0663L11.665 5.28698L10.7071 5ZM2.35356 9.64644L5.85362 6.14644L6.56072 6.85355L3.41423 10L6.56072 13.1464L5.85362 13.8536L2.35356 10.3536L2 10L2.35356 9.64644ZM17.0607 9.64644L13.5607 6.14644L12.8536 6.85355L16 10L12.8536 13.1464L13.5607 13.8535L17.0607 10.3536L17.4142 10L17.0607 9.64644Z"
fill="currentColor"
/>
</svg>
),
dimension: ( dimension: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path <path
@ -177,6 +187,22 @@ const CustomIconMap = {
/> />
</svg> </svg>
), ),
fillet: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8 5H5V15H15V12C15 8.13401 11.866 5 8 5ZM5 4H4V5V15V16H5H15H16V15V12C16 7.58172 12.4183 4 8 4H5Z"
fill="currentColor"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M4.5 3.5H5.5H8.5C12.9183 3.5 16.5 7.08172 16.5 11.5V14.5V15.5H16V12C16 7.58172 12.4182 4 7.99996 4H4.5V3.5Z"
fill="currentColor"
/>
</svg>
),
file: ( file: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path <path

View File

@ -33,6 +33,7 @@ import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
import { import {
Selections, Selections,
canExtrudeSelection, canExtrudeSelection,
canFilletSelection,
handleSelectionBatch, handleSelectionBatch,
isSelectionLastLine, isSelectionLastLine,
isRangeInbetweenCharacters, isRangeInbetweenCharacters,
@ -42,7 +43,7 @@ import {
import { applyConstraintIntersect } from './Toolbar/Intersect' import { applyConstraintIntersect } from './Toolbar/Intersect'
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance' import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
import useStateMachineCommands from 'hooks/useStateMachineCommands' import useStateMachineCommands from 'hooks/useStateMachineCommands'
import { modelingMachineConfig } from 'lib/commandBarConfigs/modelingCommandConfig' import { modelingMachineCommandConfig } from 'lib/commandBarConfigs/modelingCommandConfig'
import { import {
STRAIGHT_SEGMENT, STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT, TANGENTIAL_ARC_TO_SEGMENT,
@ -72,6 +73,7 @@ import { uuidv4 } from 'lib/utils'
import { err, trap } from 'lib/trap' import { err, trap } from 'lib/trap'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { modelingMachineEvent } from 'editor/manager' import { modelingMachineEvent } from 'editor/manager'
import { hasValidFilletSelection } from 'lang/modifyAst/addFillet'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -444,6 +446,12 @@ export const ModelingMachineProvider = ({
if (selectionRanges.codeBasedSelections.length <= 0) return false if (selectionRanges.codeBasedSelections.length <= 0) return false
return true return true
}, },
'has valid fillet selection': ({ selectionRanges }) =>
hasValidFilletSelection({
selectionRanges,
ast: kclManager.ast,
code: codeManager.code,
}),
'Selection is on face': ({ selectionRanges }, { data }) => { 'Selection is on face': ({ selectionRanges }, { data }) => {
if (data?.forceNewSketch) return false if (data?.forceNewSketch) return false
if (!isSingleCursorInPipe(selectionRanges, kclManager.ast)) if (!isSingleCursorInPipe(selectionRanges, kclManager.ast))
@ -494,7 +502,6 @@ export const ModelingMachineProvider = ({
kclManager.ast, kclManager.ast,
data.sketchPathToNode, data.sketchPathToNode,
data.extrudePathToNode, data.extrudePathToNode,
kclManager.programMemory,
data.cap data.cap
) )
if (trap(sketched)) return Promise.reject(sketched) if (trap(sketched)) return Promise.reject(sketched)
@ -920,7 +927,7 @@ export const ModelingMachineProvider = ({
state: modelingState, state: modelingState,
send: modelingSend, send: modelingSend,
actor: modelingActor, actor: modelingActor,
commandBarConfig: modelingMachineConfig, commandBarConfig: modelingMachineCommandConfig,
allCommandsRequireNetwork: true, allCommandsRequireNetwork: true,
// TODO for when sketch tools are in the toolbar: This was added when we used one "Cancel" event, // TODO for when sketch tools are in the toolbar: This was added when we used one "Cancel" event,
// but we need to support "SketchCancel" and basically // but we need to support "SketchCancel" and basically

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -304,7 +304,6 @@ describe('testing sketchOnExtrudedFace', () => {
const ast = parse(code) const ast = parse(code)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = await enginelessExecutor(ast)
const segmentSnippet = `line([9.7, 9.19], %)` const segmentSnippet = `line([9.7, 9.19], %)`
const segmentRange: [number, number] = [ const segmentRange: [number, number] = [
code.indexOf(segmentSnippet), code.indexOf(segmentSnippet),
@ -321,8 +320,7 @@ describe('testing sketchOnExtrudedFace', () => {
const extruded = sketchOnExtrudedFace( const extruded = sketchOnExtrudedFace(
ast, ast,
segmentPathToNode, segmentPathToNode,
extrudePathToNode, extrudePathToNode
programMemory
) )
if (err(extruded)) throw extruded if (err(extruded)) throw extruded
const { modifiedAst } = extruded const { modifiedAst } = extruded
@ -345,7 +343,6 @@ const sketch001 = startSketchOn(part001, seg01)`)
|> extrude(5 + 7, %)` |> extrude(5 + 7, %)`
const ast = parse(code) const ast = parse(code)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = await enginelessExecutor(ast)
const segmentSnippet = `close(%)` const segmentSnippet = `close(%)`
const segmentRange: [number, number] = [ const segmentRange: [number, number] = [
code.indexOf(segmentSnippet), code.indexOf(segmentSnippet),
@ -362,8 +359,7 @@ const sketch001 = startSketchOn(part001, seg01)`)
const extruded = sketchOnExtrudedFace( const extruded = sketchOnExtrudedFace(
ast, ast,
segmentPathToNode, segmentPathToNode,
extrudePathToNode, extrudePathToNode
programMemory
) )
if (err(extruded)) throw extruded if (err(extruded)) throw extruded
const { modifiedAst } = extruded const { modifiedAst } = extruded
@ -386,7 +382,6 @@ const sketch001 = startSketchOn(part001, seg01)`)
|> extrude(5 + 7, %)` |> extrude(5 + 7, %)`
const ast = parse(code) const ast = parse(code)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = await enginelessExecutor(ast)
const sketchSnippet = `startProfileAt([3.58, 2.06], %)` const sketchSnippet = `startProfileAt([3.58, 2.06], %)`
const sketchRange: [number, number] = [ const sketchRange: [number, number] = [
code.indexOf(sketchSnippet), code.indexOf(sketchSnippet),
@ -404,7 +399,6 @@ const sketch001 = startSketchOn(part001, seg01)`)
ast, ast,
sketchPathToNode, sketchPathToNode,
extrudePathToNode, extrudePathToNode,
programMemory,
'end' 'end'
) )
if (err(extruded)) throw extruded if (err(extruded)) throw extruded
@ -436,7 +430,6 @@ const sketch001 = startSketchOn(part001, 'END')`)
const part001 = extrude(5 + 7, sketch001)` const part001 = extrude(5 + 7, sketch001)`
const ast = parse(code) const ast = parse(code)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = await enginelessExecutor(ast)
const segmentSnippet = `line([4.99, -0.46], %)` const segmentSnippet = `line([4.99, -0.46], %)`
const segmentRange: [number, number] = [ const segmentRange: [number, number] = [
code.indexOf(segmentSnippet), code.indexOf(segmentSnippet),
@ -453,8 +446,7 @@ const sketch001 = startSketchOn(part001, 'END')`)
const updatedAst = sketchOnExtrudedFace( const updatedAst = sketchOnExtrudedFace(
ast, ast,
segmentPathToNode, segmentPathToNode,
extrudePathToNode, extrudePathToNode
programMemory
) )
if (err(updatedAst)) throw updatedAst if (err(updatedAst)) throw updatedAst
const newCode = recast(updatedAst.modifiedAst) const newCode = recast(updatedAst.modifiedAst)

View File

@ -349,7 +349,6 @@ export function sketchOnExtrudedFace(
node: Program, node: Program,
sketchPathToNode: PathToNode, sketchPathToNode: PathToNode,
extrudePathToNode: PathToNode, extrudePathToNode: PathToNode,
programMemory: ProgramMemory,
cap: 'none' | 'start' | 'end' = 'none' cap: 'none' | 'start' | 'end' = 'none'
): { modifiedAst: Program; pathToNode: PathToNode } | Error { ): { modifiedAst: Program; pathToNode: PathToNode } | Error {
let _node = { ...node } let _node = { ...node }
@ -388,7 +387,6 @@ export function sketchOnExtrudedFace(
if (cap === 'none') { if (cap === 'none') {
const __tag = addTagForSketchOnFace( const __tag = addTagForSketchOnFace(
{ {
previousProgramMemory: programMemory,
pathToNode: sketchPathToNode, pathToNode: sketchPathToNode,
node: _node, node: _node,
}, },

View File

@ -0,0 +1,315 @@
import {
parse,
recast,
initPromise,
PathToNode,
Value,
Program,
CallExpression,
} from '../wasm'
import { addFillet, isTagUsedInFillet } from './addFillet'
import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst'
import { createLiteral } from 'lang/modifyAst'
import { err } from 'lib/trap'
beforeAll(async () => {
await initPromise // Initialize the WASM environment before running tests
})
const runFilletTest = async (
code: string,
segmentSnippet: string,
extrudeSnippet: string,
radius = createLiteral(5) as Value,
expectedCode: string
) => {
const astOrError = parse(code)
if (astOrError instanceof Error) {
return new Error('AST not found')
}
const ast = astOrError as Program
const segmentRange: [number, number] = [
code.indexOf(segmentSnippet),
code.indexOf(segmentSnippet) + segmentSnippet.length,
]
const pathToSegmentNode: PathToNode = getNodePathFromSourceRange(
ast,
segmentRange
)
const extrudeRange: [number, number] = [
code.indexOf(extrudeSnippet),
code.indexOf(extrudeSnippet) + extrudeSnippet.length,
]
const pathToExtrudeNode: PathToNode = getNodePathFromSourceRange(
ast,
extrudeRange
)
if (pathToExtrudeNode instanceof Error) {
return new Error('Path to extrude node not found')
}
// const radius = createLiteral(5) as Value
const result = addFillet(ast, pathToSegmentNode, pathToExtrudeNode, radius)
if (result instanceof Error) {
return result
}
const { modifiedAst } = result
const newCode = recast(modifiedAst)
expect(newCode).toContain(expectedCode)
}
describe('Testing addFillet', () => {
/**
* 1. Ideal Case
*/
it('should add a fillet to a specific segment after extrusion, clean', async () => {
const code = `
const sketch001 = startSketchOn('XZ')
|> startProfileAt([2.16, 49.67], %)
|> line([101.49, 139.93], %)
|> line([60.04, -55.72], %)
|> line([1.29, -115.74], %)
|> line([-87.24, -47.08], %)
|> tangentialArcTo([56.15, -94.58], %)
|> tangentialArcTo([14.68, -104.52], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(50, sketch001)
`
const segmentSnippet = `line([60.04, -55.72], %)`
const extrudeSnippet = `const extrude001 = extrude(50, sketch001)`
const radius = createLiteral(5) as Value
const expectedCode = `const sketch001 = startSketchOn('XZ')
|> startProfileAt([2.16, 49.67], %)
|> line([101.49, 139.93], %)
|> line([60.04, -55.72], %, $seg01)
|> line([1.29, -115.74], %)
|> line([-87.24, -47.08], %)
|> tangentialArcTo([56.15, -94.58], %)
|> tangentialArcTo([14.68, -104.52], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(50, sketch001)
|> fillet({ radius: 5, tags: [seg01] }, %)`
await runFilletTest(
code,
segmentSnippet,
extrudeSnippet,
radius,
expectedCode
)
})
/**
* 2. Case of existing tag in the other line
*/
it('should add a fillet to a specific segment after extrusion with existing tag in any other line', async () => {
const code = `
const sketch001 = startSketchOn('XZ')
|> startProfileAt([2.16, 49.67], %)
|> line([101.49, 139.93], %)
|> line([60.04, -55.72], %)
|> line([1.29, -115.74], %)
|> line([-87.24, -47.08], %, $seg01)
|> tangentialArcTo([56.15, -94.58], %)
|> tangentialArcTo([14.68, -104.52], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(50, sketch001)
`
const segmentSnippet = `line([60.04, -55.72], %)`
const extrudeSnippet = `const extrude001 = extrude(50, sketch001)`
const radius = createLiteral(5) as Value
const expectedCode = `const sketch001 = startSketchOn('XZ')
|> startProfileAt([2.16, 49.67], %)
|> line([101.49, 139.93], %)
|> line([60.04, -55.72], %, $seg02)
|> line([1.29, -115.74], %)
|> line([-87.24, -47.08], %, $seg01)
|> tangentialArcTo([56.15, -94.58], %)
|> tangentialArcTo([14.68, -104.52], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(50, sketch001)
|> fillet({ radius: 5, tags: [seg02] }, %)`
await runFilletTest(
code,
segmentSnippet,
extrudeSnippet,
radius,
expectedCode
)
})
/**
* 3. Case of existing tag in the fillet line
*/
it('should add a fillet to a specific segment after extrusion with existing tag in that exact line', async () => {
const code = `
const sketch001 = startSketchOn('XZ')
|> startProfileAt([2.16, 49.67], %)
|> line([101.49, 139.93], %)
|> line([60.04, -55.72], %)
|> line([1.29, -115.74], %)
|> line([-87.24, -47.08], %, $seg03)
|> tangentialArcTo([56.15, -94.58], %)
|> tangentialArcTo([14.68, -104.52], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(50, sketch001)
`
const segmentSnippet = `line([-87.24, -47.08], %, $seg03)`
const extrudeSnippet = `const extrude001 = extrude(50, sketch001)`
const radius = createLiteral(5) as Value
const expectedCode = `const sketch001 = startSketchOn('XZ')
|> startProfileAt([2.16, 49.67], %)
|> line([101.49, 139.93], %)
|> line([60.04, -55.72], %)
|> line([1.29, -115.74], %)
|> line([-87.24, -47.08], %, $seg03)
|> tangentialArcTo([56.15, -94.58], %)
|> tangentialArcTo([14.68, -104.52], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(50, sketch001)
|> fillet({ radius: 5, tags: [seg03] }, %)`
await runFilletTest(
code,
segmentSnippet,
extrudeSnippet,
radius,
expectedCode
)
})
/**
* 4. Case of existing fillet on some other segment
*/
it('should add another fillet after the existing fillet', async () => {
const code = `const sketch001 = startSketchOn('XZ')
|> startProfileAt([2.16, 49.67], %)
|> line([101.49, 139.93], %)
|> line([60.04, -55.72], %)
|> line([1.29, -115.74], %)
|> line([-87.24, -47.08], %, $seg03)
|> tangentialArcTo([56.15, -94.58], %)
|> tangentialArcTo([14.68, -104.52], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(50, sketch001)
|> fillet({ radius: 10, tags: [seg03] }, %)`
const segmentSnippet = `line([60.04, -55.72], %)`
const extrudeSnippet = `const extrude001 = extrude(50, sketch001)`
const radius = createLiteral(5) as Value
const expectedCode = `const sketch001 = startSketchOn('XZ')
|> startProfileAt([2.16, 49.67], %)
|> line([101.49, 139.93], %)
|> line([60.04, -55.72], %, $seg01)
|> line([1.29, -115.74], %)
|> line([-87.24, -47.08], %, $seg03)
|> tangentialArcTo([56.15, -94.58], %)
|> tangentialArcTo([14.68, -104.52], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(50, sketch001)
|> fillet({ radius: 10, tags: [seg03] }, %)
|> fillet({ radius: 5, tags: [seg01] }, %)`
await runFilletTest(
code,
segmentSnippet,
extrudeSnippet,
radius,
expectedCode
)
})
})
describe('Testing isTagUsedInFillet', () => {
const code = `const sketch001 = startSketchOn('XZ')
|> startProfileAt([7.72, 4.13], %)
|> line([7.11, 3.48], %, $seg01)
|> line([-3.29, -13.85], %)
|> line([-6.37, 3.88], %, $seg02)
|> close(%)
const extrude001 = extrude(-5, sketch001)
|> fillet({
radius: 1.11,
tags: [
getOppositeEdge(seg01, %),
seg01,
getPreviousAdjacentEdge(seg02, %)
]
}, %)
`
it('should correctly identify getOppositeEdge and baseEdge edges', () => {
const ast = parse(code)
if (err(ast)) return
const lineOfInterest = `line([7.11, 3.48], %, $seg01)`
const range: [number, number] = [
code.indexOf(lineOfInterest),
code.indexOf(lineOfInterest) + lineOfInterest.length,
]
const pathToNode = getNodePathFromSourceRange(ast, range)
if (err(pathToNode)) return
const callExp = getNodeFromPath<CallExpression>(
ast,
pathToNode,
'CallExpression'
)
if (err(callExp)) return
const edges = isTagUsedInFillet({ ast, callExp: callExp.node })
expect(edges).toEqual(['getOppositeEdge', 'baseEdge'])
})
it('should correctly identify getPreviousAdjacentEdge edges', () => {
const ast = parse(code)
if (err(ast)) return
const lineOfInterest = `line([-6.37, 3.88], %, $seg02)`
const range: [number, number] = [
code.indexOf(lineOfInterest),
code.indexOf(lineOfInterest) + lineOfInterest.length,
]
const pathToNode = getNodePathFromSourceRange(ast, range)
if (err(pathToNode)) return
const callExp = getNodeFromPath<CallExpression>(
ast,
pathToNode,
'CallExpression'
)
if (err(callExp)) return
const edges = isTagUsedInFillet({ ast, callExp: callExp.node })
expect(edges).toEqual(['getPreviousAdjacentEdge'])
})
it('should correctly identify no edges', () => {
const ast = parse(code)
if (err(ast)) return
const lineOfInterest = `line([-3.29, -13.85], %)`
const range: [number, number] = [
code.indexOf(lineOfInterest),
code.indexOf(lineOfInterest) + lineOfInterest.length,
]
const pathToNode = getNodePathFromSourceRange(ast, range)
if (err(pathToNode)) return
const callExp = getNodeFromPath<CallExpression>(
ast,
pathToNode,
'CallExpression'
)
if (err(callExp)) return
const edges = isTagUsedInFillet({ ast, callExp: callExp.node })
expect(edges).toEqual([])
})
})

View File

@ -0,0 +1,405 @@
import {
ArrayExpression,
CallExpression,
ObjectExpression,
PathToNode,
Program,
Value,
VariableDeclaration,
VariableDeclarator,
} from '../wasm'
import {
createCallExpressionStdLib,
createLiteral,
createPipeSubstitution,
createObjectExpression,
createArrayExpression,
createIdentifier,
createPipeExpression,
} from '../modifyAst'
import {
getNodeFromPath,
getNodePathFromSourceRange,
hasSketchPipeBeenExtruded,
traverse,
} from '../queryAst'
import {
addTagForSketchOnFace,
getTagFromCallExpression,
sketchLineHelperMap,
} from '../std/sketch'
import { err } from 'lib/trap'
import { Selections, canFilletSelection } from 'lib/selections'
// import { forEach } from 'jszip'
export function addFillet(
node: Program,
pathToSegmentNode: PathToNode,
pathToExtrudeNode: PathToNode,
radius = createLiteral(5) as Value
// shouldPipe = false, // TODO: Implement this feature
): { modifiedAst: Program; pathToFilletNode: PathToNode } | Error {
// close ast to make mutations safe
let _node: Program = JSON.parse(JSON.stringify(node))
/**
* Add Tag to the Segment Expression
*/
// Find the specific sketch segment to tag with the new tag
const sketchSegmentChunk = getNodeFromPath(
_node,
pathToSegmentNode,
'CallExpression'
)
if (err(sketchSegmentChunk)) return sketchSegmentChunk
const { node: sketchSegmentNode } = sketchSegmentChunk as {
node: CallExpression
}
// Check whether selection is a valid segment from sketchLineHelpersMap
if (!(sketchSegmentNode.callee.name in sketchLineHelperMap)) {
return new Error('Selection is not a sketch segment')
}
// Add tag to the sketch segment or use existing tag
const taggedSegment = addTagForSketchOnFace(
{
// previousProgramMemory: programMemory,
pathToNode: pathToSegmentNode,
node: _node,
},
sketchSegmentNode.callee.name
)
if (err(taggedSegment)) return taggedSegment
const { tag } = taggedSegment
/**
* Find Extrude Expression automatically
*/
// 1. Get the sketch name
/**
* Add Fillet to the Extrude expression
*/
// Create the fillet call expression in one line
const filletCall = createCallExpressionStdLib('fillet', [
createObjectExpression({
radius: radius,
tags: createArrayExpression([createIdentifier(tag)]),
}),
createPipeSubstitution(),
])
// Locate the extrude call
const extrudeChunk = getNodeFromPath<VariableDeclaration>(
_node,
pathToExtrudeNode,
'VariableDeclaration'
)
if (err(extrudeChunk)) return extrudeChunk
const { node: extrudeVarDecl } = extrudeChunk
const extrudeDeclarator = extrudeVarDecl.declarations[0]
const extrudeInit = extrudeDeclarator.init
if (
!extrudeDeclarator ||
(extrudeInit.type !== 'CallExpression' &&
extrudeInit.type !== 'PipeExpression')
) {
return new Error('Extrude PipeExpression / CallExpression not found.')
}
// determine if extrude is in a PipeExpression or CallExpression
// CallExpression - no fillet
// PipeExpression - fillet exists
const getPathToNodeOfFilletLiteral = (
pathToExtrudeNode: PathToNode,
extrudeDeclarator: VariableDeclarator,
tag: string
): PathToNode => {
let pathToFilletObj: any
let inFillet = false
traverse(extrudeDeclarator.init, {
enter(node, path) {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
inFillet = true
}
if (inFillet && node.type === 'ObjectExpression') {
const hasTag = node.properties.some((prop) => {
const isTagProp = prop.key.name === 'tags'
if (isTagProp && prop.value.type === 'ArrayExpression') {
return prop.value.elements.some(
(element) =>
element.type === 'Identifier' && element.name === tag
)
}
return false
})
if (!hasTag) return false
pathToFilletObj = path
node.properties.forEach((prop, index) => {
if (prop.key.name === 'radius') {
pathToFilletObj.push(
['properties', 'ObjectExpression'],
[index, 'index'],
['value', 'Property']
)
}
})
}
},
leave(node) {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
inFillet = false
}
},
})
let indexOfPipeExpression = pathToExtrudeNode.findIndex(
(path) => path[1] === 'PipeExpression'
)
indexOfPipeExpression =
indexOfPipeExpression === -1
? pathToExtrudeNode.length
: indexOfPipeExpression
return [
...pathToExtrudeNode.slice(0, indexOfPipeExpression),
...pathToFilletObj,
]
}
if (extrudeInit.type === 'CallExpression') {
// 1. no fillet case
extrudeDeclarator.init = createPipeExpression([extrudeInit, filletCall])
return {
modifiedAst: _node,
pathToFilletNode: getPathToNodeOfFilletLiteral(
pathToExtrudeNode,
extrudeDeclarator,
tag
),
}
} else if (extrudeInit.type === 'PipeExpression') {
// 2. fillet case
// there are 2 options here:
const existingFilletCall = extrudeInit.body.find((node) => {
return node.type === 'CallExpression' && node.callee.name === 'fillet'
})
if (!existingFilletCall || existingFilletCall.type !== 'CallExpression') {
return new Error('Fillet CallExpression not found.')
}
// check if the existing fillet has the same tag as the new fillet
let filletTag = null
if (existingFilletCall.arguments[0].type === 'ObjectExpression') {
const properties = (existingFilletCall.arguments[0] as ObjectExpression)
.properties
const tagsProperty = properties.find((prop) => prop.key.name === 'tags')
if (tagsProperty && tagsProperty.value.type === 'ArrayExpression') {
const elements = (tagsProperty.value as ArrayExpression).elements
if (elements.length > 0 && elements[0].type === 'Identifier') {
filletTag = elements[0].name
}
}
} else {
return new Error('Expected an ObjectExpression node')
}
if (filletTag !== tag) {
extrudeInit.body.push(filletCall)
return {
modifiedAst: _node,
pathToFilletNode: getPathToNodeOfFilletLiteral(
pathToExtrudeNode,
extrudeDeclarator,
tag
),
}
}
} else {
return new Error('Unsupported extrude type.')
}
return new Error('Unsupported extrude type.')
}
export const hasValidFilletSelection = ({
selectionRanges,
ast,
code,
}: {
selectionRanges: Selections
ast: Program
code: string
}) => {
// case 0: check if there is anything filletable in the scene
let extrudeExists = false
traverse(ast, {
enter(node) {
if (node.type === 'CallExpression' && node.callee.name === 'extrude') {
extrudeExists = true
}
},
})
if (!extrudeExists) return false
// case 1: nothing selected, test whether the extrusion exists
if (selectionRanges) {
if (selectionRanges.codeBasedSelections.length === 0) {
return true
}
const range0 = selectionRanges.codeBasedSelections[0].range[0]
const codeLength = code.length
if (range0 === codeLength) {
return true
}
}
// case 2: sketch segment selected, test whether it is extruded
// TODO: add loft / sweep check
if (selectionRanges.codeBasedSelections.length > 0) {
const isExtruded = hasSketchPipeBeenExtruded(
selectionRanges.codeBasedSelections[0],
ast
)
if (isExtruded) {
const pathToSelectedNode = getNodePathFromSourceRange(
ast,
selectionRanges.codeBasedSelections[0].range
)
const segmentNode = getNodeFromPath<CallExpression>(
ast,
pathToSelectedNode,
'CallExpression'
)
if (err(segmentNode)) return false
if (segmentNode.node.type === 'CallExpression') {
const segmentName = segmentNode.node.callee.name
if (segmentName in sketchLineHelperMap) {
const edges = isTagUsedInFillet({
ast,
callExp: segmentNode.node,
})
// edge has already been filleted
if (
['edge', 'default'].includes(
selectionRanges.codeBasedSelections[0].type
) &&
edges.includes('baseEdge')
)
return false
return true
} else {
return false
}
}
} else {
return false
}
}
return canFilletSelection(selectionRanges)
}
type EdgeTypes =
| 'baseEdge'
| 'getNextAdjacentEdge'
| 'getPreviousAdjacentEdge'
| 'getOppositeEdge'
export const isTagUsedInFillet = ({
ast,
callExp,
}: {
ast: Program
callExp: CallExpression
}): Array<EdgeTypes> => {
const tag = getTagFromCallExpression(callExp)
if (err(tag)) return []
let inFillet = false
let inObj = false
let inTagHelper: EdgeTypes | '' = ''
const edges: Array<EdgeTypes> = []
traverse(ast, {
enter: (node) => {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
inFillet = true
}
if (inFillet && node.type === 'ObjectExpression') {
node.properties.forEach((prop) => {
if (
prop.key.name === 'tags' &&
prop.value.type === 'ArrayExpression'
) {
inObj = true
}
})
}
if (
inObj &&
inFillet &&
node.type === 'CallExpression' &&
(node.callee.name === 'getOppositeEdge' ||
node.callee.name === 'getNextAdjacentEdge' ||
node.callee.name === 'getPreviousAdjacentEdge')
) {
inTagHelper = node.callee.name
}
if (
inObj &&
inFillet &&
!inTagHelper &&
node.type === 'Identifier' &&
node.name === tag
) {
edges.push('baseEdge')
}
if (
inObj &&
inFillet &&
inTagHelper &&
node.type === 'Identifier' &&
node.name === tag
) {
edges.push(inTagHelper)
}
},
leave: (node) => {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
inFillet = false
}
if (inFillet && node.type === 'ObjectExpression') {
node.properties.forEach((prop) => {
if (
prop.key.name === 'tags' &&
prop.value.type === 'ArrayExpression'
) {
inObj = true
}
})
}
if (
inObj &&
inFillet &&
node.type === 'CallExpression' &&
(node.callee.name === 'getOppositeEdge' ||
node.callee.name === 'getNextAdjacentEdge' ||
node.callee.name === 'getPreviousAdjacentEdge')
) {
inTagHelper = ''
}
},
})
return edges
}

View File

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

View File

@ -221,7 +221,7 @@ describe('testing addTagForSketchOnFace', () => {
const pathToNode = getNodePathFromSourceRange(ast, sourceRange) const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
const sketchOnFaceRetVal = addTagForSketchOnFace( const sketchOnFaceRetVal = addTagForSketchOnFace(
{ {
previousProgramMemory: programMemory, // previousProgramMemory: programMemory, // redundant?
pathToNode, pathToNode,
node: ast, node: ast,
}, },

View File

@ -28,7 +28,6 @@ import { createPipeExpression, splitPathAtPipeExpression } from '../modifyAst'
import { import {
SketchLineHelper, SketchLineHelper,
ModifyAstBase,
TransformCallback, TransformCallback,
ConstrainInfo, ConstrainInfo,
RawValues, RawValues,
@ -37,6 +36,7 @@ import {
SingleValueInput, SingleValueInput,
VarValueKeys, VarValueKeys,
ArrayOrObjItemInput, ArrayOrObjItemInput,
AddTagInfo,
} from 'lang/std/stdTypes' } from 'lang/std/stdTypes'
import { import {
@ -308,6 +308,18 @@ function singleRawValueHelper(
] ]
} }
function getTag(index = 2): SketchLineHelper['getTag'] {
return (callExp: CallExpression) => {
if (callExp.type !== 'CallExpression')
return new Error('Not a CallExpression')
const arg = callExp.arguments?.[index]
if (!arg) return new Error('No argument')
if (arg.type !== 'TagDeclarator')
return new Error('Tag not a TagDeclarator')
return arg.value
}
}
export const lineTo: SketchLineHelper = { export const lineTo: SketchLineHelper = {
add: ({ add: ({
node, node,
@ -377,6 +389,7 @@ export const lineTo: SketchLineHelper = {
pathToNode, pathToNode,
} }
}, },
getTag: getTag(),
addTag: addTag(), addTag: addTag(),
getConstraintInfo: (callExp, ...args) => getConstraintInfo: (callExp, ...args) =>
commonConstraintInfoHelper( commonConstraintInfoHelper(
@ -503,6 +516,7 @@ export const line: SketchLineHelper = {
pathToNode, pathToNode,
} }
}, },
getTag: getTag(),
addTag: addTag(), addTag: addTag(),
getConstraintInfo: (callExp, ...args) => getConstraintInfo: (callExp, ...args) =>
commonConstraintInfoHelper( commonConstraintInfoHelper(
@ -563,6 +577,7 @@ export const xLineTo: SketchLineHelper = {
pathToNode, pathToNode,
} }
}, },
getTag: getTag(),
addTag: addTag(), addTag: addTag(),
getConstraintInfo: (callExp, ...args) => getConstraintInfo: (callExp, ...args) =>
horzVertConstraintInfoHelper( horzVertConstraintInfoHelper(
@ -623,6 +638,7 @@ export const yLineTo: SketchLineHelper = {
pathToNode, pathToNode,
} }
}, },
getTag: getTag(),
addTag: addTag(), addTag: addTag(),
getConstraintInfo: (callExp, ...args) => getConstraintInfo: (callExp, ...args) =>
horzVertConstraintInfoHelper( horzVertConstraintInfoHelper(
@ -682,6 +698,7 @@ export const xLine: SketchLineHelper = {
pathToNode, pathToNode,
} }
}, },
getTag: getTag(),
addTag: addTag(), addTag: addTag(),
getConstraintInfo: (callExp, ...args) => getConstraintInfo: (callExp, ...args) =>
horzVertConstraintInfoHelper( horzVertConstraintInfoHelper(
@ -738,6 +755,7 @@ export const yLine: SketchLineHelper = {
pathToNode, pathToNode,
} }
}, },
getTag: getTag(),
addTag: addTag(), addTag: addTag(),
getConstraintInfo: (callExp, ...args) => getConstraintInfo: (callExp, ...args) =>
horzVertConstraintInfoHelper( horzVertConstraintInfoHelper(
@ -830,6 +848,7 @@ export const tangentialArcTo: SketchLineHelper = {
pathToNode, pathToNode,
} }
}, },
getTag: getTag(),
addTag: addTag(), addTag: addTag(),
getConstraintInfo: (callExp: CallExpression, code, pathToNode) => { getConstraintInfo: (callExp: CallExpression, code, pathToNode) => {
if (callExp.type !== 'CallExpression') return [] if (callExp.type !== 'CallExpression') return []
@ -948,6 +967,7 @@ export const angledLine: SketchLineHelper = {
pathToNode, pathToNode,
} }
}, },
getTag: getTag(),
addTag: addTag(), addTag: addTag(),
getConstraintInfo: (callExp, ...args) => getConstraintInfo: (callExp, ...args) =>
commonConstraintInfoHelper( commonConstraintInfoHelper(
@ -1044,6 +1064,7 @@ export const angledLineOfXLength: SketchLineHelper = {
pathToNode, pathToNode,
} }
}, },
getTag: getTag(),
addTag: addTag(), addTag: addTag(),
getConstraintInfo: (callExp, ...args) => getConstraintInfo: (callExp, ...args) =>
commonConstraintInfoHelper( commonConstraintInfoHelper(
@ -1140,6 +1161,7 @@ export const angledLineOfYLength: SketchLineHelper = {
pathToNode, pathToNode,
} }
}, },
getTag: getTag(),
addTag: addTag(), addTag: addTag(),
getConstraintInfo: (callExp, ...args) => getConstraintInfo: (callExp, ...args) =>
commonConstraintInfoHelper( commonConstraintInfoHelper(
@ -1227,6 +1249,7 @@ export const angledLineToX: SketchLineHelper = {
pathToNode, pathToNode,
} }
}, },
getTag: getTag(),
addTag: addTag(), addTag: addTag(),
getConstraintInfo: (callExp, ...args) => getConstraintInfo: (callExp, ...args) =>
commonConstraintInfoHelper( commonConstraintInfoHelper(
@ -1316,6 +1339,7 @@ export const angledLineToY: SketchLineHelper = {
pathToNode, pathToNode,
} }
}, },
getTag: getTag(),
addTag: addTag(), addTag: addTag(),
getConstraintInfo: (callExp, ...args) => getConstraintInfo: (callExp, ...args) =>
commonConstraintInfoHelper( commonConstraintInfoHelper(
@ -1440,6 +1464,7 @@ export const angledLineThatIntersects: SketchLineHelper = {
pathToNode, pathToNode,
} }
}, },
getTag: getTag(),
addTag: addTag(), addTag: addTag(),
getConstraintInfo: (callExp: CallExpression, code, pathToNode) => { getConstraintInfo: (callExp: CallExpression, code, pathToNode) => {
if (callExp.type !== 'CallExpression') return [] if (callExp.type !== 'CallExpression') return []
@ -1792,10 +1817,7 @@ export function replaceSketchLine({
return { modifiedAst, valueUsedInTransform, pathToNode } return { modifiedAst, valueUsedInTransform, pathToNode }
} }
export function addTagForSketchOnFace( export function addTagForSketchOnFace(a: AddTagInfo, expressionName: string) {
a: ModifyAstBase,
expressionName: string
) {
if (expressionName === 'close') { if (expressionName === 'close') {
return addTag(1)(a) return addTag(1)(a)
} }
@ -1806,6 +1828,17 @@ export function addTagForSketchOnFace(
return new Error(`"${expressionName}" is not a sketch line helper`) return new Error(`"${expressionName}" is not a sketch line helper`)
} }
export function getTagFromCallExpression(
callExp: CallExpression
): string | Error {
if (callExp.callee.name === 'close') return getTag(1)(callExp)
if (callExp.callee.name in sketchLineHelperMap) {
const { getTag } = sketchLineHelperMap[callExp.callee.name]
return getTag(callExp)
}
return new Error(`"${callExp.callee.name}" is not a sketch line helper`)
}
function isAngleLiteral(lineArugement: Value): boolean { function isAngleLiteral(lineArugement: Value): boolean {
return lineArugement?.type === 'ArrayExpression' return lineArugement?.type === 'ArrayExpression'
? isLiteralArrayOrStatic(lineArugement.elements[0]) ? isLiteralArrayOrStatic(lineArugement.elements[0])
@ -1816,9 +1849,7 @@ function isAngleLiteral(lineArugement: Value): boolean {
: false : false
} }
type addTagFn = ( type addTagFn = (a: AddTagInfo) => { modifiedAst: Program; tag: string } | Error
a: ModifyAstBase
) => { modifiedAst: Program; tag: string } | Error
function addTag(tagIndex = 2): addTagFn { function addTag(tagIndex = 2): addTagFn {
return ({ node, pathToNode }) => { return ({ node, pathToNode }) => {

View File

@ -32,6 +32,11 @@ export interface ModifyAstBase {
pathToNode: PathToNode pathToNode: PathToNode
} }
export interface AddTagInfo {
node: Program
pathToNode: PathToNode
}
interface addCall extends ModifyAstBase { interface addCall extends ModifyAstBase {
to: [number, number] to: [number, number]
from: [number, number] from: [number, number]
@ -127,7 +132,8 @@ export interface SketchLineHelper {
pathToNode: PathToNode pathToNode: PathToNode
} }
| Error | Error
addTag: (a: ModifyAstBase) => getTag: (a: CallExpression) => string | Error
addTag: (a: AddTagInfo) =>
| { | {
modifiedAst: Program modifiedAst: Program
tag: string tag: string

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { CommandSetConfig, KclCommandValue } from 'lib/commandTypes' import { StateMachineCommandSetConfig, KclCommandValue } from 'lib/commandTypes'
import { KCL_DEFAULT_LENGTH } from 'lib/constants' import { KCL_DEFAULT_LENGTH } from 'lib/constants'
import { Selections } from 'lib/selections' import { Selections } from 'lib/selections'
import { modelingMachine } from 'machines/modelingMachine' import { modelingMachine, SketchTool } from 'machines/modelingMachine'
type OutputFormat = Models['OutputFormat_type'] type OutputFormat = Models['OutputFormat_type']
type OutputTypeKey = OutputFormat['type'] type OutputTypeKey = OutputFormat['type']
@ -27,9 +27,17 @@ export type ModelingCommandSchema = {
// result: (typeof EXTRUSION_RESULTS)[number] // result: (typeof EXTRUSION_RESULTS)[number]
distance: KclCommandValue distance: KclCommandValue
} }
Fillet: {
// todo
selection: Selections
radius: KclCommandValue
}
'change tool': {
tool: SketchTool
}
} }
export const modelingMachineConfig: CommandSetConfig< export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
typeof modelingMachine, typeof modelingMachine,
ModelingCommandSchema ModelingCommandSchema
> = { > = {
@ -37,22 +45,47 @@ export const modelingMachineConfig: CommandSetConfig<
description: 'Enter sketch mode.', description: 'Enter sketch mode.',
icon: 'sketch', icon: 'sketch',
}, },
// TODO the event is no 'change tool' with data: 'line', 'rectangle' etc 'change tool': [
// 'Equip Line tool': { {
// description: 'Start drawing straight lines.', description: 'Start drawing straight lines.',
// icon: 'line', icon: 'line',
// displayName: 'Line', displayName: 'Line',
// }, args: {
// 'Equip tangential arc to': { tool: {
// description: 'Start drawing an arc tangent to the current segment.', defaultValue: 'line',
// icon: 'arc', required: true,
// displayName: 'Tangential Arc', skip: true,
// }, inputType: 'string',
// 'Equip rectangle tool': { },
// description: 'Start drawing a rectangle.', },
// icon: 'rectangle', },
// displayName: 'Rectangle', {
// }, description: 'Start drawing an arc tangent to the current segment.',
icon: 'arc',
displayName: 'Tangential Arc',
args: {
tool: {
defaultValue: 'tangentialArc',
required: true,
skip: true,
inputType: 'string',
},
},
},
{
description: 'Start drawing a rectangle.',
icon: 'rectangle',
displayName: 'Rectangle',
args: {
tool: {
defaultValue: 'rectangle',
required: true,
skip: true,
inputType: 'string',
},
},
},
],
Export: { Export: {
description: 'Export the current model.', description: 'Export the current model.',
icon: 'exportFile', icon: 'exportFile',
@ -157,4 +190,36 @@ export const modelingMachineConfig: CommandSetConfig<
}, },
}, },
}, },
Fillet: {
// todo
description: 'Fillet edge',
icon: 'fillet',
needsReview: true,
args: {
selection: {
inputType: 'selection',
selectionTypes: [
'default',
'line-end',
'line-mid',
'extrude-wall', // to fix: accespts only this selection type
'start-cap',
'end-cap',
'point',
'edge',
'line',
'arc',
'all',
],
multiple: true, // TODO: multiple selection like in extrude command
required: true,
skip: true,
},
radius: {
inputType: 'kcl',
defaultValue: KCL_DEFAULT_LENGTH,
required: true,
},
},
},
} }

View File

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

View File

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

View File

@ -51,6 +51,7 @@ export const ONBOARDING_PROJECT_NAME = 'Tutorial Project $nn'
export const KCL_DEFAULT_CONSTANT_PREFIXES = { export const KCL_DEFAULT_CONSTANT_PREFIXES = {
SKETCH: 'sketch', SKETCH: 'sketch',
EXTRUDE: 'extrude', EXTRUDE: 'extrude',
SEGMENT: 'seg',
} as const } as const
/** The default KCL length expression */ /** The default KCL length expression */
export const KCL_DEFAULT_LENGTH = `5` export const KCL_DEFAULT_LENGTH = `5`

View File

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

View File

@ -406,6 +406,17 @@ export function canExtrudeSelection(selection: Selections) {
) )
} }
export function canFilletSelection(selection: Selections) {
const commonNodes = selection.codeBasedSelections.map((_, i) =>
buildCommonNodeFromSelection(selection, i)
) // TODO FILLET DUMMY PLACEHOLDER
return (
!!isSketchPipe(selection) &&
commonNodes.every((n) => nodeHasClose(n)) &&
commonNodes.every((n) => !nodeHasExtrude(n))
)
}
function canExtrudeSelectionItem(selection: Selections, i: number) { function canExtrudeSelectionItem(selection: Selections, i: number) {
const commonNode = buildCommonNodeFromSelection(selection, i) const commonNode = buildCommonNodeFromSelection(selection, i)

View File

@ -37,4 +37,7 @@ if (typeof window !== 'undefined') {
document.addEventListener('mousemove', (e) => document.addEventListener('mousemove', (e) =>
console.log(`await page.mouse.click(${e.clientX}, ${e.clientY})`) console.log(`await page.mouse.click(${e.clientX}, ${e.clientY})`)
) )
;(window as any).enableFillet = () => {
;(window as any)._enableFillet = true
}
} }

View File

@ -6,7 +6,7 @@ import { PrevVariable, findAllPreviousVariables } from 'lang/queryAst'
import { Value, parse } from 'lang/wasm' import { Value, parse } from 'lang/wasm'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { executeAst } from 'lang/langHelpers' import { executeAst } from 'lang/langHelpers'
import { trap } from 'lib/trap' import { err, trap } from 'lib/trap'
const isValidVariableName = (name: string) => const isValidVariableName = (name: string) =>
/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name) /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)
@ -86,6 +86,7 @@ export function useCalculateKclExpression({
const execAstAndSetResult = async () => { const execAstAndSetResult = async () => {
const _code = `const __result__ = ${value}` const _code = `const __result__ = ${value}`
const ast = parse(_code) const ast = parse(_code)
if (err(ast)) return
if (trap(ast, { suppress: true })) return if (trap(ast, { suppress: true })) return
const _programMem: any = { root: {}, return: null } const _programMem: any = { root: {}, return: null }

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

@ -169,7 +169,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -180,7 +180,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -191,7 +191,7 @@ checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -431,7 +431,7 @@ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -631,7 +631,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"strsim 0.10.0", "strsim 0.10.0",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -642,7 +642,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
dependencies = [ dependencies = [
"darling_core", "darling_core",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -697,7 +697,7 @@ checksum = "4078275de501a61ceb9e759d37bdd3d7210e654dbc167ac1a3678ef4435ed57b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
"synstructure", "synstructure",
] ]
@ -726,7 +726,7 @@ dependencies = [
"rustfmt-wrapper", "rustfmt-wrapper",
"serde", "serde",
"serde_tokenstream", "serde_tokenstream",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -737,7 +737,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -764,7 +764,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -936,7 +936,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -1026,7 +1026,7 @@ dependencies = [
"inflections", "inflections",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -1448,7 +1448,7 @@ dependencies = [
"pretty_assertions", "pretty_assertions",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -1824,7 +1824,7 @@ dependencies = [
"regex", "regex",
"regex-syntax 0.8.3", "regex-syntax 0.8.3",
"structmeta", "structmeta",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -1842,7 +1842,7 @@ dependencies = [
"bincode", "bincode",
"either", "either",
"fnv", "fnv",
"itertools 0.10.5", "itertools 0.12.1",
"lazy_static", "lazy_static",
"nom", "nom",
"quick-xml", "quick-xml",
@ -1877,7 +1877,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -2041,7 +2041,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"pyo3-macros-backend", "pyo3-macros-backend",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -2054,7 +2054,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"pyo3-build-config", "pyo3-build-config",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -2516,7 +2516,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"serde_derive_internals", "serde_derive_internals",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -2590,7 +2590,7 @@ checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -2601,7 +2601,7 @@ checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -2624,7 +2624,7 @@ checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -2645,7 +2645,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"serde", "serde",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -2782,7 +2782,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"structmeta-derive", "structmeta-derive",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -2793,7 +2793,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -2837,9 +2837,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.70" version = "2.0.71"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f0209b68b3613b093e0ec905354eccaedcfe83b8cb37cbdeae64026c3064c16" checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2860,7 +2860,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -2928,22 +2928,22 @@ dependencies = [
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.61" version = "1.0.62"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.61" version = "1.0.62"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -3039,7 +3039,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -3192,7 +3192,7 @@ checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -3220,7 +3220,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -3297,7 +3297,7 @@ checksum = "c88cc88fd23b5a04528f3a8436024f20010a16ec18eb23c164b1242f65860130"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
"termcolor", "termcolor",
] ]
@ -3455,7 +3455,7 @@ dependencies = [
"proc-macro-error", "proc-macro-error",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]
@ -3516,7 +3516,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -3551,7 +3551,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -3876,7 +3876,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.70", "syn 2.0.71",
] ]
[[package]] [[package]]

View File

@ -20,7 +20,7 @@ quote = "1"
regex = "1.10" regex = "1.10"
serde = { version = "1.0.204", features = ["derive"] } serde = { version = "1.0.204", features = ["derive"] }
serde_tokenstream = "0.2" serde_tokenstream = "0.2"
syn = { version = "2.0.70", features = ["full"] } syn = { version = "2.0.71", features = ["full"] }
[dev-dependencies] [dev-dependencies]
anyhow = "1.0.86" anyhow = "1.0.86"

View File

@ -837,7 +837,7 @@ fn generate_code_block_test(fn_name: &str, code_block: &str, index: usize) -> pr
// Read the output file. // Read the output file.
let actual = image::io::Reader::open(output_file).unwrap().decode().unwrap(); let actual = image::io::Reader::open(output_file).unwrap().decode().unwrap();
twenty_twenty::assert_image(&format!("tests/outputs/{}.png", #test_name_str), &actual, 1.0); twenty_twenty::assert_image(&format!("tests/outputs/{}.png", #test_name_str), &actual, 0.99);
} }
} }

View File

@ -86,7 +86,7 @@ mod test_examples_someFn {
twenty_twenty::assert_image( twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_someFn0"), &format!("tests/outputs/{}.png", "serial_test_example_someFn0"),
&actual, &actual,
1.0, 0.99,
); );
} }
} }

View File

@ -86,7 +86,7 @@ mod test_examples_someFn {
twenty_twenty::assert_image( twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_someFn0"), &format!("tests/outputs/{}.png", "serial_test_example_someFn0"),
&actual, &actual,
1.0, 0.99,
); );
} }
} }

View File

@ -86,7 +86,7 @@ mod test_examples_show {
twenty_twenty::assert_image( twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_show0"), &format!("tests/outputs/{}.png", "serial_test_example_show0"),
&actual, &actual,
1.0, 0.99,
); );
} }
@ -176,7 +176,7 @@ mod test_examples_show {
twenty_twenty::assert_image( twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_show1"), &format!("tests/outputs/{}.png", "serial_test_example_show1"),
&actual, &actual,
1.0, 0.99,
); );
} }
} }

View File

@ -86,7 +86,7 @@ mod test_examples_show {
twenty_twenty::assert_image( twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_show0"), &format!("tests/outputs/{}.png", "serial_test_example_show0"),
&actual, &actual,
1.0, 0.99,
); );
} }
} }

View File

@ -88,7 +88,7 @@ mod test_examples_my_func {
twenty_twenty::assert_image( twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_my_func0"), &format!("tests/outputs/{}.png", "serial_test_example_my_func0"),
&actual, &actual,
1.0, 0.99,
); );
} }
@ -178,7 +178,7 @@ mod test_examples_my_func {
twenty_twenty::assert_image( twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_my_func1"), &format!("tests/outputs/{}.png", "serial_test_example_my_func1"),
&actual, &actual,
1.0, 0.99,
); );
} }
} }

View File

@ -88,7 +88,7 @@ mod test_examples_line_to {
twenty_twenty::assert_image( twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_line_to0"), &format!("tests/outputs/{}.png", "serial_test_example_line_to0"),
&actual, &actual,
1.0, 0.99,
); );
} }
@ -178,7 +178,7 @@ mod test_examples_line_to {
twenty_twenty::assert_image( twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_line_to1"), &format!("tests/outputs/{}.png", "serial_test_example_line_to1"),
&actual, &actual,
1.0, 0.99,
); );
} }
} }

View File

@ -86,7 +86,7 @@ mod test_examples_min {
twenty_twenty::assert_image( twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_min0"), &format!("tests/outputs/{}.png", "serial_test_example_min0"),
&actual, &actual,
1.0, 0.99,
); );
} }
@ -176,7 +176,7 @@ mod test_examples_min {
twenty_twenty::assert_image( twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_min1"), &format!("tests/outputs/{}.png", "serial_test_example_min1"),
&actual, &actual,
1.0, 0.99,
); );
} }
} }

View File

@ -86,7 +86,7 @@ mod test_examples_show {
twenty_twenty::assert_image( twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_show0"), &format!("tests/outputs/{}.png", "serial_test_example_show0"),
&actual, &actual,
1.0, 0.99,
); );
} }
} }

View File

@ -86,7 +86,7 @@ mod test_examples_import {
twenty_twenty::assert_image( twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_import0"), &format!("tests/outputs/{}.png", "serial_test_example_import0"),
&actual, &actual,
1.0, 0.99,
); );
} }
} }

View File

@ -86,7 +86,7 @@ mod test_examples_import {
twenty_twenty::assert_image( twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_import0"), &format!("tests/outputs/{}.png", "serial_test_example_import0"),
&actual, &actual,
1.0, 0.99,
); );
} }
} }

View File

@ -86,7 +86,7 @@ mod test_examples_import {
twenty_twenty::assert_image( twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_import0"), &format!("tests/outputs/{}.png", "serial_test_example_import0"),
&actual, &actual,
1.0, 0.99,
); );
} }
} }

View File

@ -86,7 +86,7 @@ mod test_examples_show {
twenty_twenty::assert_image( twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_show0"), &format!("tests/outputs/{}.png", "serial_test_example_show0"),
&actual, &actual,
1.0, 0.99,
); );
} }
} }

View File

@ -15,7 +15,7 @@ databake = "0.1.8"
kcl-lib = { path = "../kcl" } kcl-lib = { path = "../kcl" }
proc-macro2 = "1" proc-macro2 = "1"
quote = "1" quote = "1"
syn = { version = "2.0.70", features = ["full"] } syn = { version = "2.0.71", features = ["full"] }
[dev-dependencies] [dev-dependencies]
pretty_assertions = "1.4.0" pretty_assertions = "1.4.0"

View File

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

View File

@ -0,0 +1,31 @@
use criterion::{criterion_group, criterion_main, Criterion};
pub fn bench_digest(c: &mut Criterion) {
for (name, file) in [
("pipes_on_pipes", PIPES_PROGRAM),
("big_kitt", KITT_PROGRAM),
("cube", CUBE_PROGRAM),
("math", MATH_PROGRAM),
("mike_stress_test", MIKE_STRESS_TEST_PROGRAM),
] {
let tokens = kcl_lib::token::lexer(file).unwrap();
let prog = kcl_lib::parser::Parser::new(tokens).ast().unwrap();
c.bench_function(&format!("digest_{name}"), move |b| {
let prog = prog.clone();
b.iter(move || {
let mut prog = prog.clone();
prog.compute_digest();
});
});
}
}
criterion_group!(benches, bench_digest);
criterion_main!(benches);
const KITT_PROGRAM: &str = include_str!("../../tests/executor/inputs/kittycad_svg.kcl");
const PIPES_PROGRAM: &str = include_str!("../../tests/executor/inputs/pipes_on_pipes.kcl");
const CUBE_PROGRAM: &str = include_str!("../../tests/executor/inputs/cube.kcl");
const MATH_PROGRAM: &str = include_str!("../../tests/executor/inputs/math.kcl");
const MIKE_STRESS_TEST_PROGRAM: &str = include_str!("../../tests/executor/inputs/mike_stress_test.kcl");

Binary file not shown.

Before

Width:  |  Height:  |  Size: 333 KiB

After

Width:  |  Height:  |  Size: 333 KiB

View File

@ -4,6 +4,10 @@ use kcl_lib::{
settings::types::UnitLength, settings::types::UnitLength,
}; };
/// The minimum permissible difference between asserted twenty-twenty images.
/// i.e. how different the current model snapshot can be from the previous saved one.
const MIN_DIFF: f64 = 0.99;
// mod server; // mod server;
async fn new_context(units: UnitLength) -> Result<ExecutorContext> { async fn new_context(units: UnitLength) -> Result<ExecutorContext> {
@ -83,35 +87,35 @@ const part002 = startSketchOn(part001, here)
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face.png", &result, 0.999); twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn serial_test_riddle_small() { async fn serial_test_riddle_small() {
let code = include_str!("inputs/riddle_small.kcl"); let code = include_str!("inputs/riddle_small.kcl");
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/riddle_small.png", &result, 0.999); twenty_twenty::assert_image("tests/executor/outputs/riddle_small.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn serial_test_lego() { async fn serial_test_lego() {
let code = include_str!("inputs/lego.kcl"); let code = include_str!("inputs/lego.kcl");
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/lego.png", &result, 0.999); twenty_twenty::assert_image("tests/executor/outputs/lego.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn serial_test_pipe_as_arg() { async fn serial_test_pipe_as_arg() {
let code = include_str!("inputs/pipe_as_arg.kcl"); let code = include_str!("inputs/pipe_as_arg.kcl");
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/pipe_as_arg.png", &result, 0.999); twenty_twenty::assert_image("tests/executor/outputs/pipe_as_arg.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn serial_test_pentagon_fillet_sugar() { async fn serial_test_pentagon_fillet_sugar() {
let code = include_str!("inputs/pentagon_fillet_sugar.kcl"); let code = include_str!("inputs/pentagon_fillet_sugar.kcl");
let result = execute_and_snapshot(code, UnitLength::Cm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Cm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/pentagon_fillet_sugar.png", &result, 0.999); twenty_twenty::assert_image("tests/executor/outputs/pentagon_fillet_sugar.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -139,14 +143,14 @@ const part002 = startSketchOn(part001, "start")
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face_start.png", &result, 0.999); twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face_start.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn serial_test_mike_stress_lines() { async fn serial_test_mike_stress_lines() {
let code = include_str!("inputs/mike_stress_test.kcl"); let code = include_str!("inputs/mike_stress_test.kcl");
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/mike_stress_test.png", &result, 0.999); twenty_twenty::assert_image("tests/executor/outputs/mike_stress_test.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -174,7 +178,7 @@ const part002 = startSketchOn(part001, "END")
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face_end.png", &result, 0.999); twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face_end.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -205,7 +209,7 @@ const part002 = startSketchOn(part001, "END")
twenty_twenty::assert_image( twenty_twenty::assert_image(
"tests/executor/outputs/sketch_on_face_end_negative_extrude.png", "tests/executor/outputs/sketch_on_face_end_negative_extrude.png",
&result, &result,
0.999, MIN_DIFF,
); );
} }
@ -242,7 +246,7 @@ async fn serial_test_basic_fillet_cube_start() {
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/basic_fillet_cube_start.png", &result, 0.999); twenty_twenty::assert_image("tests/executor/outputs/basic_fillet_cube_start.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -259,7 +263,7 @@ async fn serial_test_basic_fillet_cube_end() {
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/basic_fillet_cube_end.png", &result, 0.999); twenty_twenty::assert_image("tests/executor/outputs/basic_fillet_cube_end.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -279,7 +283,7 @@ async fn serial_test_basic_fillet_cube_close_opposite() {
twenty_twenty::assert_image( twenty_twenty::assert_image(
"tests/executor/outputs/basic_fillet_cube_close_opposite.png", "tests/executor/outputs/basic_fillet_cube_close_opposite.png",
&result, &result,
0.999, MIN_DIFF,
); );
} }
@ -299,7 +303,7 @@ async fn serial_test_basic_fillet_cube_next_adjacent() {
twenty_twenty::assert_image( twenty_twenty::assert_image(
"tests/executor/outputs/basic_fillet_cube_next_adjacent.png", "tests/executor/outputs/basic_fillet_cube_next_adjacent.png",
&result, &result,
0.999, MIN_DIFF,
); );
} }
@ -319,7 +323,7 @@ async fn serial_test_basic_fillet_cube_previous_adjacent() {
twenty_twenty::assert_image( twenty_twenty::assert_image(
"tests/executor/outputs/basic_fillet_cube_previous_adjacent.png", "tests/executor/outputs/basic_fillet_cube_previous_adjacent.png",
&result, &result,
0.999, MIN_DIFF,
); );
} }
@ -341,7 +345,7 @@ const fnBox = box(3, 6, 10)
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/function_sketch.png", &result, 0.999); twenty_twenty::assert_image("tests/executor/outputs/function_sketch.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -364,7 +368,7 @@ const thing = box([0,0], 3, 6, 10)"#;
twenty_twenty::assert_image( twenty_twenty::assert_image(
"tests/executor/outputs/function_sketch_with_position.png", "tests/executor/outputs/function_sketch_with_position.png",
&result, &result,
0.999, MIN_DIFF,
); );
} }
@ -382,7 +386,7 @@ async fn serial_test_execute_with_angled_line() {
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/angled_line.png", &result, 0.999); twenty_twenty::assert_image("tests/executor/outputs/angled_line.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -408,7 +412,7 @@ const bracket = startSketchOn('XY')
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/parametric.png", &result, 0.999); twenty_twenty::assert_image("tests/executor/outputs/parametric.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -442,7 +446,7 @@ const bracket = startSketchAt([0, 0])
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/parametric_with_tan_arc.png", &result, 0.999); twenty_twenty::assert_image("tests/executor/outputs/parametric_with_tan_arc.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -470,7 +474,7 @@ async fn serial_test_execute_i_shape() {
let code = include_str!("inputs/i_shape.kcl"); let code = include_str!("inputs/i_shape.kcl");
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/i_shape.png", &result, 0.999); twenty_twenty::assert_image("tests/executor/outputs/i_shape.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -479,7 +483,7 @@ async fn serial_test_execute_pipes_on_pipes() {
let code = include_str!("inputs/pipes_on_pipes.kcl"); let code = include_str!("inputs/pipes_on_pipes.kcl");
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/pipes_on_pipes.png", &result, 0.999); twenty_twenty::assert_image("tests/executor/outputs/pipes_on_pipes.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -487,7 +491,7 @@ async fn serial_test_execute_cylinder() {
let code = include_str!("inputs/cylinder.kcl"); let code = include_str!("inputs/cylinder.kcl");
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/cylinder.png", &result, 0.999); twenty_twenty::assert_image("tests/executor/outputs/cylinder.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -495,7 +499,7 @@ async fn serial_test_execute_kittycad_svg() {
let code = include_str!("inputs/kittycad_svg.kcl"); let code = include_str!("inputs/kittycad_svg.kcl");
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/kittycad_svg.png", &result, 0.999); twenty_twenty::assert_image("tests/executor/outputs/kittycad_svg.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -523,7 +527,7 @@ const pt2 = b2.value[0]
twenty_twenty::assert_image( twenty_twenty::assert_image(
"tests/executor/outputs/member_expression_sketch_group.png", "tests/executor/outputs/member_expression_sketch_group.png",
&result, &result,
1.0, MIN_DIFF,
); );
} }
@ -536,7 +540,7 @@ async fn serial_test_helix_defaults() {
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/helix_defaults.png", &result, 1.0); twenty_twenty::assert_image("tests/executor/outputs/helix_defaults.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -551,7 +555,7 @@ async fn serial_test_helix_defaults_negative_extrude() {
twenty_twenty::assert_image( twenty_twenty::assert_image(
"tests/executor/outputs/helix_defaults_negative_extrude.png", "tests/executor/outputs/helix_defaults_negative_extrude.png",
&result, &result,
1.0, MIN_DIFF,
); );
} }
@ -564,7 +568,7 @@ async fn serial_test_helix_ccw() {
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/helix_ccw.png", &result, 1.0); twenty_twenty::assert_image("tests/executor/outputs/helix_ccw.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -576,7 +580,7 @@ async fn serial_test_helix_with_length() {
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/helix_with_length.png", &result, 1.0); twenty_twenty::assert_image("tests/executor/outputs/helix_with_length.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -591,7 +595,7 @@ async fn serial_test_dimensions_match() {
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/dimensions_match.png", &result, 1.0); twenty_twenty::assert_image("tests/executor/outputs/dimensions_match.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -608,7 +612,7 @@ const body = startSketchOn('XY')
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/close_arc.png", &result, 0.999); twenty_twenty::assert_image("tests/executor/outputs/close_arc.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -634,7 +638,7 @@ let thing = box(-12, -15, 10)
box(-20, -5, 10)"#; box(-20, -5, 10)"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/negative_args.png", &result, 0.999); twenty_twenty::assert_image("tests/executor/outputs/negative_args.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -647,7 +651,7 @@ async fn serial_test_basic_tangential_arc() {
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/tangential_arc.png", &result, 0.999); twenty_twenty::assert_image("tests/executor/outputs/tangential_arc.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -660,7 +664,11 @@ async fn serial_test_basic_tangential_arc_with_point() {
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/tangential_arc_with_point.png", &result, 0.999); twenty_twenty::assert_image(
"tests/executor/outputs/tangential_arc_with_point.png",
&result,
MIN_DIFF,
);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -673,7 +681,7 @@ async fn serial_test_basic_tangential_arc_to() {
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/tangential_arc_to.png", &result, 0.999); twenty_twenty::assert_image("tests/executor/outputs/tangential_arc_to.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -703,7 +711,7 @@ box(-20, -5, 10, 'xy')"#;
twenty_twenty::assert_image( twenty_twenty::assert_image(
"tests/executor/outputs/different_planes_same_drawing.png", "tests/executor/outputs/different_planes_same_drawing.png",
&result, &result,
0.999, MIN_DIFF,
); );
} }
@ -762,7 +770,7 @@ const part004 = startSketchOn('YZ')
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/lots_of_planes.png", &result, 0.999); twenty_twenty::assert_image("tests/executor/outputs/lots_of_planes.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -779,7 +787,7 @@ async fn serial_test_holes() {
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/holes.png", &result, 0.999); twenty_twenty::assert_image("tests/executor/outputs/holes.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -798,7 +806,7 @@ async fn optional_params() {
const thing = other_circle([2, 2], 20) const thing = other_circle([2, 2], 20)
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/optional_params.png", &result, 0.999); twenty_twenty::assert_image("tests/executor/outputs/optional_params.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -834,7 +842,7 @@ const part = roundedRectangle([0, 0], 20, 20, 4)
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/rounded_with_holes.png", &result, 0.999); twenty_twenty::assert_image("tests/executor/outputs/rounded_with_holes.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -842,7 +850,7 @@ async fn serial_test_top_level_expression() {
let code = r#"startSketchOn('XY') |> circle([0,0], 22, %) |> extrude(14, %)"#; let code = r#"startSketchOn('XY') |> circle([0,0], 22, %) |> extrude(14, %)"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/top_level_expression.png", &result, 0.999); twenty_twenty::assert_image("tests/executor/outputs/top_level_expression.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -859,7 +867,7 @@ const part = startSketchOn('XY')
twenty_twenty::assert_image( twenty_twenty::assert_image(
"tests/executor/outputs/patterns_linear_basic_with_math.png", "tests/executor/outputs/patterns_linear_basic_with_math.png",
&result, &result,
0.999, MIN_DIFF,
); );
} }
@ -872,7 +880,7 @@ async fn serial_test_patterns_linear_basic() {
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/patterns_linear_basic.png", &result, 0.999); twenty_twenty::assert_image("tests/executor/outputs/patterns_linear_basic.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -888,7 +896,7 @@ async fn serial_test_patterns_linear_basic_3d() {
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/patterns_linear_basic_3d.png", &result, 0.999); twenty_twenty::assert_image("tests/executor/outputs/patterns_linear_basic_3d.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -903,7 +911,7 @@ async fn serial_test_patterns_linear_basic_negative_distance() {
twenty_twenty::assert_image( twenty_twenty::assert_image(
"tests/executor/outputs/patterns_linear_basic_negative_distance.png", "tests/executor/outputs/patterns_linear_basic_negative_distance.png",
&result, &result,
0.999, MIN_DIFF,
); );
} }
@ -919,7 +927,7 @@ async fn serial_test_patterns_linear_basic_negative_axis() {
twenty_twenty::assert_image( twenty_twenty::assert_image(
"tests/executor/outputs/patterns_linear_basic_negative_axis.png", "tests/executor/outputs/patterns_linear_basic_negative_axis.png",
&result, &result,
0.999, MIN_DIFF,
); );
} }
@ -941,7 +949,11 @@ const rectangle = startSketchOn('XY')
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/patterns_linear_basic_holes.png", &result, 0.999); twenty_twenty::assert_image(
"tests/executor/outputs/patterns_linear_basic_holes.png",
&result,
MIN_DIFF,
);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -953,7 +965,11 @@ async fn serial_test_patterns_circular_basic_2d() {
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/patterns_circular_basic_2d.png", &result, 0.999); twenty_twenty::assert_image(
"tests/executor/outputs/patterns_circular_basic_2d.png",
&result,
MIN_DIFF,
);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -969,7 +985,11 @@ async fn serial_test_patterns_circular_basic_3d() {
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/patterns_circular_basic_3d.png", &result, 0.999); twenty_twenty::assert_image(
"tests/executor/outputs/patterns_circular_basic_3d.png",
&result,
MIN_DIFF,
);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -988,7 +1008,7 @@ async fn serial_test_patterns_circular_3d_tilted_axis() {
twenty_twenty::assert_image( twenty_twenty::assert_image(
"tests/executor/outputs/patterns_circular_3d_tilted_axis.png", "tests/executor/outputs/patterns_circular_3d_tilted_axis.png",
&result, &result,
0.999, MIN_DIFF,
); );
} }
@ -1009,7 +1029,7 @@ async fn serial_test_import_obj_with_mtl() {
let code = r#"const model = import("tests/executor/inputs/cube.obj")"#; let code = r#"const model = import("tests/executor/inputs/cube.obj")"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/import_obj_with_mtl.png", &result, 0.999); twenty_twenty::assert_image("tests/executor/outputs/import_obj_with_mtl.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -1017,7 +1037,11 @@ async fn serial_test_import_obj_with_mtl_units() {
let code = r#"const model = import("tests/executor/inputs/cube.obj", {type: "obj", units: "m"})"#; let code = r#"const model = import("tests/executor/inputs/cube.obj", {type: "obj", units: "m"})"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/import_obj_with_mtl_units.png", &result, 0.999); twenty_twenty::assert_image(
"tests/executor/outputs/import_obj_with_mtl_units.png",
&result,
MIN_DIFF,
);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -1025,7 +1049,7 @@ async fn serial_test_import_gltf_with_bin() {
let code = r#"const model = import("tests/executor/inputs/cube.gltf")"#; let code = r#"const model = import("tests/executor/inputs/cube.gltf")"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/import_gltf_with_bin.png", &result, 0.999); twenty_twenty::assert_image("tests/executor/outputs/import_gltf_with_bin.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -1033,7 +1057,7 @@ async fn serial_test_import_gltf_embedded() {
let code = r#"const model = import("tests/executor/inputs/cube-embedded.gltf")"#; let code = r#"const model = import("tests/executor/inputs/cube-embedded.gltf")"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/import_gltf_embedded.png", &result, 0.999); twenty_twenty::assert_image("tests/executor/outputs/import_gltf_embedded.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -1041,7 +1065,7 @@ async fn serial_test_import_glb() {
let code = r#"const model = import("tests/executor/inputs/cube.glb")"#; let code = r#"const model = import("tests/executor/inputs/cube.glb")"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/import_glb.png", &result, 0.999); twenty_twenty::assert_image("tests/executor/outputs/import_glb.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -1049,7 +1073,7 @@ async fn serial_test_import_glb_no_assign() {
let code = r#"import("tests/executor/inputs/cube.glb")"#; let code = r#"import("tests/executor/inputs/cube.glb")"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/import_glb_no_assign.png", &result, 0.999); twenty_twenty::assert_image("tests/executor/outputs/import_glb_no_assign.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -1082,7 +1106,7 @@ const myCube = cube([0,0], 10)
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/cube_mm.png", &result, 1.0); twenty_twenty::assert_image("tests/executor/outputs/cube_mm.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -1103,7 +1127,7 @@ const myCube = cube([0,0], 10)
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Cm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Cm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/cube_cm.png", &result, 1.0); twenty_twenty::assert_image("tests/executor/outputs/cube_cm.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -1124,7 +1148,7 @@ const myCube = cube([0,0], 10)
"#; "#;
let result = execute_and_snapshot(code, UnitLength::M).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::M).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/cube_m.png", &result, 1.0); twenty_twenty::assert_image("tests/executor/outputs/cube_m.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -1145,7 +1169,7 @@ const myCube = cube([0,0], 10)
"#; "#;
let result = execute_and_snapshot(code, UnitLength::In).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::In).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/cube_in.png", &result, 1.0); twenty_twenty::assert_image("tests/executor/outputs/cube_in.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -1166,7 +1190,7 @@ const myCube = cube([0,0], 10)
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Ft).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Ft).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/cube_ft.png", &result, 1.0); twenty_twenty::assert_image("tests/executor/outputs/cube_ft.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -1187,7 +1211,7 @@ const myCube = cube([0,0], 10)
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Yd).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Yd).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/cube_yd.png", &result, 1.0); twenty_twenty::assert_image("tests/executor/outputs/cube_yd.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -1256,7 +1280,7 @@ const part003 = startSketchOn(part002, "end")
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face_of_face.png", &result, 1.0); twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face_of_face.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -1301,7 +1325,7 @@ const part002 = startSketchOn(part001, "end")
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face_circle.png", &result, 1.0); twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face_circle.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -1325,7 +1349,11 @@ const part002 = startSketchOn(part001, "end")
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face_circle_tagged.png", &result, 1.0); twenty_twenty::assert_image(
"tests/executor/outputs/sketch_on_face_circle_tagged.png",
&result,
MIN_DIFF,
);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -1391,7 +1419,7 @@ async fn serial_test_big_number_angle_to_match_length_x() {
twenty_twenty::assert_image( twenty_twenty::assert_image(
"tests/executor/outputs/big_number_angle_to_match_length_x.png", "tests/executor/outputs/big_number_angle_to_match_length_x.png",
&result, &result,
1.0, MIN_DIFF,
); );
} }
@ -1412,7 +1440,7 @@ async fn serial_test_big_number_angle_to_match_length_y() {
twenty_twenty::assert_image( twenty_twenty::assert_image(
"tests/executor/outputs/big_number_angle_to_match_length_y.png", "tests/executor/outputs/big_number_angle_to_match_length_y.png",
&result, &result,
1.0, MIN_DIFF,
); );
} }
@ -1433,7 +1461,7 @@ async fn serial_test_simple_revolve() {
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/simple_revolve.png", &result, 1.0); twenty_twenty::assert_image("tests/executor/outputs/simple_revolve.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -1453,7 +1481,7 @@ async fn serial_test_simple_revolve_uppercase() {
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/simple_revolve_uppercase.png", &result, 1.0); twenty_twenty::assert_image("tests/executor/outputs/simple_revolve_uppercase.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -1473,7 +1501,7 @@ async fn serial_test_simple_revolve_negative() {
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/simple_revolve_negative.png", &result, 1.0); twenty_twenty::assert_image("tests/executor/outputs/simple_revolve_negative.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -1543,7 +1571,11 @@ async fn serial_test_simple_revolve_custom_angle() {
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/simple_revolve_custom_angle.png", &result, 1.0); twenty_twenty::assert_image(
"tests/executor/outputs/simple_revolve_custom_angle.png",
&result,
MIN_DIFF,
);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -1563,7 +1595,11 @@ async fn serial_test_simple_revolve_custom_axis() {
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/simple_revolve_custom_axis.png", &result, 1.0); twenty_twenty::assert_image(
"tests/executor/outputs/simple_revolve_custom_axis.png",
&result,
MIN_DIFF,
);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -1587,7 +1623,7 @@ const sketch001 = startSketchOn(box, "end")
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/revolve_on_edge.png", &result, 1.0); twenty_twenty::assert_image("tests/executor/outputs/revolve_on_edge.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -1638,7 +1674,11 @@ const sketch001 = startSketchOn(box, "END")
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/revolve_on_face_circle_edge.png", &result, 1.0); twenty_twenty::assert_image(
"tests/executor/outputs/revolve_on_face_circle_edge.png",
&result,
MIN_DIFF,
);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -1660,7 +1700,7 @@ const sketch001 = startSketchOn(box, "END")
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/revolve_on_face_circle.png", &result, 1.0); twenty_twenty::assert_image("tests/executor/outputs/revolve_on_face_circle.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -1686,7 +1726,7 @@ const sketch001 = startSketchOn(box, "end")
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/revolve_on_face.png", &result, 1.0); twenty_twenty::assert_image("tests/executor/outputs/revolve_on_face.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -1700,7 +1740,7 @@ async fn serial_test_basic_revolve_circle() {
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/basic_revolve_circle.png", &result, 1.0); twenty_twenty::assert_image("tests/executor/outputs/basic_revolve_circle.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -1727,7 +1767,11 @@ const part002 = startSketchOn(part001, 'end')
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/simple_revolve_sketch_on_edge.png", &result, 1.0); twenty_twenty::assert_image(
"tests/executor/outputs/simple_revolve_sketch_on_edge.png",
&result,
MIN_DIFF,
);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -1788,7 +1832,7 @@ const plumbus1 = make_circle(p, b,$arc_b, [0, 0], 2.5)
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/plumbus_fillets.png", &result, 1.0); twenty_twenty::assert_image("tests/executor/outputs/plumbus_fillets.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -1826,7 +1870,11 @@ capScrew([0, 0.5, 0], 50, 37.5, 50, 25)
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/member_expression_in_params.png", &result, 1.0); twenty_twenty::assert_image(
"tests/executor/outputs/member_expression_in_params.png",
&result,
MIN_DIFF,
);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -1944,7 +1992,7 @@ async fn serial_test_xz_plane() {
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/xz_plane.png", &result, 1.0); twenty_twenty::assert_image("tests/executor/outputs/xz_plane.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -1958,7 +2006,7 @@ async fn serial_test_neg_xz_plane() {
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/neg_xz_plane.png", &result, 1.0); twenty_twenty::assert_image("tests/executor/outputs/neg_xz_plane.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -1985,7 +2033,11 @@ const pattn2 = patternLinear3d({
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/linear_pattern3d_a_pattern.png", &result, 1.0); twenty_twenty::assert_image(
"tests/executor/outputs/linear_pattern3d_a_pattern.png",
&result,
MIN_DIFF,
);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -2008,7 +2060,11 @@ const pattn2 = patternCircular3d({axis: [0,0, 1], center: [-20, -20, -20], repet
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/circular_pattern3d_a_pattern.png", &result, 1.0); twenty_twenty::assert_image(
"tests/executor/outputs/circular_pattern3d_a_pattern.png",
&result,
MIN_DIFF,
);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -2037,7 +2093,7 @@ extrude(10, sketch001)
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/array_of_sketches.png", &result, 1.0); twenty_twenty::assert_image("tests/executor/outputs/array_of_sketches.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -2103,7 +2159,7 @@ const sketch001 = startSketchOn(bracket, 'seg01')
twenty_twenty::assert_image( twenty_twenty::assert_image(
"tests/executor/outputs/sketch_on_face_after_fillets_referencing_face.png", "tests/executor/outputs/sketch_on_face_after_fillets_referencing_face.png",
&result, &result,
1.0, MIN_DIFF,
); );
} }
@ -2140,7 +2196,11 @@ const pattn1 = patternLinear3d({
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/pattern3d_array_of_extrudes.png", &result, 1.0); twenty_twenty::assert_image(
"tests/executor/outputs/pattern3d_array_of_extrudes.png",
&result,
MIN_DIFF,
);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -2191,7 +2251,7 @@ const baseExtrusion = extrude(width, sketch001)
twenty_twenty::assert_image( twenty_twenty::assert_image(
"tests/executor/outputs/fillets_referencing_other_fillets.png", "tests/executor/outputs/fillets_referencing_other_fillets.png",
&result, &result,
1.0, MIN_DIFF,
); );
} }
@ -2243,7 +2303,7 @@ const baseExtrusion = extrude(width, sketch001)
twenty_twenty::assert_image( twenty_twenty::assert_image(
"tests/executor/outputs/chamfers_referencing_other_chamfers.png", "tests/executor/outputs/chamfers_referencing_other_chamfers.png",
&result, &result,
1.0, MIN_DIFF,
); );
} }
@ -2302,7 +2362,7 @@ const pattn1 = patternLinear3d({
twenty_twenty::assert_image( twenty_twenty::assert_image(
"tests/executor/outputs/linear_pattern3d_filleted_sketch.png", "tests/executor/outputs/linear_pattern3d_filleted_sketch.png",
&result, &result,
1.0, MIN_DIFF,
); );
} }
@ -2333,7 +2393,7 @@ const pattn2 = patternCircular3d({axis: [0,0, 1], center: [-20, -20, -20], repet
twenty_twenty::assert_image( twenty_twenty::assert_image(
"tests/executor/outputs/circular_pattern3d_filleted_sketch.png", "tests/executor/outputs/circular_pattern3d_filleted_sketch.png",
&result, &result,
1.0, MIN_DIFF,
); );
} }
@ -2364,7 +2424,7 @@ const pattn2 = patternCircular3d({axis: [0,0, 1], center: [-20, -20, -20], repet
twenty_twenty::assert_image( twenty_twenty::assert_image(
"tests/executor/outputs/circular_pattern3d_chamfered_sketch.png", "tests/executor/outputs/circular_pattern3d_chamfered_sketch.png",
&result, &result,
1.0, MIN_DIFF,
); );
} }
@ -2429,7 +2489,11 @@ const sketch001 = startSketchOn(part001, 'chamfer1')
"#; "#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face_of_chamfer.png", &result, 1.0); twenty_twenty::assert_image(
"tests/executor/outputs/sketch_on_face_of_chamfer.png",
&result,
MIN_DIFF,
);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -2463,21 +2527,21 @@ let p = triangle(200)
async fn serial_test_global_tags() { async fn serial_test_global_tags() {
let code = include_str!("inputs/global-tags.kcl"); let code = include_str!("inputs/global-tags.kcl");
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/global_tags.png", &result, 0.999); twenty_twenty::assert_image("tests/executor/outputs/global_tags.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn serial_test_pattern_vase() { async fn serial_test_pattern_vase() {
let code = include_str!("inputs/pattern_vase.kcl"); let code = include_str!("inputs/pattern_vase.kcl");
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/pattern_vase.png", &result, 0.999); twenty_twenty::assert_image("tests/executor/outputs/pattern_vase.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn serial_test_scoped_tags() { async fn serial_test_scoped_tags() {
let code = include_str!("inputs/scoped-tags.kcl"); let code = include_str!("inputs/scoped-tags.kcl");
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/scoped_tags.png", &result, 0.999); twenty_twenty::assert_image("tests/executor/outputs/scoped_tags.png", &result, MIN_DIFF);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -2487,7 +2551,7 @@ async fn serial_test_order_sketch_extrude_in_order() {
twenty_twenty::assert_image( twenty_twenty::assert_image(
"tests/executor/outputs/order-sketch-extrude-in-order.png", "tests/executor/outputs/order-sketch-extrude-in-order.png",
&result, &result,
0.999, MIN_DIFF,
); );
} }
@ -2498,7 +2562,7 @@ async fn serial_test_order_sketch_extrude_out_of_order() {
twenty_twenty::assert_image( twenty_twenty::assert_image(
"tests/executor/outputs/order-sketch-extrude-out-of-order.png", "tests/executor/outputs/order-sketch-extrude-out-of-order.png",
&result, &result,
0.999, MIN_DIFF,
); );
} }
@ -2506,5 +2570,5 @@ async fn serial_test_order_sketch_extrude_out_of_order() {
async fn serial_test_extrude_custom_plane() { async fn serial_test_extrude_custom_plane() {
let code = include_str!("inputs/extrude-custom-plane.kcl"); let code = include_str!("inputs/extrude-custom-plane.kcl");
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/extrude-custom-plane.png", &result, 0.999); twenty_twenty::assert_image("tests/executor/outputs/extrude-custom-plane.png", &result, MIN_DIFF);
} }

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: 94 KiB

After

Width:  |  Height:  |  Size: 94 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 165 KiB