Compare commits

...

27 Commits

Author SHA1 Message Date
cba953c245 Cut release v0.24.3 (#3053) 2024-07-18 10:55:23 -07:00
54ca6ea0b2 artifact map clean up 1 (#3064)
remove old shit
2024-07-18 17:52:39 +10:00
max
6a01608c3a Unit Tests for hasValidFilletSelection (#3063)
* tests

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

* "err" instead of "instanceof Error"

* trigger CI

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
2024-07-18 16:33:49 +10:00
530f15e04a switch to js 2024-07-17 19:49:50 -04:00
725e59d987 use shell for now 2024-07-17 19:47:02 -04:00
54313c9b03 fix template again 2024-07-17 19:44:02 -04:00
890d96496c add a new cryptic_error template 2024-07-17 19:42:28 -04:00
35999366a7 Stl test for larger file (#3052)
* add shit

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

* add image

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

* images updated

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-07-17 15:55:59 -07:00
2affc7271d Bump max_frame_size (#3050)
We use the WebSocket connection to send binary data (in the form of
shapefiles) from the engine to the client. These can very easily get
larger than the default 16MB limit on the max_frame_size. I don't
understand why it won't stich multiple frames together - but given what
I can see when this crashes, the max_message_size isn't the LIMFAC,
max_frame_size is.

That's an issue for future-us.

Signed-off-by: Paul Tagliamonte <paul@zoo.dev>
2024-07-17 15:32:57 -04:00
d30fbf8b4b Bump tokio from 1.38.0 to 1.38.1 in /src/wasm-lib (#3043)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.38.0 to 1.38.1.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.38.0...tokio-1.38.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-17 09:06:39 -07:00
3f7e776464 Improve 'Release a new version' readme (#3048) 2024-07-17 10:23:21 -04:00
79cff57f43 show default planes bug (#3047) 2024-07-17 18:58:01 +10:00
1cd2cd82b2 Add a close button to sidebar panes (#3038)
* Add a close button to sidebar panes

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

* Rerun CI

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

* Fix up dark mode look and feel

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-17 01:17:53 -04:00
60e187bd3e Fix defined but never used warnings (#3045) 2024-07-17 04:26:14 +00:00
c64175425b Updater smoke-test instructions (#3044)
updater smoke test instructions
2024-07-17 14:00:05 +10:00
36464e6984 fillet ui follow up (#3035) 2024-07-17 03:58:48 +00:00
2f0002e53c Cut release v0.24.2 (#3042) 2024-07-17 13:55:17 +10:00
482833c88f Lf94/pause improvements (#3032)
* Add stream idle mode as a setting (default is off)

* Add pause icon
2024-07-17 12:45:11 +10:00
d9d0a72306 Remove unused function ProgramMemory::get_tags (#3033)
Seems to be unused since #2941.
2024-07-16 15:12:05 -04:00
65cd9fab64 Revert the snapshots. (#3039)
* Revert "Compute the AST digest in the LSP (#3037)"

This reverts commit 5e41e382ce.

* Compute the AST digest in the LSP (#3037)

This is a slow-roll to calling this in more places; but this is
non-critical, so if this breaks on some unexpected AST or what have you,
we're not breaking anything except the LSP (which we'll see pretty
quickly) while also testing it on all user input.

If something goes south, please feel free to revert this commit.

Signed-off-by: Paul Tagliamonte <paul@zoo.dev>

---------

Signed-off-by: Paul Tagliamonte <paul@zoo.dev>
2024-07-16 13:16:51 -04:00
5e41e382ce Compute the AST digest in the LSP (#3037)
This is a slow-roll to calling this in more places; but this is
non-critical, so if this breaks on some unexpected AST or what have you,
we're not breaking anything except the LSP (which we'll see pretty
quickly) while also testing it on all user input.

If something goes south, please feel free to revert this commit.

Signed-off-by: Paul Tagliamonte <paul@zoo.dev>
2024-07-16 12:13:17 -04:00
1e3cb00092 Move Args into its own module (#3027) 2024-07-16 07:45:43 -05:00
d1a2bd01ca Lower threshold for 2020 tests (#3030)
* Lower threshold for 2020 tests

Now that the tests zoom into the model and center it before taking a
snapshot, they should be less sensitive.

* Genuine, nontrivial changes to the integration test images
2024-07-15 17:41:41 -05:00
aca13d087b add in benchmarks for digest code (#3031)
(initial results look good!)
2024-07-15 13:45:55 -04:00
fcdde3e482 Bump syn from 2.0.70 to 2.0.71 in /src/wasm-lib (#3029)
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.70 to 2.0.71.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.70...2.0.71)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-15 09:26:18 -07:00
a1df3d0ffc Fillet UI (#2718)
* draft: fillet ast mod + test

* Kurt's rejig

* playwright

* update button enable logic

* remove fillet button in production build

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

* trigger CI

* fix typo

* give a way to turn on fillets

---------

Co-authored-by: max-mrgrsk <margorskyi@gmail.com>
Co-authored-by: max-mrgrsk <156543465+max-mrgrsk@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-15 19:20:32 +10:00
1852e6167b Fix argument persistence and accidental submission command bar bugs (#3021) 2024-07-14 18:15:34 -04:00
294 changed files with 162900 additions and 1390 deletions

View File

@ -0,0 +1,37 @@
name: Cryptic KCL Error
description: File a bug report for source code that produces a confusing error
title: "[CRYPTIC]: "
labels: ["cryptic-error"]
assignees: []
body:
- type: markdown
attributes:
value: "Thank you for taking the time to report a confusing error. Please provide as much information as possible to help us resolve it."
- type: textarea
id: kcl
attributes:
label: Paste minimal KCL source that produces a cryptic error
description: Minimal KCL reproducer that produces a cryptic error
placeholder: "const ..."
render: javascript
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected Behavior
description: Description of what you expected to happen (if you know).
placeholder: "I expected that..."
validations:
required: false
- type: textarea
id: additional-context
attributes:
label: Additional Context
description: Add any other context about the problem here.
placeholder: "Anything else you want to add..."
validations:
required: false

View File

@ -124,20 +124,40 @@ Before you submit a contribution PR to this repo, please ensure that:
## Release a new version
1. Bump the versions by running `./make-realease.sh` while on a fresh pull of main
#### 1. Bump the versions by running `./make-release.sh` and create a Cut Release PR
That will create the branch with the updated json files for you.
run `./make-release.sh` for a patch update
run `./make-release.sh "minor"` for minor
run `./make-release.sh "major"` for major
That will create the branch with the updated json files for you:
- run `./make-release.sh` or `./make-release.sh patch` for a patch update;
- run `./make-release.sh minor` for minor; or
- run `./make-release.sh major` for major.
After it runs you should just need to push the push the branch and open a PR (it will suggest a changelog for you too, delete any that are not user facing)
After it runs you should just need the push the branch and open a PR.
The PR may serve as a place to discuss the human-readable changelog and extra QA.
**Important:** It needs to be prefixed with `Cut release v` to build in release mode and a few other things to test in the best context possible, the intent would be for instance to have `Cut release v1.2.3` for the `v1.2.3` release candidate.
2. Merge the PR
The PR may then serve as a place to discuss the human-readable changelog and extra QA. The `make-release.sh` tool suggests a changelog for you too to be used as PR description, just make sure to delete lines that are not user facing.
#### 2. Smoke test artifacts from the Cut Release PR
The release builds can be find under the `artifact` zip, at the very bottom of the `ci` action page for each commit on this branch.
We don't have a strict process, but click around and check for anything obvious, posting results as comments in the Cut Release PR.
The other `ci` output in Cut Release PRs is `updater-test`, because we don't have a way to test this fully automated, we have a semi-automated process. Download updater-test zip file, install the app, run it, expect an updater prompt to a dummy v0.99.99, install it and check that the app comes back at that version (on both macOS and Windows).
#### 3. Merge the Cut Release PR
This will kick the `create-release` action, that creates a _Draft_ release out of this Cut Release PR merge after less than a minute, with the new version as title and Cut Release PR as description.
#### 4. Publish the release
Head over to https://github.com/KittyCAD/modeling-app/releases, the draft release corresponding to the merged Cut Release PR should show up at the top as _Draft_. Click on it, verify the content, and hit _Publish_.
#### 5. Profit
A new Action kicks in at https://github.com/KittyCAD/modeling-app/actions, which can be found under `release` event filter.
3. Profit (A new Action kicks in at https://github.com/KittyCAD/modeling-app/actions if the PR was correctly named)
## Fuzzing the parser

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -3099,6 +3099,49 @@ const sketch002 = startSketchOn(extrude001, $seg01)
).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 index = inputString.indexOf('(')
if (index !== -1) {
@ -3500,11 +3543,62 @@ test.describe('Command bar tests', () => {
`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)
await page.setViewportSize({ width: 1200, height: 500 })
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(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
@ -3515,23 +3609,17 @@ test.describe('Command bar tests', () => {
.or(page.getByRole('button', { name: '⌘K' }))
.click()
let cmdSearchBar = page.getByPlaceholder('Search commands')
await expect(cmdSearchBar).toBeVisible()
await page.keyboard.press('Escape')
cmdSearchBar = page.getByPlaceholder('Search commands')
await expect(cmdSearchBar).not.toBeVisible()
// Now try the same, but with the keyboard shortcut, check focus
await page.keyboard.press('Meta+K')
cmdSearchBar = page.getByPlaceholder('Search commands')
await expect(cmdSearchBar).toBeVisible()
await expect(cmdSearchBar).toBeFocused()
// Try typing in the command bar
await page.keyboard.type('theme')
const themeOption = page.getByRole('option', {
name: 'Settings · app · theme',
})
await cmdSearchBar.fill('theme')
await expect(themeOption).toBeVisible()
await themeOption.click()
const themeInput = page.getByPlaceholder('Select an option')
@ -3553,6 +3641,24 @@ test.describe('Command bar tests', () => {
).toBeVisible()
// Check that the theme changed
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 ({
@ -3577,7 +3683,7 @@ test.describe('Command bar tests', () => {
await expect(cmdSearchBar).toBeFocused()
// Try typing in the command bar
await page.keyboard.type('theme')
await cmdSearchBar.fill('theme')
const themeOption = page.getByRole('option', {
name: 'Settings · app · theme',
})
@ -3648,7 +3754,9 @@ test.describe('Command bar tests', () => {
await page.mouse.click(700, 200)
// 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,
// since the default variable name is already in use (distance)
@ -3663,11 +3771,12 @@ test.describe('Command bar tests', () => {
// Review step and argument hotkeys
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
await expect(
page.getByRole('button', { name: 'Distance 5', exact: false })
page.getByRole('button', { name: 'distance', exact: false })
).toBeDisabled()
await continueButton.click()
@ -3724,6 +3833,7 @@ const extrude001 = extrude(distance001, sketch001)`.replace(
// 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
@ -4682,10 +4792,10 @@ test.describe('Sketch tests', () => {
// click extrude
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.
await expect(
page.getByRole('button', { name: 'Selection 1 face' })
page.getByRole('button', { name: 'selection : 1 face', exact: false })
).toBeVisible()
})
test("Existing sketch with bad code delete user's code", async ({ page }) => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 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: 51 KiB

After

Width:  |  Height:  |  Size: 51 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: 36 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 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",
"version": "0.24.1",
"version": "0.24.3",
"private": true,
"dependencies": {
"@codemirror/autocomplete": "^6.17.0",

View File

@ -30,7 +30,7 @@ import { URI } from 'vscode-uri'
import { LanguageServerClient } from '../client'
import { CompletionItemKindMap } from './autocomplete'
import { addToken, SemanticToken } from './semantic-tokens'
import { deferExecution, posToOffset, formatMarkdownContents } from './util'
import { posToOffset, formatMarkdownContents } from './util'
import lspAutocompleteExt from './autocomplete'
import lspHoverExt from './hover'
import lspFormatExt from './format'

View File

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

View File

@ -44,7 +44,7 @@ export function App() {
}, [projectName, projectPath])
useHotKeyListener()
const { context } = useModelingContext()
const { context, state } = useModelingContext()
const { auth, settings } = useSettingsAuthContext()
const token = auth?.context?.token
@ -57,7 +57,6 @@ export function App() {
const {
app: { onboardingStatus },
} = settings.context
const { state } = useModelingContext()
useHotkeys('backspace', (e) => {
e.preventDefault()

View File

@ -16,6 +16,7 @@ import {
canRectangleTool,
isEditingExistingSketch,
} from 'machines/modelingMachine'
import { DEV } from 'env'
export function Toolbar({
className = '',
@ -118,6 +119,16 @@ export function Toolbar({
}),
{ 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>) {
const span = toolbarButtonsRef.current
@ -404,6 +415,36 @@ export function Toolbar({
</ActionButton>
</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>
</menu>
)

View File

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

View File

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

View File

@ -3,6 +3,7 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
import { useKclContext } from 'lang/KclProvider'
import { CommandArgument } from 'lib/commandTypes'
import {
Selection,
canSubmitSelectionArg,
getSelectionType,
getSelectionTypeDisplayText,
@ -11,6 +12,25 @@ import { modelingMachine } from 'machines/modelingMachine'
import { useCallback, useEffect, useRef, useState } from 'react'
import { StateFrom } from 'xstate'
const semanticEntityNames: { [key: string]: Array<Selection['type']> } = {
face: ['extrude-wall', 'start-cap', 'end-cap'],
edge: ['edge', 'line', 'arc'],
point: ['point', 'line-end', 'line-mid'],
}
function getSemanticSelectionType(selectionType: Array<Selection['type']>) {
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>) =>
snapshot.context.selectionRanges
@ -85,7 +105,9 @@ function CommandBarSelectionInput({
>
{canSubmitSelection
? 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
id="selection"
name="selection"

View File

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

View File

@ -72,6 +72,7 @@ import { uuidv4 } from 'lib/utils'
import { err, trap } from 'lib/trap'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { modelingMachineEvent } from 'editor/manager'
import { hasValidFilletSelection } from 'lang/modifyAst/addFillet'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
@ -130,6 +131,9 @@ export const ModelingMachineProvider = ({
},
'sketch exit execute': ({ store }) => {
;(async () => {
// blocks entering a sketch until after exit sketch code has run
kclManager.isExecuting = true
await sceneInfra.camControls.snapToPerspectiveBeforeHandingBackControlToEngine()
sceneInfra.camControls.syncDirection = 'engineToClient'
@ -164,7 +168,7 @@ export const ModelingMachineProvider = ({
store.videoElement?.pause()
kclManager.executeCode(true).then(() => {
if (engineCommandManager.engineConnection?.freezeFrame) return
if (engineCommandManager.engineConnection?.idleMode) return
store.videoElement?.play()
})
@ -444,6 +448,12 @@ export const ModelingMachineProvider = ({
if (selectionRanges.codeBasedSelections.length <= 0) return false
return true
},
'has valid fillet selection': ({ selectionRanges }) =>
hasValidFilletSelection({
selectionRanges,
ast: kclManager.ast,
code: codeManager.code,
}),
'Selection is on face': ({ selectionRanges }, { data }) => {
if (data?.forceNewSketch) return false
if (!isSingleCursorInPipe(selectionRanges, kclManager.ast))
@ -494,7 +504,6 @@ export const ModelingMachineProvider = ({
kclManager.ast,
data.sketchPathToNode,
data.extrudePathToNode,
kclManager.programMemory,
data.cap
)
if (trap(sketched)) return Promise.reject(sketched)

View File

@ -1,6 +1,8 @@
import styles from './ModelingPane.module.css'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useModelingContext } from 'hooks/useModelingContext'
import { ActionButton } from 'components/ActionButton'
import Tooltip from 'components/Tooltip'
export interface ModelingPaneProps
extends React.PropsWithChildren,
@ -8,16 +10,32 @@ export interface ModelingPaneProps
title: string
Menu?: React.ReactNode | React.FC
detailsTestId?: string
onClose: () => void
}
export const ModelingPaneHeader = ({
title,
Menu,
}: Pick<ModelingPaneProps, 'title' | 'Menu'>) => {
onClose,
}: Pick<ModelingPaneProps, 'title' | 'Menu' | 'onClose'>) => {
return (
<div className={styles.header}>
<div className="flex gap-2 items-center flex-1">{title}</div>
{Menu instanceof Function ? <Menu /> : Menu}
<ActionButton
Element="button"
iconStart={{
icon: 'close',
iconClassName: '!text-current',
bgClassName: 'bg-transparent dark:bg-transparent',
}}
className="!p-0 !bg-transparent hover:text-primary border-transparent dark:!border-transparent hover:!border-primary dark:hover:!border-chalkboard-70 !outline-none"
onClick={onClose}
>
<Tooltip position="bottom-right" delay={750}>
Close
</Tooltip>
</ActionButton>
</div>
)
}
@ -29,6 +47,7 @@ export const ModelingPane = ({
className,
Menu,
detailsTestId,
onClose,
...props
}: ModelingPaneProps) => {
const { settings } = useSettingsAuthContext()
@ -51,7 +70,7 @@ export const ModelingPane = ({
(className || '')
}
>
<ModelingPaneHeader title={title} Menu={Menu} />
<ModelingPaneHeader title={title} Menu={Menu} onClose={onClose} />
<div className="relative w-full">{children}</div>
</section>
)

View File

@ -24,14 +24,12 @@ export const KclEditorMenu = ({ children }: PropsWithChildren) => {
}
}}
>
<Menu.Button className="p-0 border-none relative">
<Menu.Button className="!p-0 !bg-transparent hover:text-primary border-transparent dark:!border-transparent hover:!border-primary dark:hover:!border-chalkboard-70 ui-open:!border-primary dark:ui-open:!border-chalkboard-70 !outline-none">
<ActionIcon
icon="three-dots"
className="p-1"
size="sm"
bgClassName={
'!bg-transparent hover:!bg-primary/10 hover:dark:!bg-chalkboard-100 ui-open:!bg-primary/10 dark:ui-open:!bg-chalkboard-100 rounded-sm'
}
bgClassName="bg-transparent dark:bg-transparent"
iconClassName={'!text-chalkboard-90 dark:!text-chalkboard-40'}
/>
</Menu.Button>

View File

@ -204,6 +204,7 @@ function ModelingSidebarSection({
id={`${pane.id}-pane`}
title={pane.title}
Menu={pane.Menu}
onClose={() => togglePane(pane.id)}
>
{pane.Content instanceof Function ? (
<pane.Content />

View File

@ -1,4 +1,3 @@
import { DEV } from 'env'
import { MouseEventHandler, useEffect, useRef, useState } from 'react'
import { getNormalisedCoordinates } from '../lib/utils'
import Loading from './Loading'
@ -11,6 +10,10 @@ import { btnName } from 'lib/cameraControls'
import { sendSelectEventToEngine } from 'lib/selections'
import { kclManager, engineCommandManager, sceneInfra } from 'lib/singletons'
import { useAppStream } from 'AppState'
import {
EngineConnectionStateType,
DisconnectingType,
} from 'lang/std/engineConnection'
export const Stream = () => {
const [isLoading, setIsLoading] = useState(true)
@ -20,15 +23,28 @@ export const Stream = () => {
const { settings } = useSettingsAuthContext()
const { state, send, context } = useModelingContext()
const { mediaStream } = useAppStream()
const { overallState } = useNetworkContext()
const { overallState, immediateState } = useNetworkContext()
const [isFreezeFrame, setIsFreezeFrame] = useState(false)
const [isPaused, setIsPaused] = useState(false)
const IDLE = true
const IDLE = settings.context.app.streamIdleMode.current
const isNetworkOkay =
overallState === NetworkHealthState.Ok ||
overallState === NetworkHealthState.Weak
useEffect(() => {
if (
immediateState.type === EngineConnectionStateType.Disconnecting &&
immediateState.value.type === DisconnectingType.Pause
) {
setIsPaused(true)
}
if (immediateState.type === EngineConnectionStateType.Connecting) {
setIsPaused(false)
}
}, [immediateState])
// Linux has a default behavior to paste text on middle mouse up
// This adds a listener to block that pasting if the click target
// is not a text input, so users can move in the 3D scene with
@ -65,25 +81,28 @@ export const Stream = () => {
sceneInfra.modelingSend({ type: 'Cancel' })
// Give video time to pause
window.requestAnimationFrame(() => {
engineCommandManager.tearDown()
engineCommandManager.tearDown({ idleMode: true })
})
}
// Teardown everything if we go hidden or reconnect
if (IDLE && DEV) {
if (globalThis?.window?.document) {
globalThis.window.document.onvisibilitychange = () => {
if (globalThis.window.document.visibilityState === 'hidden') {
clearTimeout(timeoutIdIdleA)
timeoutIdIdleA = setTimeout(teardown, IDLE_TIME_MS)
} else if (!engineCommandManager.engineConnection?.isReady()) {
clearTimeout(timeoutIdIdleA)
engineCommandManager.engineConnection?.connect(true)
}
}
const onVisibilityChange = () => {
if (globalThis.window.document.visibilityState === 'hidden') {
clearTimeout(timeoutIdIdleA)
timeoutIdIdleA = setTimeout(teardown, IDLE_TIME_MS)
} else if (!engineCommandManager.engineConnection?.isReady()) {
clearTimeout(timeoutIdIdleA)
engineCommandManager.engineConnection?.connect(true)
}
}
// Teardown everything if we go hidden or reconnect
if (IDLE) {
globalThis?.window?.document?.addEventListener(
'visibilitychange',
onVisibilityChange
)
}
let timeoutIdIdleB: ReturnType<typeof setTimeout> | undefined = undefined
const onAnyInput = () => {
@ -93,7 +112,7 @@ export const Stream = () => {
timeoutIdIdleB = setTimeout(teardown, IDLE_TIME_MS)
}
if (IDLE && DEV) {
if (IDLE) {
globalThis?.window?.document?.addEventListener('keydown', onAnyInput)
globalThis?.window?.document?.addEventListener('mousemove', onAnyInput)
globalThis?.window?.document?.addEventListener('mousedown', onAnyInput)
@ -101,7 +120,7 @@ export const Stream = () => {
globalThis?.window?.document?.addEventListener('touchstart', onAnyInput)
}
if (IDLE && DEV) {
if (IDLE) {
timeoutIdIdleB = setTimeout(teardown, IDLE_TIME_MS)
}
@ -109,7 +128,14 @@ export const Stream = () => {
globalThis?.window?.document?.removeEventListener('paste', handlePaste, {
capture: true,
})
if (IDLE && DEV) {
if (IDLE) {
clearTimeout(timeoutIdIdleA)
clearTimeout(timeoutIdIdleB)
globalThis?.window?.document?.removeEventListener(
'visibilitychange',
onVisibilityChange
)
globalThis?.window?.document?.removeEventListener('keydown', onAnyInput)
globalThis?.window?.document?.removeEventListener(
'mousemove',
@ -126,7 +152,7 @@ export const Stream = () => {
)
}
}
}, [])
}, [IDLE])
useEffect(() => {
setIsFirstRender(kclManager.isFirstRender)
@ -249,6 +275,32 @@ export const Stream = () => {
<ClientSideScene
cameraControls={settings.context.modeling.mouseControls.current}
/>
{isPaused && (
<div className="text-center absolute inset-0">
<div
className="flex flex-col items-center justify-center h-screen"
data-testid="paused"
>
<div className="border-primary border p-2 rounded-sm">
<svg
width="8"
height="12"
viewBox="0 0 8 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2 12V0H0V12H2ZM8 12V0H6V12H8Z"
fill="var(--primary)"
/>
</svg>
</div>
<p className="text-base mt-2 text-primary bold">Paused</p>
</div>
</div>
)}
{(!isNetworkOkay || isLoading || isFirstRender) && !isFreezeFrame && (
<div className="text-center absolute inset-0">
<Loading>

View File

@ -1,11 +1,16 @@
import { createContext, useContext } from 'react'
import {
ConnectingTypeGroup,
EngineConnectionStateType,
EngineConnectionState,
initialConnectingTypeGroupState,
} from '../lang/std/engineConnection'
import { NetworkStatus, NetworkHealthState } from './useNetworkStatus'
export const NetworkContext = createContext<NetworkStatus>({
immediateState: {
type: EngineConnectionStateType.Disconnected,
} as EngineConnectionState,
hasIssues: undefined,
overallState: NetworkHealthState.Disconnected,
internetConnected: true,

View File

@ -6,6 +6,7 @@ import {
EngineCommandManagerEvents,
EngineConnectionEvents,
EngineConnectionStateType,
EngineConnectionState,
ErrorType,
initialConnectingTypeGroupState,
} from '../lang/std/engineConnection'
@ -19,6 +20,7 @@ export enum NetworkHealthState {
}
export interface NetworkStatus {
immediateState: EngineConnectionState
hasIssues: boolean | undefined
overallState: NetworkHealthState
internetConnected: boolean
@ -33,6 +35,9 @@ export interface NetworkStatus {
// Must be called from one place in the application.
// We've chosen the <Router /> component for this.
export function useNetworkStatus() {
const [immediateState, setImmediateState] = useState<EngineConnectionState>({
type: EngineConnectionStateType.Disconnected,
})
const [steps, setSteps] = useState(
structuredClone(initialConnectingTypeGroupState)
)
@ -126,6 +131,7 @@ export function useNetworkStatus() {
const onConnectionStateChange = ({
detail: engineConnectionState,
}: CustomEvent) => {
setImmediateState(engineConnectionState)
setSteps((steps) => {
let nextSteps = structuredClone(steps)
@ -215,6 +221,7 @@ export function useNetworkStatus() {
}, [])
return {
immediateState,
hasIssues,
overallState,
internetConnected,

View File

@ -1,4 +1,4 @@
import { useLayoutEffect, useEffect, useRef, useState } from 'react'
import { useLayoutEffect, useEffect, useRef } from 'react'
import { engineCommandManager, kclManager } from 'lib/singletons'
import { deferExecution } from 'lib/utils'
import { Themes } from 'lib/theme'

View File

@ -346,6 +346,7 @@ export class KclManager {
return
}
this.ast = { ...ast }
this.isExecuting = true // executeAst sets this to false again
return this.executeAst(ast, zoomToFit)
}
format() {

View File

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

View File

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

View File

@ -0,0 +1,399 @@
import {
parse,
recast,
initPromise,
PathToNode,
Value,
Program,
CallExpression,
} from '../wasm'
import {
addFillet,
hasValidFilletSelection,
isTagUsedInFillet,
} from './addFillet'
import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst'
import { createLiteral } from 'lang/modifyAst'
import { err } from 'lib/trap'
import { Selections } from 'lib/selections'
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 (err(astOrError)) {
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 (err(pathToExtrudeNode)) {
return new Error('Path to extrude node not found')
}
// const radius = createLiteral(5) as Value
const result = addFillet(ast, pathToSegmentNode, pathToExtrudeNode, radius)
if (err(result)) {
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([])
})
})
describe('Testing button states', () => {
const runButtonStateTest = async (
code: string,
segmentSnippet: string,
expectedState: boolean
) => {
// ast
const astOrError = parse(code)
if (err(astOrError)) {
return new Error('AST not found')
}
const ast = astOrError as Program
// selectionRanges
const range: [number, number] = segmentSnippet
? [
code.indexOf(segmentSnippet),
code.indexOf(segmentSnippet) + segmentSnippet.length,
]
: [ast.end, ast.end] // empty line in the end of the code
const selectionRanges: Selections = {
codeBasedSelections: [
{
range,
type: 'default',
},
],
otherSelections: [],
}
// state
const buttonState = hasValidFilletSelection({
ast,
selectionRanges,
code,
})
expect(buttonState).toEqual(expectedState)
}
const codeWithBody: string = `
const sketch001 = startSketchOn('XY')
|> startProfileAt([-20, -5], %)
|> line([0, 10], %)
|> line([10, 0], %)
|> line([0, -10], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(-10, sketch001)
`
const codeWithoutBodies: string = `
const sketch001 = startSketchOn('XY')
|> startProfileAt([-20, -5], %)
|> line([0, 10], %)
|> line([10, 0], %)
|> line([0, -10], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
`
// body is missing
it('should return false when body is missing and nothing is selected', async () => {
await runButtonStateTest(codeWithoutBodies, '', false)
})
it('should return false when body is missing and segment is selected', async () => {
await runButtonStateTest(codeWithoutBodies, `line([10, 0], %)`, false)
})
// body exists
it('should return true when body exists and nothing is selected', async () => {
await runButtonStateTest(codeWithBody, '', true)
})
it('should return true when body exists and segment is selected', async () => {
await runButtonStateTest(codeWithBody, `line([10, 0], %)`, true)
})
it('hould return false when body exists and not a segment is selected', async () => {
await runButtonStateTest(codeWithBody, `close(%)`, false)
})
})

View File

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

@ -143,6 +143,7 @@ export enum DisconnectingType {
Error = 'error',
Timeout = 'timeout',
Quit = 'quit',
Pause = 'pause',
}
// Sorted by severity
@ -200,6 +201,7 @@ export type DisconnectingValue =
| State<DisconnectingType.Error, ErrorType>
| State<DisconnectingType.Timeout, void>
| State<DisconnectingType.Quit, void>
| State<DisconnectingType.Pause, void>
// These are ordered by the expected sequence.
export enum ConnectingType {
@ -300,7 +302,7 @@ class EngineConnection extends EventTarget {
pc?: RTCPeerConnection
unreliableDataChannel?: RTCDataChannel
mediaStream?: MediaStream
freezeFrame: boolean = false
idleMode: boolean = false
onIceCandidate = function (
this: RTCPeerConnection,
@ -391,10 +393,10 @@ class EngineConnection extends EventTarget {
this.pingPongSpan = { ping: undefined, pong: undefined }
// Without an interval ping, our connection will timeout.
// If this.freezeFrame is true we skip this logic so only reconnect
// If this.idleMode is true we skip this logic so only reconnect
// happens on mouse move
this.pingIntervalId = setInterval(() => {
if (this.freezeFrame) return
if (this.idleMode) return
switch (this.state.type as EngineConnectionStateType) {
case EngineConnectionStateType.ConnectionEstablished:
@ -456,8 +458,8 @@ class EngineConnection extends EventTarget {
return this.state.type === EngineConnectionStateType.ConnectionEstablished
}
tearDown(opts?: { freeze: boolean }) {
this.freezeFrame = opts?.freeze ?? false
tearDown(opts?: { idleMode: boolean }) {
this.idleMode = opts?.idleMode ?? false
this.disconnectAll()
clearInterval(this.pingIntervalId)
@ -497,10 +499,19 @@ class EngineConnection extends EventTarget {
this.onNetworkStatusReady
)
this.state = {
type: EngineConnectionStateType.Disconnecting,
value: { type: DisconnectingType.Quit },
}
this.state = opts?.idleMode
? {
type: EngineConnectionStateType.Disconnecting,
value: {
type: DisconnectingType.Pause,
},
}
: {
type: EngineConnectionStateType.Disconnecting,
value: {
type: DisconnectingType.Quit,
},
}
}
/**
@ -868,8 +879,7 @@ class EngineConnection extends EventTarget {
.join('\n')
if (message.request_id) {
const artifactThatFailed =
this.engineCommandManager.artifactMap[message.request_id] ||
this.engineCommandManager.lastArtifactMap[message.request_id]
this.engineCommandManager.artifactMap[message.request_id]
console.error(
`Error in response to request ${message.request_id}:\n${errorsString}
failed cmd type was ${artifactThatFailed?.commandType}`
@ -1099,8 +1109,6 @@ class EngineConnection extends EventTarget {
this.unreliableDataChannel?.readyState === 'closed'
if (allClosed) {
// Do not notify the rest of the program that we have cut off anything.
if (this.freezeFrame) return
this.state = { type: EngineConnectionStateType.Disconnected }
}
}
@ -1174,13 +1182,6 @@ export class EngineCommandManager extends EventTarget {
* of the KCL code that generated it.
*/
artifactMap: ArtifactMap = {}
/**
* The {@link ArtifactMap} from the previous engine connection. This is used as a fallback
* when the engine connection is reset without a full client-side refresh.
*
* @deprecated This was used during a short time when we were choosing to not execute the engine in certain cases.
*/
lastArtifactMap: ArtifactMap = {}
/**
* The client-side representation of the scene command artifacts that have been sent to the server;
* that is, the *non-modeling* commands and corresponding artifacts.
@ -1584,10 +1585,7 @@ export class EngineCommandManager extends EventTarget {
type: 'receive-reliable',
data: message,
id,
cmd_type:
command?.commandType ||
this.lastArtifactMap[id]?.commandType ||
sceneCommand?.commandType,
cmd_type: command?.commandType || sceneCommand?.commandType,
})
Object.values(this.subscriptions[modelingResponse.type] || {}).forEach(
(callback) => callback(modelingResponse)
@ -1738,7 +1736,7 @@ export class EngineCommandManager extends EventTarget {
}
}
}
tearDown() {
tearDown(opts?: { idleMode: boolean }) {
if (this.engineConnection) {
this.engineConnection.removeEventListener(
EngineConnectionEvents.Opened,
@ -1757,7 +1755,7 @@ export class EngineCommandManager extends EventTarget {
this.onEngineConnectionNewTrack as EventListener
)
this.engineConnection?.tearDown()
this.engineConnection?.tearDown(opts)
this.engineConnection = undefined
// Our window.tearDown assignment causes this case to happen which is
@ -1765,11 +1763,10 @@ export class EngineCommandManager extends EventTarget {
// @ts-ignore
} else if (this.engineCommandManager?.engineConnection) {
// @ts-ignore
this.engineCommandManager?.engineConnection?.tearDown()
this.engineCommandManager?.engineConnection?.tearDown(opts)
}
}
async startNewSession() {
this.lastArtifactMap = this.artifactMap
this.artifactMap = {}
await this.initPlanes()
}

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