Merge branch 'main' into pierremtb/issue2610

This commit is contained in:
Pierre Jacquier
2024-07-19 05:34:53 -04:00
260 changed files with 160901 additions and 427 deletions

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -7221,6 +7221,7 @@ test.describe('Test network and connection issues', () => {
// Expect the network to be up
await expect(page.getByText('Network Health (Connected)')).toBeVisible()
await expect(page.getByTestId('loading-stream')).not.toBeAttached()
// Click off the code pane.
await page.mouse.click(100, 100)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -16,14 +16,14 @@ export const TEST_COLORS = {
} as const
async function waitForPageLoad(page: Page) {
// wait for 'Loading stream...' spinner
await page.getByTestId('loading-stream').waitFor()
// wait for all spinners to be gone
await page
.getByTestId('loading')
.waitFor({ state: 'detached', timeout: 20_000 })
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await page.getByTestId('start-sketch').waitFor()
await expect(page.getByTestId('start-sketch')).toBeEnabled({
timeout: 20_000,
})
}
async function removeCurrentCode(page: Page) {
@ -471,8 +471,10 @@ export const doExport = async (
page: Page
): Promise<Paths> => {
await page.getByRole('button', { name: APP_NAME }).click()
await expect(page.getByRole('button', { name: 'Export Part' })).toBeVisible()
await page.getByRole('button', { name: 'Export Part' }).click()
await expect(
page.getByRole('button', { name: 'Export', exact: false })
).toBeVisible()
await page.getByRole('button', { name: 'Export', exact: false }).click()
await expect(page.getByTestId('command-bar')).toBeVisible()
// Go through export via command bar

View File

@ -90,7 +90,7 @@ describe('ZMA authorized user flows', () => {
const menuButton = await $('[data-testid="user-sidebar-toggle"]')
await click(menuButton)
const settingsButton = await $('[data-testid="settings-button"]')
const settingsButton = await $('[data-testid="user-settings"]')
await click(settingsButton)
const projectDirInput = await $('[data-testid="project-directory-input"]')

View File

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

View File

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

108
src-tauri/Cargo.lock generated
View File

@ -332,7 +332,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -367,7 +367,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -407,7 +407,7 @@ checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -550,7 +550,7 @@ dependencies = [
"proc-macro-crate 3.1.0",
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
"syn_derive",
]
@ -823,7 +823,7 @@ dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -1073,7 +1073,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
dependencies = [
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -1083,7 +1083,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f"
dependencies = [
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -1107,7 +1107,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim 0.10.0",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -1118,7 +1118,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
dependencies = [
"darling_core",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -1179,7 +1179,7 @@ checksum = "4078275de501a61ceb9e759d37bdd3d7210e654dbc167ac1a3678ef4435ed57b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
"synstructure",
]
@ -1216,7 +1216,7 @@ dependencies = [
"regex",
"serde",
"serde_tokenstream",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -1227,7 +1227,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -1288,7 +1288,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -1320,7 +1320,7 @@ checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -1427,7 +1427,7 @@ checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -1588,7 +1588,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -1704,7 +1704,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -1980,7 +1980,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -2008,7 +2008,7 @@ dependencies = [
"inflections",
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -2083,7 +2083,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -3377,7 +3377,7 @@ dependencies = [
"regex",
"regex-syntax 0.8.3",
"structmeta",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -3496,7 +3496,7 @@ dependencies = [
"phf_shared 0.11.2",
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -3564,7 +3564,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -4438,7 +4438,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -4558,7 +4558,7 @@ checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -4569,7 +4569,7 @@ checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -4602,7 +4602,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -4623,7 +4623,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -4665,7 +4665,7 @@ dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -4933,7 +4933,7 @@ dependencies = [
"proc-macro2",
"quote",
"structmeta-derive",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -4944,7 +4944,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -4966,7 +4966,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -4999,9 +4999,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.70"
version = "2.0.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f0209b68b3613b093e0ec905354eccaedcfe83b8cb37cbdeae64026c3064c16"
checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462"
dependencies = [
"proc-macro2",
"quote",
@ -5017,7 +5017,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -5034,7 +5034,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -5251,7 +5251,7 @@ dependencies = [
"serde",
"serde_json",
"sha2",
"syn 2.0.70",
"syn 2.0.71",
"tauri-utils",
"thiserror",
"time",
@ -5269,7 +5269,7 @@ dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
"tauri-codegen",
"tauri-utils",
]
@ -5627,22 +5627,22 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c"
[[package]]
name = "thiserror"
version = "1.0.61"
version = "1.0.62"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.61"
version = "1.0.62"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -5740,7 +5740,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -5940,7 +5940,7 @@ checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -5969,7 +5969,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -6099,7 +6099,7 @@ checksum = "c88cc88fd23b5a04528f3a8436024f20010a16ec18eb23c164b1242f65860130"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
"termcolor",
]
@ -6316,7 +6316,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -6415,7 +6415,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
"wasm-bindgen-shared",
]
@ -6449,7 +6449,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@ -6590,7 +6590,7 @@ checksum = "ac1345798ecd8122468840bcdf1b95e5dc6d2206c5e4b0eafa078d061f59c9bc"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -6696,7 +6696,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -6707,7 +6707,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -7159,7 +7159,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]

View File

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

View File

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

View File

@ -49,9 +49,9 @@ export const AppHeader = ({
<>
<CommandBarOpenButton />
<RefreshButton />
<UserSidebarMenu user={user} />
</>
)}
<UserSidebarMenu user={user} />
</div>
</header>
)

View File

@ -3,6 +3,7 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
import { useKclContext } from 'lang/KclProvider'
import { CommandArgument } from 'lib/commandTypes'
import {
Selection,
canSubmitSelectionArg,
getSelectionType,
getSelectionTypeDisplayText,
@ -11,13 +12,13 @@ import { modelingMachine } from 'machines/modelingMachine'
import { useCallback, useEffect, useRef, useState } from 'react'
import { StateFrom } from 'xstate'
const semanticEntityNames = {
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: string[]) {
function getSemanticSelectionType(selectionType: Array<Selection['type']>) {
const semanticSelectionType = new Set()
selectionType.forEach((type) => {
Object.entries(semanticEntityNames).forEach(([entity, entityTypes]) => {

View File

@ -311,6 +311,16 @@ const CustomIconMap = {
/>
</svg>
),
link: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.5864 4.46513C11.9532 3.09829 14.1693 3.09829 15.5361 4.46513C16.903 5.83196 16.903 8.04804 15.5361 9.41488L13.5364 11.4147C13.5839 10.9639 13.5635 10.5074 13.4752 10.0616L14.829 8.70777C15.8053 7.73146 15.8053 6.14855 14.829 5.17224C13.8527 4.19592 12.2698 4.19592 11.2935 5.17224L9.17217 7.29356C8.19586 8.26987 8.19586 9.85278 9.17217 10.8291C9.53458 11.1915 9.98056 11.4194 10.4481 11.5127C10.3749 11.6902 10.2662 11.8565 10.122 12.0007L9.76392 12.3587C9.28973 12.1899 8.84465 11.9158 8.46507 11.5362C7.09823 10.1694 7.09823 7.95328 8.46507 6.58645L10.5864 4.46513ZM4.46507 10.5864L6.46488 8.58663C6.41734 9.03738 6.43772 9.49394 6.52601 9.93972L5.17217 11.2935C4.19586 12.2699 4.19586 13.8528 5.17217 14.8291C6.14849 15.8054 7.7314 15.8054 8.70771 14.8291L10.829 12.7078C11.8053 11.7315 11.8053 10.1485 10.829 9.17223C10.4666 8.80983 10.0207 8.58195 9.55314 8.48859C9.62635 8.31113 9.73506 8.14487 9.87926 8.00066L10.2373 7.64262C10.7115 7.81138 11.1566 8.08555 11.5361 8.46512C12.903 9.83196 12.903 12.048 11.5361 13.4149L9.41481 15.5362C8.04798 16.903 5.8319 16.903 4.46507 15.5362C3.09823 14.1694 3.09823 11.9533 4.46507 10.5864Z"
fill="currentColor"
/>
</svg>
),
'make-variable': (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path

View File

@ -33,7 +33,6 @@ import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
import {
Selections,
canExtrudeSelection,
canFilletSelection,
handleSelectionBatch,
isSelectionLastLine,
isRangeInbetweenCharacters,
@ -132,6 +131,9 @@ export const ModelingMachineProvider = ({
},
'sketch exit execute': ({ store }) => {
;(async () => {
// blocks entering a sketch until after exit sketch code has run
kclManager.isExecuting = true
await sceneInfra.camControls.snapToPerspectiveBeforeHandingBackControlToEngine()
sceneInfra.camControls.syncDirection = 'engineToClient'
@ -166,7 +168,7 @@ export const ModelingMachineProvider = ({
store.videoElement?.pause()
kclManager.executeCode(true).then(() => {
if (engineCommandManager.engineConnection?.freezeFrame) return
if (engineCommandManager.engineConnection?.idleMode) return
store.videoElement?.play()
})

View File

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

View File

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

View File

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

View File

@ -1,10 +1,10 @@
import { Popover, Transition } from '@headlessui/react'
import { ActionButton } from './ActionButton'
import { ActionButton, ActionButtonProps } from './ActionButton'
import { type IndexLoaderData } from 'lib/types'
import { paths } from 'lib/paths'
import { isTauri } from '../lib/isTauri'
import { Link } from 'react-router-dom'
import { Fragment } from 'react'
import { Link, useLocation, useNavigate } from 'react-router-dom'
import { Fragment, useMemo } from 'react'
import { sep } from '@tauri-apps/api/path'
import { Logo } from './Logo'
import { APP_NAME } from 'lib/constants'
@ -12,6 +12,9 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
import { CustomIcon } from './CustomIcon'
import { useLspContext } from './LspProvider'
import { engineCommandManager } from 'lib/singletons'
import usePlatform from 'hooks/usePlatform'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import Tooltip from './Tooltip'
const ProjectSidebarMenu = ({
project,
@ -80,6 +83,10 @@ function ProjectMenuPopover({
project?: IndexLoaderData['project']
file?: IndexLoaderData['file']
}) {
const platform = usePlatform()
const location = useLocation()
const navigate = useNavigate()
const filePath = useAbsoluteFilePath()
const { commandBarState, commandBarSend } = useCommandsContext()
const { onProjectClose } = useLspContext()
const exportCommandInfo = { name: 'Export', groupId: 'modeling' }
@ -90,13 +97,82 @@ function ProjectMenuPopover({
)
)
// We filter this memoized list so that no orphan "break" elements are rendered.
const projectMenuItems = useMemo<(ActionButtonProps | 'break')[]>(
() =>
[
{
id: 'settings',
Element: 'button',
children: (
<>
<span className="flex-1">Project settings</span>
<kbd className="hotkey">{`${platform === 'macos' ? '⌘' : 'Ctrl'}${
isTauri() ? '' : '⬆'
},`}</kbd>
</>
),
onClick: () => {
const targetPath = location.pathname.includes(paths.FILE)
? filePath + paths.SETTINGS
: paths.HOME + paths.SETTINGS
navigate(targetPath + '?tab=project')
},
},
'break',
{
id: 'export',
Element: 'button',
children: (
<>
<span>Export current part</span>
{!findCommand(exportCommandInfo) && (
<Tooltip position="right" className="!max-w-none min-w-fit">
Awaiting engine connection
</Tooltip>
)}
</>
),
disabled: !findCommand(exportCommandInfo),
onClick: () =>
commandBarSend({
type: 'Find and select command',
data: exportCommandInfo,
}),
},
'break',
{
id: 'go-home',
Element: 'button',
children: 'Go to Home',
className: !isTauri() ? 'hidden' : '',
onClick: () => {
onProjectClose(file || null, project?.path || null, true)
// Clear the scene and end the session.
engineCommandManager.endSession()
},
},
].filter(
(props) =>
props === 'break' ||
(typeof props !== 'string' && !props.className?.includes('hidden'))
) as (ActionButtonProps | 'break')[],
[
platform,
findCommand,
commandBarSend,
engineCommandManager,
onProjectClose,
isTauri,
]
)
return (
<Popover className="relative">
<Popover.Button
className="rounded-sm h-9 mr-auto max-h-min min-w-max border-0 py-1 pl-0 pr-2 flex items-center focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary dark:hover:bg-chalkboard-90"
className="gap-1 rounded-sm h-9 mr-auto max-h-min min-w-max border-0 py-1 px-2 flex items-center focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary dark:hover:bg-chalkboard-90"
data-testid="project-sidebar-toggle"
>
<CustomIcon name="three-dots" className="w-5 h-5 rotate-90" />
<div className="flex flex-col items-start py-0.5">
<span className="hidden text-sm text-chalkboard-110 dark:text-chalkboard-20 whitespace-nowrap lg:block">
{isTauri() && file?.name
@ -109,68 +185,53 @@ function ProjectMenuPopover({
</span>
)}
</div>
<CustomIcon
name="caretDown"
className="w-4 h-4 text-chalkboard-70 dark:text-chalkboard-40 ui-open:rotate-180"
/>
</Popover.Button>
<Transition
enter="duration-200 ease-out"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="duration-100 ease-in"
leaveFrom="opacity-100"
leaveTo="opacity-0"
as={Fragment}
>
<Popover.Overlay className="fixed inset-0 z-20 bg-chalkboard-110/50" />
</Transition>
<Transition
enter="duration-100 ease-out"
enterFrom="opacity-0 -translate-x-1/4"
enterTo="opacity-100 translate-x-0"
leave="duration-75 ease-in"
leaveFrom="opacity-100 translate-x-0"
leaveTo="opacity-0 -translate-x-4"
enterFrom="opacity-0 -translate-y-2"
enterTo="opacity-100 translate-y-0"
as={Fragment}
>
<Popover.Panel
className="fixed inset-0 right-auto z-30 grid w-64 h-screen max-h-screen grid-cols-1 border rounded-r-md shadow-md bg-chalkboard-10 dark:bg-chalkboard-100 border-chalkboard-40 dark:border-chalkboard-80"
style={{ gridTemplateRows: 'auto 1fr auto' }}
className={`z-10 absolute top-full left-0 mt-1 pb-1 w-48 bg-chalkboard-10 dark:bg-chalkboard-90
border border-solid border-chalkboard-20 dark:border-chalkboard-90 rounded
shadow-lg`}
>
{({ close }) => (
<>
<div className="flex flex-col gap-2 p-4">
<ActionButton
Element="button"
iconStart={{ icon: 'exportFile', className: 'p-1' }}
className="border-transparent dark:border-transparent"
disabled={!findCommand(exportCommandInfo)}
onClick={() =>
commandBarSend({
type: 'Find and select command',
data: exportCommandInfo,
})
}
>
Export Part
</ActionButton>
{isTauri() && (
<ActionButton
Element="button"
onClick={() => {
onProjectClose(file || null, project?.path || null, true)
// Clear the scene and end the session.
engineCommandManager.endSession()
}}
iconStart={{
icon: 'arrowLeft',
className: 'p-1',
}}
className="border-transparent dark:border-transparent"
>
Go to Home
</ActionButton>
)}
</div>
</>
<ul className="relative flex flex-col items-stretch content-stretch p-0.5">
{projectMenuItems.map((props, index) => {
if (props === 'break') {
return index !== projectMenuItems.length - 1 ? (
<li key={`break-${index}`} className="contents">
<hr className="border-chalkboard-20 dark:border-chalkboard-80" />
</li>
) : null
}
const { id, className, children, ...rest } = props
return (
<li key={id} className="contents">
<ActionButton
{...rest}
className={
'relative !font-sans flex items-center gap-2 rounded-sm py-1.5 px-2 cursor-pointer hover:bg-chalkboard-20 dark:hover:bg-chalkboard-80 border-none text-left ' +
className
}
onMouseUp={() => {
close()
}}
>
{children}
</ActionButton>
</li>
)
})}
</ul>
)}
</Popover.Panel>
</Transition>

View File

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

View File

@ -8,7 +8,9 @@
--_delay: 200ms;
--_triangle-width: 8px;
--_triangle-height: 12px;
--_p-inline: calc(50% + calc(var(--isRTL) * var(--_triangle-width) / 2));
--_p-inline-arrow-alignment: calc(
50% + calc(var(--isRTL) * var(--_triangle-width) / 2)
);
--_p-block: 4px;
--_bg: var(--chalkboard-10);
--_shadow-alpha: 8%;
@ -33,7 +35,7 @@
font-weight: normal;
line-height: initial;
letter-spacing: 0;
padding: var(--_p-block) var(--_p-inline);
padding: var(--_p-block) calc(2 * var(--_p-block));
margin: 0;
border-radius: 3px;
background: var(--_bg);
@ -119,7 +121,7 @@
}
.tooltip.top-right {
inset-inline-end: var(--_p-inline);
inset-inline-end: var(--_p-inline-arrow-alignment);
inset-block-end: calc(100% + var(--_p-block) + var(--_triangle-height));
}
@ -130,7 +132,7 @@
}
.tooltip.right {
inset-inline-start: calc(100% + var(--_p-inline) + var(--_triangle-height));
inset-inline-start: calc(100% + var(--_triangle-height));
inset-block-end: 50%;
--_y: 50%;
}
@ -142,7 +144,7 @@
}
.tooltip.bottom-right {
inset-inline-end: var(--_p-inline);
inset-inline-end: var(--_p-inline-arrow-alignment);
inset-block-start: calc(100% + var(--_p-block) + var(--_triangle-height));
}
@ -165,7 +167,7 @@
}
.tooltip.bottom-left {
inset-inline-start: var(--_p-inline);
inset-inline-start: var(--_p-inline-arrow-alignment);
inset-block-start: calc(100% + var(--_p-block) + var(--_triangle-height));
}
@ -176,7 +178,9 @@
}
.tooltip.left {
inset-inline-end: calc(100% + var(--_p-inline) + var(--_triangle-height));
inset-inline-end: calc(
100% + var(--_p-inline-arrow-alignment) + var(--_triangle-height)
);
inset-block-end: 50%;
--_y: 50%;
}
@ -188,7 +192,7 @@
}
.tooltip.top-left {
inset-inline-start: var(--_p-inline);
inset-inline-start: var(--_p-inline-arrow-alignment);
inset-block-end: calc(100% + var(--_p-block) + var(--_triangle-height));
}

View File

@ -25,11 +25,11 @@ export function UnitsMenu() {
border border-solid border-chalkboard-10 dark:border-chalkboard-90 rounded
shadow-lg`}
>
<ul className="relative flex flex-col gap-0.5 items-stretch content-stretch">
<ul className="relative flex flex-col items-stretch content-stretch p-0.5">
{baseUnitsUnion.map((unit) => (
<li key={unit} className="contents">
<button
className="flex items-center gap-2 py-1 px-2 cursor-pointer hover:bg-chalkboard-20 dark:hover:bg-chalkboard-80 border-none text-left"
className="flex items-center gap-2 m-0 py-1.5 px-2 cursor-pointer hover:bg-chalkboard-20 dark:hover:bg-chalkboard-80 border-none text-left"
onClick={() => {
settings.send({
type: 'set.modeling.defaultUnit',

View File

@ -1,18 +1,20 @@
import { Popover, Transition } from '@headlessui/react'
import { ActionButton } from './ActionButton'
import { faBug, faSignOutAlt } from '@fortawesome/free-solid-svg-icons'
import { faGithub } from '@fortawesome/free-brands-svg-icons'
import { ActionButton, ActionButtonProps } from './ActionButton'
import { useLocation, useNavigate } from 'react-router-dom'
import { Fragment, useState } from 'react'
import { Fragment, useMemo, useState } from 'react'
import { paths } from 'lib/paths'
import { Models } from '@kittycad/lib'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import Tooltip from './Tooltip'
import usePlatform from 'hooks/usePlatform'
import { isTauri } from 'lib/isTauri'
import { CustomIcon } from './CustomIcon'
type User = Models['User_type']
const UserSidebarMenu = ({ user }: { user?: User }) => {
const platform = usePlatform()
const location = useLocation()
const filePath = useAbsoluteFilePath()
const displayedName = getDisplayName(user)
@ -20,6 +22,128 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
const navigate = useNavigate()
const send = useSettingsAuthContext()?.auth?.send
// We filter this memoized list so that no orphan "break" elements are rendered.
const userMenuItems = useMemo<(ActionButtonProps | 'break')[]>(
() =>
[
{
id: 'settings',
Element: 'button',
children: (
<>
<span className="flex-1">User settings</span>
<kbd className="hotkey">{`${platform === 'macos' ? '⌘' : 'Ctrl'}${
isTauri() ? '' : '⬆'
},`}</kbd>
</>
),
'data-testid': 'user-settings',
onClick: () => {
const targetPath = location.pathname.includes(paths.FILE)
? filePath + paths.SETTINGS
: paths.HOME + paths.SETTINGS
navigate(targetPath + '?tab=user')
},
},
{
id: 'keybindings',
Element: 'button',
children: 'Keyboard shortcuts',
onClick: () => {
const targetPath = location.pathname.includes(paths.FILE)
? filePath + paths.SETTINGS
: paths.HOME + paths.SETTINGS
navigate(targetPath + '?tab=keybindings')
},
},
{
id: 'account',
Element: 'externalLink',
to: 'https://zoo.dev/account',
children: (
<>
<span className="flex-1">Manage account</span>
<CustomIcon
name="link"
className="w-3 h-3 text-chalkboard-70 dark:text-chalkboard-40"
/>
</>
),
},
'break',
{
id: 'request-feature',
Element: 'externalLink',
to: 'https://github.com/KittyCAD/modeling-app/discussions',
children: (
<>
<span className="flex-1">Request a feature</span>
<CustomIcon
name="link"
className="w-3 h-3 text-chalkboard-70 dark:text-chalkboard-40"
/>
</>
),
},
{
id: 'report-bug',
Element: 'externalLink',
to: 'https://github.com/KittyCAD/modeling-app/issues/new/choose',
children: (
<>
<span className="flex-1">Report a bug</span>
<CustomIcon
name="link"
className="w-3 h-3 text-chalkboard-70 dark:text-chalkboard-40"
/>
</>
),
},
{
id: 'community',
Element: 'externalLink',
to: 'https://discord.gg/JQEpHR7Nt2',
children: (
<>
<span className="flex-1">Ask the community</span>
<CustomIcon
name="link"
className="w-3 h-3 text-chalkboard-70 dark:text-chalkboard-40"
/>
</>
),
},
{
id: 'release-notes',
Element: 'externalLink',
to: 'https://github.com/KittyCAD/modeling-app/releases',
children: (
<>
<span className="flex-1">Release notes</span>
<CustomIcon
name="link"
className="w-3 h-3 text-chalkboard-70 dark:text-chalkboard-40"
/>
</>
),
},
'break',
{
id: 'sign-out',
Element: 'button',
'data-testid': 'user-sidebar-sign-out',
children: 'Sign out',
onClick: () => send('Log out'),
className: '', // Just making TS's filter type coercion happy 😠
},
].filter(
(props) =>
props === 'break' ||
(typeof props !== 'string' && !props.className?.includes('hidden'))
) as (ActionButtonProps | 'break')[],
[platform, location, filePath, navigate, send]
)
// This image host goes down sometimes. We will instead rewrite the
// resource to be a local one.
if (user?.image === 'https://placekitten.com/200/200') {
@ -43,139 +167,90 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
return (
<Popover className="relative">
{user?.image && !imageLoadFailed ? (
<Popover.Button
className="relative border-0 rounded-full w-fit min-w-max p-0 group"
data-testid="user-sidebar-toggle"
>
<div className="rounded-full border overflow-hidden">
<img
src={user?.image || ''}
alt={user?.name || ''}
className="h-8 w-8 rounded-full"
referrerPolicy="no-referrer"
onError={() => setImageLoadFailed(true)}
/>
</div>
<Tooltip position="bottom-right" delay={1000}>
User menu
</Tooltip>
</Popover.Button>
) : (
<ActionButton
Element={Popover.Button}
iconStart={{ icon: 'menu' }}
className="border-transparent !px-0"
data-testid="user-sidebar-toggle"
>
<Tooltip position="bottom-right" delay={1000}>
User menu
</Tooltip>
</ActionButton>
)}
<Transition
enter="duration-200 ease-out"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="duration-100 ease-in"
leaveFrom="opacity-100"
leaveTo="opacity-0"
as={Fragment}
<Popover.Button
className="relative group border-0 w-fit min-w-max p-0 rounded-l-full focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary"
data-testid="user-sidebar-toggle"
>
<Popover.Overlay className="fixed z-20 inset-0 bg-chalkboard-110/50" />
</Transition>
<div className="flex items-center">
<div className="rounded-full border overflow-hidden">
{user?.image && !imageLoadFailed ? (
<img
src={user?.image || ''}
alt={user?.name || ''}
className="h-7 w-7 rounded-full"
referrerPolicy="no-referrer"
onError={() => setImageLoadFailed(true)}
/>
) : (
<CustomIcon
name="person"
className="w-5 h-5 text-chalkboard-70 dark:text-chalkboard-40 bg-chalkboard-20 dark:bg-chalkboard-80"
/>
)}
</div>
<CustomIcon
name="caretDown"
className="w-4 h-4 text-chalkboard-70 dark:text-chalkboard-40 ui-open:rotate-180"
/>
</div>
<Tooltip position="bottom-right" delay={1000} hoverOnly>
User menu
</Tooltip>
</Popover.Button>
<Transition
enter="duration-100 ease-out"
enterFrom="opacity-0 translate-x-1/4"
enterTo="opacity-100 translate-x-0"
leave="duration-75 ease-in"
leaveFrom="opacity-100 translate-x-0"
leaveTo="opacity-0 translate-x-4"
enterFrom="opacity-0 -translate-y-2"
enterTo="opacity-100 translate-y-0"
as={Fragment}
>
<Popover.Panel className="fixed inset-0 left-auto z-30 w-64 bg-chalkboard-10 dark:bg-chalkboard-90 border border-chalkboard-30 dark:border-chalkboard-80 shadow-md rounded-l-md overflow-hidden">
<Popover.Panel
className={`z-10 absolute top-full right-0 mt-1 pb-1 w-48 bg-chalkboard-10 dark:bg-chalkboard-90
border border-solid border-chalkboard-20 dark:border-chalkboard-90 rounded
shadow-lg`}
>
{({ close }) => (
<>
{user && (
<div className="flex items-center gap-4 px-4 py-3 bg-chalkboard-20/50 dark:bg-chalkboard-80/50 border-b border-b-chalkboard-30 dark:border-b-chalkboard-80">
{user.image && !imageLoadFailed && (
<div className="rounded-full shadow-inner overflow-hidden">
<img
src={user.image}
alt={user.name || ''}
className="h-8 w-8"
referrerPolicy="no-referrer"
onError={() => setImageLoadFailed(true)}
/>
</div>
)}
<div>
<p className="m-0 text-mono" data-testid="username">
{displayedName || ''}
<div className="flex flex-col gap-1 px-2.5 py-3 bg-chalkboard-20 dark:bg-chalkboard-80/50">
<p className="m-0 text-mono text-xs" data-testid="username">
{displayedName || ''}
</p>
{displayedName !== user.email && (
<p
className="m-0 text-chalkboard-70 dark:text-chalkboard-40 text-xs"
data-testid="email"
>
{user.email}
</p>
{displayedName !== user.email && (
<p
className="m-0 text-chalkboard-70 dark:text-chalkboard-40 text-xs"
data-testid="email"
>
{user.email}
</p>
)}
</div>
)}
</div>
)}
<div className="p-4 flex flex-col gap-2">
<ActionButton
Element="button"
iconStart={{ icon: 'settings' }}
className="border-transparent dark:border-transparent hover:bg-transparent"
onClick={() => {
// since /settings is a nested route the sidebar doesn't close
// automatically when navigating to it
close()
const targetPath = location.pathname.includes(paths.FILE)
? filePath + paths.SETTINGS
: paths.HOME + paths.SETTINGS
navigate(targetPath)
}}
data-testid="settings-button"
>
Settings
</ActionButton>
<ActionButton
Element="externalLink"
to="https://github.com/KittyCAD/modeling-app/discussions"
iconStart={{ icon: faGithub, className: 'p-1', size: 'sm' }}
className="border-transparent dark:border-transparent"
>
Request a feature
</ActionButton>
<ActionButton
Element="externalLink"
to="https://github.com/KittyCAD/modeling-app/issues/new/choose"
iconStart={{ icon: faBug, className: 'p-1', size: 'sm' }}
className="border-transparent dark:border-transparent"
>
Report a bug
</ActionButton>
<ActionButton
Element="button"
onClick={() => send('Log out')}
iconStart={{
icon: faSignOutAlt,
className: 'p-1',
bgClassName: '!bg-transparent',
size: 'sm',
iconClassName: '!text-destroy-70',
}}
className="border-transparent dark:border-transparent hover:border-destroy-40 dark:hover:border-destroy-60 hover:bg-destroy-10/20 dark:hover:bg-destroy-80/20"
data-testid="user-sidebar-sign-out"
>
Sign out
</ActionButton>
</div>
<ul className="relative flex flex-col items-stretch content-stretch p-0.5">
{userMenuItems.map((props, index) => {
if (props === 'break') {
return index !== userMenuItems.length - 1 ? (
<li key={`break-${index}`} className="contents">
<hr className="border-chalkboard-20 dark:border-chalkboard-80" />
</li>
) : null
}
const { id, children, ...rest } = props
return (
<li key={id} className="contents">
<ActionButton
{...rest}
className="!font-sans flex items-center gap-2 rounded-sm py-1.5 px-2 cursor-pointer hover:bg-chalkboard-20 dark:hover:bg-chalkboard-80 border-none text-left"
onMouseUp={() => {
close()
}}
>
{children}
</ActionButton>
</li>
)
})}
</ul>
</>
)}
</Popover.Panel>

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import { useLayoutEffect, useEffect, useRef, useState } from 'react'
import { useLayoutEffect, useEffect, useRef } from 'react'
import { engineCommandManager, kclManager } from 'lib/singletons'
import { deferExecution } from 'lib/utils'
import { Themes } from 'lib/theme'
@ -45,9 +45,6 @@ export function useSetupEngineManager(
streamRef?.current?.offsetWidth ?? 0,
streamRef?.current?.offsetHeight ?? 0
)
if (restart) {
kclManager.isFirstRender = false
}
engineCommandManager.start({
restart,
setMediaStream: (mediaStream) => setMediaStream(mediaStream),

View File

@ -260,8 +260,17 @@ code {
@layer components {
kbd.hotkey {
@apply font-mono text-xs inline-block px-1 py-0.5 rounded-sm;
@apply font-mono text-xs inline-block px-0.5 py-[2px] rounded;
/* This is the only place in our code where layout is impacted by theme.
* We may not want that later, if hotkeys are possibly visible
* while switching theme, but more padding feels better in dark mode.
*/
@apply dark:px-1;
@apply text-chalkboard-70 dark:text-chalkboard-40;
@apply bg-chalkboard-20 dark:bg-chalkboard-90;
@apply border border-t-0 border-b-2 border-chalkboard-30 dark:border-chalkboard-80;
}
}

View File

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

View File

@ -7,10 +7,15 @@ import {
Program,
CallExpression,
} from '../wasm'
import { addFillet, isTagUsedInFillet } from './addFillet'
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
@ -24,7 +29,7 @@ const runFilletTest = async (
expectedCode: string
) => {
const astOrError = parse(code)
if (astOrError instanceof Error) {
if (err(astOrError)) {
return new Error('AST not found')
}
@ -48,14 +53,14 @@ const runFilletTest = async (
ast,
extrudeRange
)
if (pathToExtrudeNode instanceof Error) {
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 (result instanceof Error) {
if (err(result)) {
return result
}
const { modifiedAst } = result
@ -313,3 +318,82 @@ const extrude001 = extrude(-5, sketch001)
expect(edges).toEqual([])
})
})
describe('Testing button states', () => {
const runButtonStateTest = async (
code: string,
segmentSnippet: string,
expectedState: boolean
) => {
// ast
const astOrError = parse(code)
if (err(astOrError)) {
return new Error('AST not found')
}
const ast = astOrError as Program
// selectionRanges
const range: [number, number] = segmentSnippet
? [
code.indexOf(segmentSnippet),
code.indexOf(segmentSnippet) + segmentSnippet.length,
]
: [ast.end, ast.end] // empty line in the end of the code
const selectionRanges: Selections = {
codeBasedSelections: [
{
range,
type: 'default',
},
],
otherSelections: [],
}
// state
const buttonState = hasValidFilletSelection({
ast,
selectionRanges,
code,
})
expect(buttonState).toEqual(expectedState)
}
const codeWithBody: string = `
const sketch001 = startSketchOn('XY')
|> startProfileAt([-20, -5], %)
|> line([0, 10], %)
|> line([10, 0], %)
|> line([0, -10], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(-10, sketch001)
`
const codeWithoutBodies: string = `
const sketch001 = startSketchOn('XY')
|> startProfileAt([-20, -5], %)
|> line([0, 10], %)
|> line([10, 0], %)
|> line([0, -10], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
`
// body is missing
it('should return false when body is missing and nothing is selected', async () => {
await runButtonStateTest(codeWithoutBodies, '', false)
})
it('should return false when body is missing and segment is selected', async () => {
await runButtonStateTest(codeWithoutBodies, `line([10, 0], %)`, false)
})
// body exists
it('should return true when body exists and nothing is selected', async () => {
await runButtonStateTest(codeWithBody, '', true)
})
it('should return true when body exists and segment is selected', async () => {
await runButtonStateTest(codeWithBody, `line([10, 0], %)`, true)
})
it('hould return false when body exists and not a segment is selected', async () => {
await runButtonStateTest(codeWithBody, `close(%)`, false)
})
})

View File

@ -30,7 +30,6 @@ import {
} from '../std/sketch'
import { err } from 'lib/trap'
import { Selections, canFilletSelection } from 'lib/selections'
// import { forEach } from 'jszip'
export function addFillet(
node: Program,

View File

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

View File

@ -163,6 +163,17 @@ export function createSettings() {
validate: (v) => typeof v === 'boolean',
hideOnPlatform: 'both', //for now
}),
/**
* Stream resource saving behavior toggle
*/
streamIdleMode: new Setting<boolean>({
defaultValue: false,
description: 'Toggle stream idling, saving bandwidth and battery',
validate: (v) => typeof v === 'boolean',
commandConfig: {
inputType: 'boolean',
},
}),
onboardingStatus: new Setting<string>({
defaultValue: '',
validate: (v) => typeof v === 'string',

View File

@ -38,6 +38,7 @@ function configurationToSettingsPayload(
: undefined,
onboardingStatus: configuration?.settings?.app?.onboarding_status,
dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner,
streamIdleMode: configuration?.settings?.app?.stream_idle_mode,
projectDirectory: configuration?.settings?.project?.directory,
enableSSAO: configuration?.settings?.modeling?.enable_ssao,
},
@ -75,6 +76,7 @@ function projectConfigurationToSettingsPayload(
: undefined,
onboardingStatus: configuration?.settings?.app?.onboarding_status,
dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner,
streamIdleMode: configuration?.settings?.app?.stream_idle_mode,
enableSSAO: configuration?.settings?.modeling?.enable_ssao,
},
modeling: {

File diff suppressed because one or more lines are too long

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