Compare commits
27 Commits
achalmers/
...
v0.24.3
Author | SHA1 | Date | |
---|---|---|---|
cba953c245 | |||
54ca6ea0b2 | |||
6a01608c3a | |||
530f15e04a | |||
725e59d987 | |||
54313c9b03 | |||
890d96496c | |||
35999366a7 | |||
2affc7271d | |||
d30fbf8b4b | |||
3f7e776464 | |||
79cff57f43 | |||
1cd2cd82b2 | |||
60e187bd3e | |||
c64175425b | |||
36464e6984 | |||
2f0002e53c | |||
482833c88f | |||
d9d0a72306 | |||
65cd9fab64 | |||
5e41e382ce | |||
1e3cb00092 | |||
d1a2bd01ca | |||
aca13d087b | |||
fcdde3e482 | |||
a1df3d0ffc | |||
1852e6167b |
37
.github/ISSUE_TEMPLATE/cryptic_error.yml
vendored
Normal 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
|
38
README.md
@ -124,20 +124,40 @@ Before you submit a contribution PR to this repo, please ensure that:
|
|||||||
|
|
||||||
## Release a new version
|
## 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.
|
That will create the branch with the updated json files for you:
|
||||||
run `./make-release.sh` for a patch update
|
- run `./make-release.sh` or `./make-release.sh patch` for a patch update;
|
||||||
run `./make-release.sh "minor"` for minor
|
- run `./make-release.sh minor` for minor; or
|
||||||
run `./make-release.sh "major"` for major
|
- 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
|
## Fuzzing the parser
|
||||||
|
|
||||||
|
@ -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()
|
||||||
@ -3724,6 +3833,7 @@ const extrude001 = extrude(distance001, sketch001)`.replace(
|
|||||||
// Click in the scene a couple times to draw a line
|
// Click in the scene a couple times to draw a line
|
||||||
// so tangential arc is valid
|
// so tangential arc is valid
|
||||||
await page.mouse.click(700, 200)
|
await page.mouse.click(700, 200)
|
||||||
|
await page.mouse.move(700, 300, { steps: 5 })
|
||||||
await page.mouse.click(700, 300)
|
await page.mouse.click(700, 300)
|
||||||
|
|
||||||
// switch to tangential arc via command bar
|
// switch to tangential arc via command bar
|
||||||
@ -4682,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 }) => {
|
||||||
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 72 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "untitled-app",
|
"name": "untitled-app",
|
||||||
"version": "0.24.1",
|
"version": "0.24.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/autocomplete": "^6.17.0",
|
"@codemirror/autocomplete": "^6.17.0",
|
||||||
|
@ -30,7 +30,7 @@ import { URI } from 'vscode-uri'
|
|||||||
import { LanguageServerClient } from '../client'
|
import { LanguageServerClient } from '../client'
|
||||||
import { CompletionItemKindMap } from './autocomplete'
|
import { CompletionItemKindMap } from './autocomplete'
|
||||||
import { addToken, SemanticToken } from './semantic-tokens'
|
import { addToken, SemanticToken } from './semantic-tokens'
|
||||||
import { deferExecution, posToOffset, formatMarkdownContents } from './util'
|
import { posToOffset, formatMarkdownContents } from './util'
|
||||||
import lspAutocompleteExt from './autocomplete'
|
import lspAutocompleteExt from './autocomplete'
|
||||||
import lspHoverExt from './hover'
|
import lspHoverExt from './hover'
|
||||||
import lspFormatExt from './format'
|
import lspFormatExt from './format'
|
||||||
|
@ -80,5 +80,5 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"productName": "Zoo Modeling App",
|
"productName": "Zoo Modeling App",
|
||||||
"version": "0.24.1"
|
"version": "0.24.3"
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,7 @@ export function App() {
|
|||||||
}, [projectName, projectPath])
|
}, [projectName, projectPath])
|
||||||
|
|
||||||
useHotKeyListener()
|
useHotKeyListener()
|
||||||
const { context } = useModelingContext()
|
const { context, state } = useModelingContext()
|
||||||
|
|
||||||
const { auth, settings } = useSettingsAuthContext()
|
const { auth, settings } = useSettingsAuthContext()
|
||||||
const token = auth?.context?.token
|
const token = auth?.context?.token
|
||||||
@ -57,7 +57,6 @@ export function App() {
|
|||||||
const {
|
const {
|
||||||
app: { onboardingStatus },
|
app: { onboardingStatus },
|
||||||
} = settings.context
|
} = settings.context
|
||||||
const { state } = useModelingContext()
|
|
||||||
|
|
||||||
useHotkeys('backspace', (e) => {
|
useHotkeys('backspace', (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
@ -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 = '',
|
||||||
@ -118,6 +119,16 @@ export function Toolbar({
|
|||||||
}),
|
}),
|
||||||
{ 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
|
||||||
@ -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>
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
@ -114,6 +114,7 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
|
|||||||
>
|
>
|
||||||
{argName}
|
{argName}
|
||||||
</span>
|
</span>
|
||||||
|
<span className="sr-only">: </span>
|
||||||
{argValue ? (
|
{argValue ? (
|
||||||
arg.inputType === 'selection' ? (
|
arg.inputType === 'selection' ? (
|
||||||
getSelectionTypeDisplayText(argValue as Selections)
|
getSelectionTypeDisplayText(argValue as Selections)
|
||||||
|
@ -3,6 +3,7 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
|
|||||||
import { useKclContext } from 'lang/KclProvider'
|
import { useKclContext } from 'lang/KclProvider'
|
||||||
import { CommandArgument } from 'lib/commandTypes'
|
import { CommandArgument } from 'lib/commandTypes'
|
||||||
import {
|
import {
|
||||||
|
Selection,
|
||||||
canSubmitSelectionArg,
|
canSubmitSelectionArg,
|
||||||
getSelectionType,
|
getSelectionType,
|
||||||
getSelectionTypeDisplayText,
|
getSelectionTypeDisplayText,
|
||||||
@ -11,6 +12,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: { [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>) =>
|
const selectionSelector = (snapshot: StateFrom<typeof modelingMachine>) =>
|
||||||
snapshot.context.selectionRanges
|
snapshot.context.selectionRanges
|
||||||
|
|
||||||
@ -85,7 +105,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"
|
||||||
|
@ -187,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
|
||||||
|
@ -72,6 +72,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>
|
||||||
@ -130,6 +131,9 @@ export const ModelingMachineProvider = ({
|
|||||||
},
|
},
|
||||||
'sketch exit execute': ({ store }) => {
|
'sketch exit execute': ({ store }) => {
|
||||||
;(async () => {
|
;(async () => {
|
||||||
|
// blocks entering a sketch until after exit sketch code has run
|
||||||
|
kclManager.isExecuting = true
|
||||||
|
|
||||||
await sceneInfra.camControls.snapToPerspectiveBeforeHandingBackControlToEngine()
|
await sceneInfra.camControls.snapToPerspectiveBeforeHandingBackControlToEngine()
|
||||||
|
|
||||||
sceneInfra.camControls.syncDirection = 'engineToClient'
|
sceneInfra.camControls.syncDirection = 'engineToClient'
|
||||||
@ -164,7 +168,7 @@ export const ModelingMachineProvider = ({
|
|||||||
|
|
||||||
store.videoElement?.pause()
|
store.videoElement?.pause()
|
||||||
kclManager.executeCode(true).then(() => {
|
kclManager.executeCode(true).then(() => {
|
||||||
if (engineCommandManager.engineConnection?.freezeFrame) return
|
if (engineCommandManager.engineConnection?.idleMode) return
|
||||||
|
|
||||||
store.videoElement?.play()
|
store.videoElement?.play()
|
||||||
})
|
})
|
||||||
@ -444,6 +448,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 +504,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)
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import styles from './ModelingPane.module.css'
|
import styles from './ModelingPane.module.css'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
|
import { ActionButton } from 'components/ActionButton'
|
||||||
|
import Tooltip from 'components/Tooltip'
|
||||||
|
|
||||||
export interface ModelingPaneProps
|
export interface ModelingPaneProps
|
||||||
extends React.PropsWithChildren,
|
extends React.PropsWithChildren,
|
||||||
@ -8,16 +10,32 @@ export interface ModelingPaneProps
|
|||||||
title: string
|
title: string
|
||||||
Menu?: React.ReactNode | React.FC
|
Menu?: React.ReactNode | React.FC
|
||||||
detailsTestId?: string
|
detailsTestId?: string
|
||||||
|
onClose: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ModelingPaneHeader = ({
|
export const ModelingPaneHeader = ({
|
||||||
title,
|
title,
|
||||||
Menu,
|
Menu,
|
||||||
}: Pick<ModelingPaneProps, 'title' | 'Menu'>) => {
|
onClose,
|
||||||
|
}: Pick<ModelingPaneProps, 'title' | 'Menu' | 'onClose'>) => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<div className="flex gap-2 items-center flex-1">{title}</div>
|
<div className="flex gap-2 items-center flex-1">{title}</div>
|
||||||
{Menu instanceof Function ? <Menu /> : Menu}
|
{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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -29,6 +47,7 @@ export const ModelingPane = ({
|
|||||||
className,
|
className,
|
||||||
Menu,
|
Menu,
|
||||||
detailsTestId,
|
detailsTestId,
|
||||||
|
onClose,
|
||||||
...props
|
...props
|
||||||
}: ModelingPaneProps) => {
|
}: ModelingPaneProps) => {
|
||||||
const { settings } = useSettingsAuthContext()
|
const { settings } = useSettingsAuthContext()
|
||||||
@ -51,7 +70,7 @@ export const ModelingPane = ({
|
|||||||
(className || '')
|
(className || '')
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ModelingPaneHeader title={title} Menu={Menu} />
|
<ModelingPaneHeader title={title} Menu={Menu} onClose={onClose} />
|
||||||
<div className="relative w-full">{children}</div>
|
<div className="relative w-full">{children}</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
|
@ -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
|
<ActionIcon
|
||||||
icon="three-dots"
|
icon="three-dots"
|
||||||
className="p-1"
|
className="p-1"
|
||||||
size="sm"
|
size="sm"
|
||||||
bgClassName={
|
bgClassName="bg-transparent dark:bg-transparent"
|
||||||
'!bg-transparent hover:!bg-primary/10 hover:dark:!bg-chalkboard-100 ui-open:!bg-primary/10 dark:ui-open:!bg-chalkboard-100 rounded-sm'
|
|
||||||
}
|
|
||||||
iconClassName={'!text-chalkboard-90 dark:!text-chalkboard-40'}
|
iconClassName={'!text-chalkboard-90 dark:!text-chalkboard-40'}
|
||||||
/>
|
/>
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
|
@ -204,6 +204,7 @@ function ModelingSidebarSection({
|
|||||||
id={`${pane.id}-pane`}
|
id={`${pane.id}-pane`}
|
||||||
title={pane.title}
|
title={pane.title}
|
||||||
Menu={pane.Menu}
|
Menu={pane.Menu}
|
||||||
|
onClose={() => togglePane(pane.id)}
|
||||||
>
|
>
|
||||||
{pane.Content instanceof Function ? (
|
{pane.Content instanceof Function ? (
|
||||||
<pane.Content />
|
<pane.Content />
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
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'
|
||||||
@ -11,6 +10,10 @@ 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'
|
import { useAppStream } from 'AppState'
|
||||||
|
import {
|
||||||
|
EngineConnectionStateType,
|
||||||
|
DisconnectingType,
|
||||||
|
} from 'lang/std/engineConnection'
|
||||||
|
|
||||||
export const Stream = () => {
|
export const Stream = () => {
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
@ -20,15 +23,28 @@ export const Stream = () => {
|
|||||||
const { settings } = useSettingsAuthContext()
|
const { settings } = useSettingsAuthContext()
|
||||||
const { state, send, context } = useModelingContext()
|
const { state, send, context } = useModelingContext()
|
||||||
const { mediaStream } = useAppStream()
|
const { mediaStream } = useAppStream()
|
||||||
const { overallState } = useNetworkContext()
|
const { overallState, immediateState } = useNetworkContext()
|
||||||
const [isFreezeFrame, setIsFreezeFrame] = useState(false)
|
const [isFreezeFrame, setIsFreezeFrame] = useState(false)
|
||||||
|
const [isPaused, setIsPaused] = useState(false)
|
||||||
|
|
||||||
const IDLE = true
|
const IDLE = settings.context.app.streamIdleMode.current
|
||||||
|
|
||||||
const isNetworkOkay =
|
const isNetworkOkay =
|
||||||
overallState === NetworkHealthState.Ok ||
|
overallState === NetworkHealthState.Ok ||
|
||||||
overallState === NetworkHealthState.Weak
|
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
|
// Linux has a default behavior to paste text on middle mouse up
|
||||||
// This adds a listener to block that pasting if the click target
|
// 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
|
// 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' })
|
sceneInfra.modelingSend({ type: 'Cancel' })
|
||||||
// Give video time to pause
|
// Give video time to pause
|
||||||
window.requestAnimationFrame(() => {
|
window.requestAnimationFrame(() => {
|
||||||
engineCommandManager.tearDown()
|
engineCommandManager.tearDown({ idleMode: true })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Teardown everything if we go hidden or reconnect
|
const onVisibilityChange = () => {
|
||||||
if (IDLE && DEV) {
|
if (globalThis.window.document.visibilityState === 'hidden') {
|
||||||
if (globalThis?.window?.document) {
|
clearTimeout(timeoutIdIdleA)
|
||||||
globalThis.window.document.onvisibilitychange = () => {
|
timeoutIdIdleA = setTimeout(teardown, IDLE_TIME_MS)
|
||||||
if (globalThis.window.document.visibilityState === 'hidden') {
|
} else if (!engineCommandManager.engineConnection?.isReady()) {
|
||||||
clearTimeout(timeoutIdIdleA)
|
clearTimeout(timeoutIdIdleA)
|
||||||
timeoutIdIdleA = setTimeout(teardown, IDLE_TIME_MS)
|
engineCommandManager.engineConnection?.connect(true)
|
||||||
} 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
|
let timeoutIdIdleB: ReturnType<typeof setTimeout> | undefined = undefined
|
||||||
|
|
||||||
const onAnyInput = () => {
|
const onAnyInput = () => {
|
||||||
@ -93,7 +112,7 @@ export const Stream = () => {
|
|||||||
timeoutIdIdleB = setTimeout(teardown, IDLE_TIME_MS)
|
timeoutIdIdleB = setTimeout(teardown, IDLE_TIME_MS)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IDLE && DEV) {
|
if (IDLE) {
|
||||||
globalThis?.window?.document?.addEventListener('keydown', onAnyInput)
|
globalThis?.window?.document?.addEventListener('keydown', onAnyInput)
|
||||||
globalThis?.window?.document?.addEventListener('mousemove', onAnyInput)
|
globalThis?.window?.document?.addEventListener('mousemove', onAnyInput)
|
||||||
globalThis?.window?.document?.addEventListener('mousedown', onAnyInput)
|
globalThis?.window?.document?.addEventListener('mousedown', onAnyInput)
|
||||||
@ -101,7 +120,7 @@ export const Stream = () => {
|
|||||||
globalThis?.window?.document?.addEventListener('touchstart', onAnyInput)
|
globalThis?.window?.document?.addEventListener('touchstart', onAnyInput)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IDLE && DEV) {
|
if (IDLE) {
|
||||||
timeoutIdIdleB = setTimeout(teardown, IDLE_TIME_MS)
|
timeoutIdIdleB = setTimeout(teardown, IDLE_TIME_MS)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,7 +128,14 @@ export const Stream = () => {
|
|||||||
globalThis?.window?.document?.removeEventListener('paste', handlePaste, {
|
globalThis?.window?.document?.removeEventListener('paste', handlePaste, {
|
||||||
capture: true,
|
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('keydown', onAnyInput)
|
||||||
globalThis?.window?.document?.removeEventListener(
|
globalThis?.window?.document?.removeEventListener(
|
||||||
'mousemove',
|
'mousemove',
|
||||||
@ -126,7 +152,7 @@ export const Stream = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [])
|
}, [IDLE])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsFirstRender(kclManager.isFirstRender)
|
setIsFirstRender(kclManager.isFirstRender)
|
||||||
@ -249,6 +275,32 @@ export const Stream = () => {
|
|||||||
<ClientSideScene
|
<ClientSideScene
|
||||||
cameraControls={settings.context.modeling.mouseControls.current}
|
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 && (
|
{(!isNetworkOkay || isLoading || isFirstRender) && !isFreezeFrame && (
|
||||||
<div className="text-center absolute inset-0">
|
<div className="text-center absolute inset-0">
|
||||||
<Loading>
|
<Loading>
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
import { createContext, useContext } from 'react'
|
import { createContext, useContext } from 'react'
|
||||||
import {
|
import {
|
||||||
ConnectingTypeGroup,
|
ConnectingTypeGroup,
|
||||||
|
EngineConnectionStateType,
|
||||||
|
EngineConnectionState,
|
||||||
initialConnectingTypeGroupState,
|
initialConnectingTypeGroupState,
|
||||||
} from '../lang/std/engineConnection'
|
} from '../lang/std/engineConnection'
|
||||||
import { NetworkStatus, NetworkHealthState } from './useNetworkStatus'
|
import { NetworkStatus, NetworkHealthState } from './useNetworkStatus'
|
||||||
|
|
||||||
export const NetworkContext = createContext<NetworkStatus>({
|
export const NetworkContext = createContext<NetworkStatus>({
|
||||||
|
immediateState: {
|
||||||
|
type: EngineConnectionStateType.Disconnected,
|
||||||
|
} as EngineConnectionState,
|
||||||
hasIssues: undefined,
|
hasIssues: undefined,
|
||||||
overallState: NetworkHealthState.Disconnected,
|
overallState: NetworkHealthState.Disconnected,
|
||||||
internetConnected: true,
|
internetConnected: true,
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
EngineCommandManagerEvents,
|
EngineCommandManagerEvents,
|
||||||
EngineConnectionEvents,
|
EngineConnectionEvents,
|
||||||
EngineConnectionStateType,
|
EngineConnectionStateType,
|
||||||
|
EngineConnectionState,
|
||||||
ErrorType,
|
ErrorType,
|
||||||
initialConnectingTypeGroupState,
|
initialConnectingTypeGroupState,
|
||||||
} from '../lang/std/engineConnection'
|
} from '../lang/std/engineConnection'
|
||||||
@ -19,6 +20,7 @@ export enum NetworkHealthState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface NetworkStatus {
|
export interface NetworkStatus {
|
||||||
|
immediateState: EngineConnectionState
|
||||||
hasIssues: boolean | undefined
|
hasIssues: boolean | undefined
|
||||||
overallState: NetworkHealthState
|
overallState: NetworkHealthState
|
||||||
internetConnected: boolean
|
internetConnected: boolean
|
||||||
@ -33,6 +35,9 @@ export interface NetworkStatus {
|
|||||||
// Must be called from one place in the application.
|
// Must be called from one place in the application.
|
||||||
// We've chosen the <Router /> component for this.
|
// We've chosen the <Router /> component for this.
|
||||||
export function useNetworkStatus() {
|
export function useNetworkStatus() {
|
||||||
|
const [immediateState, setImmediateState] = useState<EngineConnectionState>({
|
||||||
|
type: EngineConnectionStateType.Disconnected,
|
||||||
|
})
|
||||||
const [steps, setSteps] = useState(
|
const [steps, setSteps] = useState(
|
||||||
structuredClone(initialConnectingTypeGroupState)
|
structuredClone(initialConnectingTypeGroupState)
|
||||||
)
|
)
|
||||||
@ -126,6 +131,7 @@ export function useNetworkStatus() {
|
|||||||
const onConnectionStateChange = ({
|
const onConnectionStateChange = ({
|
||||||
detail: engineConnectionState,
|
detail: engineConnectionState,
|
||||||
}: CustomEvent) => {
|
}: CustomEvent) => {
|
||||||
|
setImmediateState(engineConnectionState)
|
||||||
setSteps((steps) => {
|
setSteps((steps) => {
|
||||||
let nextSteps = structuredClone(steps)
|
let nextSteps = structuredClone(steps)
|
||||||
|
|
||||||
@ -215,6 +221,7 @@ export function useNetworkStatus() {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
immediateState,
|
||||||
hasIssues,
|
hasIssues,
|
||||||
overallState,
|
overallState,
|
||||||
internetConnected,
|
internetConnected,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useLayoutEffect, useEffect, useRef, useState } from 'react'
|
import { useLayoutEffect, useEffect, useRef } 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'
|
||||||
|
@ -346,6 +346,7 @@ export class KclManager {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.ast = { ...ast }
|
this.ast = { ...ast }
|
||||||
|
this.isExecuting = true // executeAst sets this to false again
|
||||||
return this.executeAst(ast, zoomToFit)
|
return this.executeAst(ast, zoomToFit)
|
||||||
}
|
}
|
||||||
format() {
|
format() {
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
|
399
src/lang/modifyAst/addFillet.test.ts
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
404
src/lang/modifyAst/addFillet.ts
Normal 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
|
||||||
|
}
|
@ -143,6 +143,7 @@ export enum DisconnectingType {
|
|||||||
Error = 'error',
|
Error = 'error',
|
||||||
Timeout = 'timeout',
|
Timeout = 'timeout',
|
||||||
Quit = 'quit',
|
Quit = 'quit',
|
||||||
|
Pause = 'pause',
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sorted by severity
|
// Sorted by severity
|
||||||
@ -200,6 +201,7 @@ export type DisconnectingValue =
|
|||||||
| State<DisconnectingType.Error, ErrorType>
|
| State<DisconnectingType.Error, ErrorType>
|
||||||
| State<DisconnectingType.Timeout, void>
|
| State<DisconnectingType.Timeout, void>
|
||||||
| State<DisconnectingType.Quit, void>
|
| State<DisconnectingType.Quit, void>
|
||||||
|
| State<DisconnectingType.Pause, void>
|
||||||
|
|
||||||
// These are ordered by the expected sequence.
|
// These are ordered by the expected sequence.
|
||||||
export enum ConnectingType {
|
export enum ConnectingType {
|
||||||
@ -300,7 +302,7 @@ class EngineConnection extends EventTarget {
|
|||||||
pc?: RTCPeerConnection
|
pc?: RTCPeerConnection
|
||||||
unreliableDataChannel?: RTCDataChannel
|
unreliableDataChannel?: RTCDataChannel
|
||||||
mediaStream?: MediaStream
|
mediaStream?: MediaStream
|
||||||
freezeFrame: boolean = false
|
idleMode: boolean = false
|
||||||
|
|
||||||
onIceCandidate = function (
|
onIceCandidate = function (
|
||||||
this: RTCPeerConnection,
|
this: RTCPeerConnection,
|
||||||
@ -391,10 +393,10 @@ class EngineConnection extends EventTarget {
|
|||||||
this.pingPongSpan = { ping: undefined, pong: undefined }
|
this.pingPongSpan = { ping: undefined, pong: undefined }
|
||||||
|
|
||||||
// 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.idleMode is true we skip this logic so only reconnect
|
||||||
// happens on mouse move
|
// happens on mouse move
|
||||||
this.pingIntervalId = setInterval(() => {
|
this.pingIntervalId = setInterval(() => {
|
||||||
if (this.freezeFrame) return
|
if (this.idleMode) return
|
||||||
|
|
||||||
switch (this.state.type as EngineConnectionStateType) {
|
switch (this.state.type as EngineConnectionStateType) {
|
||||||
case EngineConnectionStateType.ConnectionEstablished:
|
case EngineConnectionStateType.ConnectionEstablished:
|
||||||
@ -456,8 +458,8 @@ class EngineConnection extends EventTarget {
|
|||||||
return this.state.type === EngineConnectionStateType.ConnectionEstablished
|
return this.state.type === EngineConnectionStateType.ConnectionEstablished
|
||||||
}
|
}
|
||||||
|
|
||||||
tearDown(opts?: { freeze: boolean }) {
|
tearDown(opts?: { idleMode: boolean }) {
|
||||||
this.freezeFrame = opts?.freeze ?? false
|
this.idleMode = opts?.idleMode ?? false
|
||||||
this.disconnectAll()
|
this.disconnectAll()
|
||||||
clearInterval(this.pingIntervalId)
|
clearInterval(this.pingIntervalId)
|
||||||
|
|
||||||
@ -497,10 +499,19 @@ class EngineConnection extends EventTarget {
|
|||||||
this.onNetworkStatusReady
|
this.onNetworkStatusReady
|
||||||
)
|
)
|
||||||
|
|
||||||
this.state = {
|
this.state = opts?.idleMode
|
||||||
type: EngineConnectionStateType.Disconnecting,
|
? {
|
||||||
value: { type: DisconnectingType.Quit },
|
type: EngineConnectionStateType.Disconnecting,
|
||||||
}
|
value: {
|
||||||
|
type: DisconnectingType.Pause,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
type: EngineConnectionStateType.Disconnecting,
|
||||||
|
value: {
|
||||||
|
type: DisconnectingType.Quit,
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -868,8 +879,7 @@ class EngineConnection extends EventTarget {
|
|||||||
.join('\n')
|
.join('\n')
|
||||||
if (message.request_id) {
|
if (message.request_id) {
|
||||||
const artifactThatFailed =
|
const artifactThatFailed =
|
||||||
this.engineCommandManager.artifactMap[message.request_id] ||
|
this.engineCommandManager.artifactMap[message.request_id]
|
||||||
this.engineCommandManager.lastArtifactMap[message.request_id]
|
|
||||||
console.error(
|
console.error(
|
||||||
`Error in response to request ${message.request_id}:\n${errorsString}
|
`Error in response to request ${message.request_id}:\n${errorsString}
|
||||||
failed cmd type was ${artifactThatFailed?.commandType}`
|
failed cmd type was ${artifactThatFailed?.commandType}`
|
||||||
@ -1099,8 +1109,6 @@ class EngineConnection extends EventTarget {
|
|||||||
this.unreliableDataChannel?.readyState === 'closed'
|
this.unreliableDataChannel?.readyState === 'closed'
|
||||||
if (allClosed) {
|
if (allClosed) {
|
||||||
// Do not notify the rest of the program that we have cut off anything.
|
// Do not notify the rest of the program that we have cut off anything.
|
||||||
if (this.freezeFrame) return
|
|
||||||
|
|
||||||
this.state = { type: EngineConnectionStateType.Disconnected }
|
this.state = { type: EngineConnectionStateType.Disconnected }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1174,13 +1182,6 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
* of the KCL code that generated it.
|
* of the KCL code that generated it.
|
||||||
*/
|
*/
|
||||||
artifactMap: ArtifactMap = {}
|
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;
|
* 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.
|
* that is, the *non-modeling* commands and corresponding artifacts.
|
||||||
@ -1584,10 +1585,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
type: 'receive-reliable',
|
type: 'receive-reliable',
|
||||||
data: message,
|
data: message,
|
||||||
id,
|
id,
|
||||||
cmd_type:
|
cmd_type: command?.commandType || sceneCommand?.commandType,
|
||||||
command?.commandType ||
|
|
||||||
this.lastArtifactMap[id]?.commandType ||
|
|
||||||
sceneCommand?.commandType,
|
|
||||||
})
|
})
|
||||||
Object.values(this.subscriptions[modelingResponse.type] || {}).forEach(
|
Object.values(this.subscriptions[modelingResponse.type] || {}).forEach(
|
||||||
(callback) => callback(modelingResponse)
|
(callback) => callback(modelingResponse)
|
||||||
@ -1738,7 +1736,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tearDown() {
|
tearDown(opts?: { idleMode: boolean }) {
|
||||||
if (this.engineConnection) {
|
if (this.engineConnection) {
|
||||||
this.engineConnection.removeEventListener(
|
this.engineConnection.removeEventListener(
|
||||||
EngineConnectionEvents.Opened,
|
EngineConnectionEvents.Opened,
|
||||||
@ -1757,7 +1755,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
this.onEngineConnectionNewTrack as EventListener
|
this.onEngineConnectionNewTrack as EventListener
|
||||||
)
|
)
|
||||||
|
|
||||||
this.engineConnection?.tearDown()
|
this.engineConnection?.tearDown(opts)
|
||||||
this.engineConnection = undefined
|
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
|
||||||
@ -1765,11 +1763,10 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
} else if (this.engineCommandManager?.engineConnection) {
|
} else if (this.engineCommandManager?.engineConnection) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.engineCommandManager?.engineConnection?.tearDown()
|
this.engineCommandManager?.engineConnection?.tearDown(opts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async startNewSession() {
|
async startNewSession() {
|
||||||
this.lastArtifactMap = this.artifactMap
|
|
||||||
this.artifactMap = {}
|
this.artifactMap = {}
|
||||||
await this.initPlanes()
|
await this.initPlanes()
|
||||||
}
|
}
|
||||||
|