Merge branch 'main' into coredump-enginecommandmanager

This commit is contained in:
Dan Shaw
2024-05-21 12:30:21 -07:00
49 changed files with 4249 additions and 151 deletions

View File

@ -59,7 +59,9 @@ followed by:
```
yarn build:wasm-dev
```
or if you have the gh cli installed
```
./get-latest-wasm-bundle.sh # this will download the latest main wasm bundle
```
@ -100,6 +102,7 @@ yarn test
Which will run our suite of [Vitest unit](https://vitest.dev/) and [React Testing Library E2E](https://testing-library.com/docs/react-testing-library/intro/) tests, in interactive mode by default.
For running the rust (not tauri rust though) only, you can
```bash
cd src/wasm-lib
cargo test
@ -162,6 +165,7 @@ console.log(
- `)
)
```
grab the md list and delete any that are older than the last bump
2. Merge the PR
@ -191,23 +195,26 @@ $ cargo +nightly fuzz run parser
For more information on fuzzing you can check out
[this guide](https://rust-fuzz.github.io/book/cargo-fuzz.html).
### Playwright
First time running plawright locally, you'll need to add the secrets file
```bash
touch ./e2e/playwright/playwright-secrets.env
printf 'token="your-token"\nsnapshottoken="your-snapshot-token"' > ./e2e/playwright/playwright-secrets.env
```
then replace "your-token" with a dev token from dev.zoo.dev/account/api-tokens
then:
run playwright
```
yarn playwright test
```
run a specific test suite
```
yarn playwright test src/e2e-tests/example.spec.ts
```
@ -216,14 +223,17 @@ run a specific test change the test from `test('...` to `test.only('...`
(note if you commit this, the tests will instantly fail without running any of the tests)
run headed
```
yarn playwright test --headed
```
run with step through debugger
```
PWDEBUG=1 yarn playwright test
```
However, if you want a debugger I recommend using VSCode and the `playwright` extension, as the above command is a cruder debugger that steps into every function call which is annoying.
With the extension you can set a breakpoint after `waitForDefaultPlanesVisibilityChange` in order to skip app loading, then the vscode debugger's "step over" is much better for being able to stay at the right level of abstraction as you debug the code.
@ -268,7 +278,6 @@ Where `./store` should look like this
</details>
However because much of our tests involve clicking in the stream at specific locations, it's code-gen looks `await page.locator('video').click();` when really we need to use a pixel coord, so I think it's of limited use.
#### Some notes on CI
@ -299,3 +308,7 @@ PS: for the debug panel, the following JSON is useful for snapping the camera
```
</details>
## KCL
For how to contribute to KCL, [see our KCL README](https://github.com/KittyCAD/modeling-app/tree/main/src/wasm-lib/kcl).

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -56,6 +56,9 @@ layout: manual
* [`patternLinear3d`](kcl/patternLinear3d)
* [`pi`](kcl/pi)
* [`pow`](kcl/pow)
* [`profileStart`](kcl/profileStart)
* [`profileStartX`](kcl/profileStartX)
* [`profileStartY`](kcl/profileStartY)
* [`revolve`](kcl/revolve)
* [`segAng`](kcl/segAng)
* [`segEndX`](kcl/segEndX)

206
docs/kcl/profileStart.md Normal file

File diff suppressed because one or more lines are too long

201
docs/kcl/profileStartX.md Normal file

File diff suppressed because one or more lines are too long

200
docs/kcl/profileStartY.md Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -424,6 +424,80 @@ test('if you write invalid kcl you get inlined errors', async ({ page }) => {
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
})
test('error with 2 source ranges gets 2 diagnostics', async ({ page }) => {
const u = getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const length = .750
const width = 0.500
const height = 0.500
const dia = 4
fn squareHole = (l, w) => {
const squareHoleSketch = startSketchOn('XY')
|> startProfileAt([-width / 2, -length / 2], %)
|> lineTo([width / 2, -length / 2], %)
|> lineTo([width / 2, length / 2], %)
|> lineTo([-width / 2, length / 2], %)
|> close(%)
return squareHoleSketch
}
`
)
})
await page.setViewportSize({ width: 1000, height: 500 })
await page.goto('/')
const lspStartPromise = page.waitForEvent('console', async (message) => {
// it would be better to wait for a message that the kcl lsp has started by looking for the message message.text().includes('[lsp] [window/logMessage]')
// but that doesn't seem to make it to the console for macos/safari :(
if (message.text().includes('start kcl lsp')) {
await new Promise((resolve) => setTimeout(resolve, 200))
return true
}
return false
})
await page.goto('/')
await u.waitForAuthSkipAppStart()
await lspStartPromise
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// check no error to begin with
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
// Click on the bottom of the code editor to add a new line
await page.click('.cm-content')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('Enter')
await page.keyboard.type(`const extrusion = startSketchOn('XY')
|> circle([0, 0], dia/2, %)
|> hole(squareHole(length, width, height), %)
|> extrude(height, %)`)
// error in gutter
await expect(page.locator('.cm-lint-marker-error').first()).toBeVisible()
await page.hover('.cm-lint-marker-error:first-child')
await expect(page.getByText('Expected 2 arguments, got 3')).toBeVisible()
// Make sure there are two diagnostics
await expect(page.locator('.cm-lint-marker-error')).toHaveCount(2)
})
test('if your kcl gets an error from the engine it is inlined', async ({
page,
}) => {
@ -646,8 +720,8 @@ test('Auto complete works', async ({ page }) => {
await page.click('.cm-content')
await page.keyboard.type('const part001 = start')
// expect there to be three auto complete options
await expect(page.locator('.cm-completionLabel')).toHaveCount(3)
// expect there to be six auto complete options
await expect(page.locator('.cm-completionLabel')).toHaveCount(6)
await page.getByText('startSketchOn').click()
await page.keyboard.type("'XZ'")
await page.keyboard.press('Tab')
@ -775,6 +849,57 @@ test('Project settings can be set and override user settings', async ({
await expect(page.locator('select[name="app-theme"]')).toHaveValue('light')
})
test('Project settings can be opened with keybinding from the editor', async ({
page,
}) => {
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/', { waitUntil: 'domcontentloaded' })
await page
.getByRole('button', { name: 'Start Sketch' })
.waitFor({ state: 'visible' })
// Put the cursor in the editor
await page.click('.cm-content')
// Open the settings modal with the browser keyboard shortcut
await page.keyboard.press('Meta+Shift+,')
await expect(
page.getByRole('heading', { name: 'Settings', exact: true })
).toBeVisible()
await page
.locator('select[name="app-theme"]')
.selectOption({ value: 'light' })
// Verify the toast appeared
await expect(
page.getByText(`Set theme to "light" for this project`)
).toBeVisible()
// Check that the theme changed
await expect(page.locator('body')).not.toHaveClass(`body-bg dark`)
// Check that the user setting was not changed
await page.getByRole('radio', { name: 'User' }).click()
await expect(page.locator('select[name="app-theme"]')).toHaveValue('dark')
// Roll back to default "system" theme
await page
.getByText(
'themeRoll back themeRoll back to match defaultThe overall appearance of the appl'
)
.hover()
await page
.getByRole('button', {
name: 'Roll back theme ; Has tooltip: Roll back to match default',
})
.click()
await expect(page.locator('select[name="app-theme"]')).toHaveValue('system')
// Check that the project setting did not change
await page.getByRole('radio', { name: 'Project' }).click()
await expect(page.locator('select[name="app-theme"]')).toHaveValue('light')
})
test('Click through each onboarding step', async ({ page }) => {
const u = getUtils(page)
@ -1093,6 +1218,51 @@ test.describe('Command bar tests', () => {
await expect(page.locator('body')).not.toHaveClass(`body-bg dark`)
})
test('Command bar keybinding works from code editor and can change a setting', async ({
page,
}) => {
// Brief boilerplate
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/', { waitUntil: 'domcontentloaded' })
let cmdSearchBar = page.getByPlaceholder('Search commands')
// Put the cursor in the code editor
await page.click('.cm-content')
// Now try the same, but with the keyboard shortcut, check focus
await page.keyboard.press('Meta+K')
await expect(cmdSearchBar).toBeVisible()
await expect(cmdSearchBar).toBeFocused()
// Try typing in the command bar
await page.keyboard.type('theme')
const themeOption = page.getByRole('option', {
name: 'Settings · app · theme',
})
await expect(themeOption).toBeVisible()
await themeOption.click()
const themeInput = page.getByPlaceholder('Select an option')
await expect(themeInput).toBeVisible()
await expect(themeInput).toBeFocused()
// Select dark theme
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await expect(page.getByRole('option', { name: 'system' })).toHaveAttribute(
'data-headlessui-state',
'active'
)
await page.keyboard.press('Enter')
// Check the toast appeared
await expect(
page.getByText(`Set theme to "system" for this project`)
).toBeVisible()
// Check that the theme changed
await expect(page.locator('body')).not.toHaveClass(`body-bg dark`)
})
test('Can extrude from the command bar', async ({ page }) => {
await page.addInitScript(async () => {
localStorage.setItem(

View File

@ -17,12 +17,12 @@
"@replit/codemirror-interact": "^6.3.1",
"@tauri-apps/api": "2.0.0-beta.8",
"@tauri-apps/plugin-dialog": "^2.0.0-beta.2",
"@tauri-apps/plugin-fs": "^2.0.0-beta.2",
"@tauri-apps/plugin-fs": "^2.0.0-beta.3",
"@tauri-apps/plugin-http": "^2.0.0-beta.2",
"@tauri-apps/plugin-os": "^2.0.0-beta.2",
"@tauri-apps/plugin-process": "^2.0.0-beta.2",
"@tauri-apps/plugin-shell": "^2.0.0-beta.2",
"@tauri-apps/plugin-updater": "^2.0.0-beta.2",
"@tauri-apps/plugin-updater": "^2.0.0-beta.3",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^15.0.2",
"@testing-library/user-event": "^14.5.2",
@ -52,15 +52,15 @@
"react-json-view": "^1.21.3",
"react-modal": "^3.16.1",
"react-modal-promise": "^1.0.2",
"react-router-dom": "^6.22.3",
"react-router-dom": "^6.23.1",
"sketch-helpers": "^0.0.4",
"swr": "^2.2.5",
"three": "^0.163.0",
"three": "^0.164.1",
"ts-node": "^10.9.2",
"typescript": "^5.4.5",
"ua-parser-js": "^1.0.37",
"uuid": "^9.0.1",
"vitest": "^1.5.0",
"vitest": "^1.6.0",
"vscode-jsonrpc": "^8.1.0",
"vscode-languageserver-protocol": "^3.17.5",
"wasm-pack": "^0.12.1",

View File

@ -23,6 +23,7 @@ import { useRefreshSettings } from 'hooks/useRefreshSettings'
import { ModelingSidebar } from 'components/ModelingSidebar/ModelingSidebar'
import { LowerRightControls } from 'components/LowerRightControls'
import ModalContainer from 'react-modal-promise'
import useHotkeyWrapper from 'lib/hotkeyWrapper'
export function App() {
useRefreshSettings(paths.FILE + 'SETTINGS')
@ -63,8 +64,8 @@ export function App() {
useHotkeys('backspace', (e) => {
e.preventDefault()
})
useHotkeys(
isTauri() ? 'mod + ,' : 'shift + mod + ,',
useHotkeyWrapper(
[isTauri() ? 'mod + ,' : 'shift + mod + ,'],
() => navigate(filePath + paths.SETTINGS),
{
splitKey: '|',

View File

@ -1,11 +1,11 @@
import { Dialog, Popover, Transition } from '@headlessui/react'
import { Fragment, useEffect } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { useCommandsContext } from 'hooks/useCommandsContext'
import CommandBarArgument from './CommandBarArgument'
import CommandComboBox from '../CommandComboBox'
import CommandBarReview from './CommandBarReview'
import { useLocation } from 'react-router-dom'
import useHotkeyWrapper from 'lib/hotkeyWrapper'
export const CommandBar = () => {
const { pathname } = useLocation()
@ -22,7 +22,7 @@ export const CommandBar = () => {
}, [pathname])
// Hook up keyboard shortcuts
useHotkeys(['mod+k', 'mod+/'], () => {
useHotkeyWrapper(['mod+k', 'mod+/'], () => {
if (commandBarState.context.commands.length === 0) return
if (commandBarState.matches('Closed')) {
commandBarSend({ type: 'Open' })

View File

@ -8,7 +8,6 @@ import { Dialog, Disclosure } from '@headlessui/react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faChevronRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
import { useFileContext } from 'hooks/useFileContext'
import { useHotkeys } from 'react-hotkeys-hook'
import styles from './FileTree.module.css'
import { sortProject } from 'lib/tauriFS'
import { FILE_EXT } from 'lib/constants'
@ -16,6 +15,7 @@ import { CustomIcon } from './CustomIcon'
import { codeManager, kclManager } from 'lib/singletons'
import { useDocumentHasFocus } from 'hooks/useDocumentHasFocus'
import { useLspContext } from './LspProvider'
import useHotkeyWrapper from 'lib/hotkeyWrapper'
function getIndentationCSS(level: number) {
return `calc(1rem * ${level + 1})`
@ -333,8 +333,8 @@ export const FileTreeMenu = () => {
send({ type: 'Create file', data: { name: '', makeDir: true } })
}
useHotkeys('meta + n', createFile)
useHotkeys('meta + shift + n', createFolder)
useHotkeyWrapper(['meta + n'], createFile)
useHotkeyWrapper(['meta + shift + n'], createFolder)
return (
<>

View File

@ -55,9 +55,9 @@ import { Models } from '@kittycad/lib/dist/types/src'
import toast from 'react-hot-toast'
import { EditorSelection } from '@uiw/react-codemirror'
import { CoreDumpManager } from 'lib/coredump'
import { useHotkeys } from 'react-hotkeys-hook'
import { useSearchParams } from 'react-router-dom'
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
import useHotkeyWrapper from 'lib/hotkeyWrapper'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
@ -103,7 +103,7 @@ export const ModelingMachineProvider = ({
htmlRef,
token
)
useHotkeys('meta + shift + .', () => coreDump(coreDumpManager, true))
useHotkeyWrapper(['meta + shift + .'], () => coreDump(coreDumpManager, true))
// Settings machine setup
// const retrievedSettings = useRef(

View File

@ -36,10 +36,6 @@ import {
import interact from '@replit/codemirror-interact'
import { kclManager, editorManager, codeManager } from 'lib/singletons'
import { useHotkeys } from 'react-hotkeys-hook'
import { isTauri } from 'lib/isTauri'
import { useNavigate } from 'react-router-dom'
import { paths } from 'lib/paths'
import makeUrlPathRelative from 'lib/makeUrlPathRelative'
import { useLspContext } from 'components/LspProvider'
import { Prec, EditorState, Extension } from '@codemirror/state'
import {
@ -67,7 +63,6 @@ export const KclEditorPane = () => {
? getSystemTheme()
: context.app.theme.current
const { copilotLSP, kclLSP } = useLspContext()
const navigate = useNavigate()
useEffect(() => {
if (typeof window === 'undefined') return
@ -76,6 +71,8 @@ export const KclEditorPane = () => {
return () => window.removeEventListener('online', onlineCallback)
}, [])
// Since these already exist in the editor, we don't need to define them
// with the wrapper.
useHotkeys('mod+z', (e) => {
e.preventDefault()
editorManager.undo()
@ -87,6 +84,7 @@ export const KclEditorPane = () => {
const textWrapping = context.textEditor.textWrapping
const cursorBlinking = context.textEditor.blinkingCursor
const codeMirrorHotkeys = codeManager.getCodemirrorHotkeys()
const editorExtensions = useMemo(() => {
const extensions = [
@ -106,20 +104,7 @@ export const KclEditorPane = () => {
...completionKeymap,
...lintKeymap,
indentWithTab,
{
key: 'Meta-k',
run: () => {
editorManager.commandBarSend({ type: 'Open' })
return false
},
},
{
key: isTauri() ? 'Meta-,' : 'Meta-Shift-,',
run: () => {
navigate(makeUrlPathRelative(paths.SETTINGS))
return false
},
},
...codeMirrorHotkeys,
{
key: editorShortcutMeta.convertToVariable.codeMirror,
run: () => {
@ -188,7 +173,13 @@ export const KclEditorPane = () => {
}
return extensions
}, [kclLSP, copilotLSP, textWrapping.current, cursorBlinking.current])
}, [
kclLSP,
copilotLSP,
textWrapping.current,
cursorBlinking.current,
codeMirrorHotkeys,
])
const initialCode = useRef(codeManager.code)

View File

@ -6,6 +6,7 @@ import { isTauri } from 'lib/isTauri'
import { writeTextFile } from '@tauri-apps/plugin-fs'
import toast from 'react-hot-toast'
import { editorManager } from 'lib/singletons'
import { KeyBinding } from '@uiw/react-codemirror'
const PERSIST_CODE_TOKEN = 'persistCode'
@ -13,6 +14,7 @@ export default class CodeManager {
private _code: string = bracket
#updateState: (arg: string) => void = () => {}
private _currentFilePath: string | null = null
private _hotkeys: { [key: string]: () => void } = {}
constructor() {
if (isTauri()) {
@ -48,6 +50,20 @@ export default class CodeManager {
this.#updateState = setCode
}
registerHotkey(hotkey: string, callback: () => void) {
this._hotkeys[hotkey] = callback
}
getCodemirrorHotkeys(): KeyBinding[] {
return Object.keys(this._hotkeys).map((key) => ({
key,
run: () => {
this._hotkeys[key]()
return false
},
}))
}
updateCurrentFilePath(path: string) {
this._currentFilePath = path
}

View File

@ -1,20 +1,21 @@
import { ToolTip } from '../useStore'
import { Selection, Selections } from 'lib/selections'
import {
ArrayExpression,
BinaryExpression,
Program,
SyntaxType,
Value,
CallExpression,
ExpressionStatement,
VariableDeclaration,
ReturnStatement,
ArrayExpression,
PathToNode,
PipeExpression,
Program,
ProgramMemory,
ReturnStatement,
SketchGroup,
SourceRange,
PipeExpression,
SyntaxType,
Value,
VariableDeclaration,
VariableDeclarator,
} from './wasm'
import { createIdentifier, splitPathAtLastIndex } from './modifyAst'
import { getSketchSegmentFromSourceRange } from './std/sketchConstraints'
@ -295,6 +296,58 @@ export function getNodePathFromSourceRange(
return path
}
type KCLNode =
| Value
| ExpressionStatement
| VariableDeclaration
| VariableDeclarator
| ReturnStatement
export function traverse(
node: KCLNode,
option: {
enter?: (node: KCLNode) => void
leave?: (node: KCLNode) => void
}
) {
option?.enter?.(node)
const _traverse = (node: KCLNode) => traverse(node, option)
if (node.type === 'VariableDeclaration') {
node.declarations.forEach(_traverse)
} else if (node.type === 'VariableDeclarator') {
_traverse(node.init)
} else if (node.type === 'PipeExpression') {
node.body.forEach(_traverse)
} else if (node.type === 'CallExpression') {
_traverse(node.callee)
node.arguments.forEach(_traverse)
} else if (node.type === 'BinaryExpression') {
_traverse(node.left)
_traverse(node.right)
} else if (node.type === 'Identifier') {
// do nothing
} else if (node.type === 'Literal') {
// do nothing
} else if (node.type === 'ArrayExpression') {
node.elements.forEach(_traverse)
} else if (node.type === 'ObjectExpression') {
node.properties.forEach(({ key, value }) => {
_traverse(key)
_traverse(value)
})
} else if (node.type === 'UnaryExpression') {
_traverse(node.argument)
} else if (node.type === 'MemberExpression') {
// hmm this smell
_traverse(node.object)
_traverse(node.property)
} else if ('body' in node && Array.isArray(node.body)) {
node.body.forEach(_traverse)
}
option?.leave?.(node)
}
export interface PrevVariable<T> {
key: string
value: T

View File

@ -551,6 +551,7 @@ class EngineConnection {
// Everything is now connected.
this.state = { type: EngineConnectionStateType.ConnectionEstablished }
this.engineCommandManager.inSequence = 1
this.onEngineConnectionOpen(this)
})

37
src/lib/hotkeyWrapper.ts Normal file
View File

@ -0,0 +1,37 @@
import { Options, useHotkeys } from 'react-hotkeys-hook'
import { useEffect } from 'react'
import { codeManager } from './singletons'
// Hotkey wrapper wraps hotkeys for the app (outside of the editor)
// With hotkeys inside the editor.
// This way we can have hotkeys defined in one place and not have to worry about
// conflicting hotkeys, or them only being implemented for the app but not
// inside the editor.
// TODO: would be nice if this didn't have to be a react hook. It's not needed
// for the code mirror stuff but but it is needed for the useHotkeys hook.
export default function useHotkeyWrapper(
hotkey: string[],
callback: () => void,
additionalOptions?: Options
) {
useHotkeys(hotkey, callback, additionalOptions)
useEffect(() => {
for (const key of hotkey) {
const keybinding = mapHotkeyToCodeMirrorHotkey(key)
codeManager.registerHotkey(keybinding, callback)
}
})
}
// Convert hotkey to code mirror hotkey
// See: https://codemirror.net/docs/ref/#view.KeyBinding
function mapHotkeyToCodeMirrorHotkey(hotkey: string): string {
return hotkey
.replaceAll('+', '-')
.replaceAll(' ', '')
.replaceAll('mod', 'Meta')
.replaceAll('meta', 'Meta')
.replaceAll('ctrl', 'Ctrl')
.replaceAll('shift', 'Shift')
.replaceAll('alt', 'Alt')
}

View File

@ -177,6 +177,15 @@ dependencies = [
"num-traits 0.2.18",
]
[[package]]
name = "arbitrary"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110"
dependencies = [
"derive_arbitrary",
]
[[package]]
name = "arc-swap"
version = "1.7.0"
@ -979,6 +988,17 @@ dependencies = [
"syn 2.0.65",
]
[[package]]
name = "derive_arbitrary"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.65",
]
[[package]]
name = "diesel_derives"
version = "2.1.3"
@ -1851,15 +1871,6 @@ dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.12.1"
@ -2674,7 +2685,7 @@ dependencies = [
"bincode",
"either",
"fnv",
"itertools 0.11.0",
"itertools 0.12.1",
"lazy_static",
"nom",
"quick-xml",
@ -5303,13 +5314,16 @@ dependencies = [
[[package]]
name = "zip"
version = "0.6.6"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
checksum = "f1f4a27345eb6f7aa7bd015ba7eb4175fa4e1b462a29874b779e0bbcf96c6ac7"
dependencies = [
"byteorder",
"arbitrary",
"crc32fast",
"crossbeam-utils",
"displaydoc",
"indexmap 2.2.5",
"thiserror",
]
[[package]]

View File

@ -43,7 +43,7 @@ url = { version = "2.5.0", features = ["serde"] }
uuid = { version = "1.8.0", features = ["v4", "js", "serde"] }
validator = { version = "0.18.1", features = ["derive"] }
winnow = "0.5.40"
zip = { version = "0.6.6", default-features = false }
zip = { version = "1.3.0", default-features = false }
[target.'cfg(target_arch = "wasm32")'.dependencies]
js-sys = { version = "0.3.69" }

View File

@ -0,0 +1,20 @@
# KCL
Our language for defining geometry and working with our Geometry Engine efficiently. Short for KittyCAD Language, named after our Design API.
## Contributing a standard library function
We've built a lot of tooling to make contributing to KCL easier. If you are interested in contributing a new standard library function to KCL, here is the rough process:
1. Open just the wasm-lib folder in your editor of choice. VS Code, for example, struggles to run rust-analyzer on the entire modeling-app directory because it's such a turducken of TS and Rust code.
2. Find the definition for similar standard library functions in `./kcl/src/std` and place your new one near it or in the same category file.
3. Add your new code. A new standard library function consists of:
4. A `pub async` of the actual standard library function in Rust
5. A doc comment block containing at least one example using your new standard library function (the Rust compiler will error if you don't provide an example our teammates are dope)
6. A `stdlib` macro providing the name that will need to be written by KCL users to use the function (this is usually a camelCase version of your Rust implementation, which is named with snake_case)
7. An inner function that is published only to the crate
8. Add your new standard library function to [the long list of CORE_FNS in mod.rs](https://github.com/KittyCAD/modeling-app/blob/main/src/wasm-lib/kcl/src/std/mod.rs#L42)
9. Get a production Zoo dev token and run `export KITTYCAD_API_TOKEN=your-token-here` in a terminal
10. Run `TWENTY_TWENTY=overwrite cargo nextest run --workspace --no-fail-fast` to take snapshot tests of your example code running in the engine
11. Run `EXPECTORATE=overwrite cargo test --all generate_stdlib -- --nocapture` to generate new Markdown documentation for your function that will be used [to generate docs on our website](https://zoo.dev/docs/kcl).
12. Create a PR in GitHub.

View File

@ -1214,7 +1214,11 @@ impl CallExpression {
let func = memory.get(&fn_name, self.into())?;
let result = func
.call_fn(fn_args, memory.clone(), ctx.clone())
.await?
.await
.map_err(|e| {
// Add the call expression to the source ranges.
e.add_source_ranges(vec![self.into()])
})?
.ok_or_else(|| {
KclError::UndefinedValue(KclErrorDetails {
message: format!("Result of user-defined function {} is undefined", fn_name),

View File

@ -142,6 +142,25 @@ impl KclError {
new
}
pub fn add_source_ranges(&self, source_ranges: Vec<SourceRange>) -> Self {
let mut new = self.clone();
match &mut new {
KclError::Lexical(e) => e.source_ranges.extend(source_ranges),
KclError::Syntax(e) => e.source_ranges.extend(source_ranges),
KclError::Semantic(e) => e.source_ranges.extend(source_ranges),
KclError::Type(e) => e.source_ranges.extend(source_ranges),
KclError::Unimplemented(e) => e.source_ranges.extend(source_ranges),
KclError::Unexpected(e) => e.source_ranges.extend(source_ranges),
KclError::ValueAlreadyDefined(e) => e.source_ranges.extend(source_ranges),
KclError::UndefinedValue(e) => e.source_ranges.extend(source_ranges),
KclError::InvalidExpression(e) => e.source_ranges.extend(source_ranges),
KclError::Engine(e) => e.source_ranges.extend(source_ranges),
KclError::Internal(e) => e.source_ranges.extend(source_ranges),
}
new
}
}
/// This is different than to_string() in that it will serialize the Error

View File

@ -498,14 +498,13 @@ impl Backend {
for (entry, value) in self.code_map.inner().await.iter() {
let file_name = entry.replace("file://", "").to_string();
let options = zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Stored);
let options = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
zip.start_file(file_name, options)?;
zip.write_all(value)?;
}
// Apply the changes you've made.
// Dropping the `ZipWriter` will have the same effect, but may silently fail
zip.finish()?;
drop(zip);
Ok(buf)
}

View File

@ -533,3 +533,38 @@ pub async fn to_degrees(args: Args) -> Result<MemoryItem, KclError> {
fn inner_to_degrees(num: f64) -> Result<f64, KclError> {
Ok(num.to_degrees())
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn test_inner_max() {
let nums = vec![4.0, 5.0, 6.0];
let result = inner_max(nums);
assert_eq!(result, 6.0);
}
#[test]
fn test_inner_max_with_neg() {
let nums = vec![4.0, -5.0];
let result = inner_max(nums);
assert_eq!(result, 4.0);
}
#[test]
fn test_inner_min() {
let nums = vec![4.0, 5.0, 6.0];
let result = inner_min(nums);
assert_eq!(result, 4.0);
}
#[test]
fn test_inner_min_with_neg() {
let nums = vec![4.0, -5.0];
let result = inner_min(nums);
assert_eq!(result, -5.0);
}
}

View File

@ -68,6 +68,9 @@ lazy_static! {
Box::new(crate::std::sketch::StartSketchAt),
Box::new(crate::std::sketch::StartSketchOn),
Box::new(crate::std::sketch::StartProfileAt),
Box::new(crate::std::sketch::ProfileStartX),
Box::new(crate::std::sketch::ProfileStartY),
Box::new(crate::std::sketch::ProfileStart),
Box::new(crate::std::sketch::Close),
Box::new(crate::std::sketch::Arc),
Box::new(crate::std::sketch::TangentialArc),

View File

@ -12,7 +12,7 @@ use crate::{
errors::{KclError, KclErrorDetails},
executor::{
BasePath, ExtrudeGroup, ExtrudeSurface, Face, GeoMeta, MemoryItem, Path, Plane, PlaneType, Point2d, Point3d,
Position, Rotation, SketchGroup, SketchGroupSet, SketchSurface, SourceRange,
Position, Rotation, SketchGroup, SketchGroupSet, SketchSurface, SourceRange, UserVal,
},
std::{
utils::{
@ -995,7 +995,14 @@ async fn start_sketch_on_face(
args: Args,
) -> Result<Box<Face>, KclError> {
let extrude_plane_id = match tag {
SketchOnFaceTag::String(ref s) => extrude_group
SketchOnFaceTag::String(ref s) => {
if s.is_empty() {
return Err(KclError::Type(KclErrorDetails {
message: "Expected a non-empty tag for the face to sketch on".to_string(),
source_ranges: vec![args.source_range],
}));
}
extrude_group
.value
.iter()
.find_map(|extrude_surface| match extrude_surface {
@ -1015,7 +1022,8 @@ async fn start_sketch_on_face(
message: format!("Expected a face with the tag `{}`", tag),
source_ranges: vec![args.source_range],
})
})??,
})??
}
SketchOnFaceTag::StartOrEnd(StartOrEnd::Start) => extrude_group.start_cap_id.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: "Expected a start face to sketch on".to_string(),
@ -1206,6 +1214,78 @@ pub(crate) async fn inner_start_profile_at(
Ok(Box::new(sketch_group))
}
/// Returns the X component of the sketch profile start point.
pub async fn profile_start_x(args: Args) -> Result<MemoryItem, KclError> {
let sketch_group: Box<SketchGroup> = args.get_sketch_group()?;
let x = inner_profile_start_x(sketch_group)?;
args.make_user_val_from_f64(x)
}
/// ```no_run
/// const sketch001 = startSketchOn('XY')
/// |> startProfileAt([5, 2], %)
/// |> angledLine([-26.6, 50], %)
/// |> angledLine([90, 50], %)
/// |> angledLineToX({ angle: 30, to: profileStartX(%) }, %)
/// ```
#[stdlib {
name = "profileStartX"
}]
pub(crate) fn inner_profile_start_x(sketch_group: Box<SketchGroup>) -> Result<f64, KclError> {
Ok(sketch_group.start.to[0])
}
/// Returns the Y component of the sketch profile start point.
pub async fn profile_start_y(args: Args) -> Result<MemoryItem, KclError> {
let sketch_group: Box<SketchGroup> = args.get_sketch_group()?;
let x = inner_profile_start_y(sketch_group)?;
args.make_user_val_from_f64(x)
}
/// ```no_run
/// const sketch001 = startSketchOn('XY')
/// |> startProfileAt([5, 2], %)
/// |> angledLine({ angle: -60, length: 14 }, %)
/// |> angledLineToY({ angle: 30, to: profileStartY(%) }, %)
/// ```
#[stdlib {
name = "profileStartY"
}]
pub(crate) fn inner_profile_start_y(sketch_group: Box<SketchGroup>) -> Result<f64, KclError> {
Ok(sketch_group.start.to[1])
}
/// Returns the sketch profile start point.
pub async fn profile_start(args: Args) -> Result<MemoryItem, KclError> {
let sketch_group: Box<SketchGroup> = args.get_sketch_group()?;
let point = inner_profile_start(sketch_group)?;
Ok(MemoryItem::UserVal(UserVal {
value: serde_json::to_value(point).map_err(|e| {
KclError::Type(KclErrorDetails {
message: format!("Failed to convert point to json: {}", e),
source_ranges: vec![args.source_range],
})
})?,
meta: Default::default(),
}))
}
/// ```no_run
/// const sketch001 = startSketchOn('XY')
/// |> startProfileAt([5, 2], %)
/// |> angledLine({ angle: 120, length: 50 }, %, 'seg01')
/// |> angledLine({ angle: segAng('seg01', %) + 120, length: 50 }, %)
/// |> lineTo(profileStart(%), %)
/// |> close(%)
/// |> extrude(20, %)
/// ```
#[stdlib {
name = "profileStart"
}]
pub(crate) fn inner_profile_start(sketch_group: Box<SketchGroup>) -> Result<[f64; 2], KclError> {
Ok(sketch_group.start.to)
}
/// Close the current sketch.
pub async fn close(args: Args) -> Result<MemoryItem, KclError> {
let (sketch_group, tag): (Box<SketchGroup>, Option<String>) = args.get_sketch_group_and_optional_tag()?;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

View File

@ -2057,3 +2057,58 @@ const bracket = startSketchOn('XY')
r#"engine: KclErrorDetails { source_ranges: [SourceRange([1443, 1443])], message: "Modeling command failed: Some([ApiError { error_code: BadRequest, message: \"Fillet failed\" }])" }"#
);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_error_empty_start_sketch_on_string() {
let code = r#"const part001 = startSketchOn('-XZ')
|> startProfileAt([75.75, 184.25], %)
|> line([190.03, -118.13], %)
|> line([-33.38, -202.86], %)
|> line([-315.86, -64.2], %)
|> tangentialArcTo([-147.66, 121.34], %)
|> close(%)
|> extrude(100, %)
const secondSketch = startSketchOn(part001, '')
|> circle([-20, 50], 40, %)
|> extrude(20, %)
"#;
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm).await;
assert!(result.is_err());
assert_eq!(
result.err().unwrap().to_string(),
r#"type: KclErrorDetails { source_ranges: [SourceRange([272, 298])], message: "Expected a non-empty tag for the face to sketch on" }"#
);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_error_user_function_wrong_args() {
let code = r#"const length = .750
const width = 0.500
const height = 0.500
const dia = 4
fn squareHole = (l, w) => {
const squareHoleSketch = startSketchOn('XY')
|> startProfileAt([-width / 2, -length / 2], %)
|> lineTo([width / 2, -length / 2], %)
|> lineTo([width / 2, length / 2], %)
|> lineTo([-width / 2, length / 2], %)
|> close(%)
return squareHoleSketch
}
const extrusion = startSketchOn('XY')
|> circle([0, 0], dia/2, %)
|> hole(squareHole(length, width, height), %)
|> extrude(height, %)
"#;
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm).await;
assert!(result.is_err());
assert_eq!(
result.err().unwrap().to_string(),
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([92, 364]), SourceRange([444, 477])], message: "Expected 2 arguments, got 3" }"#
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 KiB

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

137
yarn.lock
View File

@ -1970,10 +1970,10 @@
"@react-hook/latest" "^1.0.2"
"@react-hook/passive-layout-effect" "^1.2.0"
"@remix-run/router@1.15.3":
version "1.15.3"
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.15.3.tgz#d2509048d69dbb72d5389a14945339f1430b2d3c"
integrity sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w==
"@remix-run/router@1.16.1":
version "1.16.1"
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.16.1.tgz#73db3c48b975eeb06d0006481bde4f5f2d17d1cd"
integrity sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==
"@replit/codemirror-interact@^6.3.1":
version "6.3.1"
@ -2106,6 +2106,11 @@
resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.2.0.tgz#874d36135e4badce2719e7bdc556ce240cbaff14"
integrity sha512-P5XgYoAw/vfW65byBbJQCw+cagdXDT/qH6wmABiLt4v4YBT2q2vqCOhihe+D1Nt325F/S/0Tkv6C5z0Lv+VBQQ==
"@tauri-apps/api@2.0.0-beta.11":
version "2.0.0-beta.11"
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.0.0-beta.11.tgz#7ec6f6f007da4e55315270619448ce39fd11b4e3"
integrity sha512-wJRY+fBUm3KpqZDHMIz5HRv+1vlnvRJ/dFxiyY3NlINTx2qXqDou5qWYcP1CuZXsd39InWVPV3FAZvno/kGCkA==
"@tauri-apps/api@2.0.0-beta.4":
version "2.0.0-beta.4"
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.0.0-beta.4.tgz#7688950f6e03f38b3bac73585f8f4cdd61be6aa6"
@ -2189,12 +2194,12 @@
dependencies:
"@tauri-apps/api" "2.0.0-beta.4"
"@tauri-apps/plugin-fs@^2.0.0-beta.2":
version "2.0.0-beta.2"
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-fs/-/plugin-fs-2.0.0-beta.2.tgz#b2dfcd72422f778e4c32edcfad24c1e96299c760"
integrity sha512-jqeRBrm0h9QUoep5OzHx5R0vgFCYVAmZIy45jJpR7hHvnEgUwDU8JLUUVPvWniq6tUtxjwr1V/a0Hm9pE9V+NQ==
"@tauri-apps/plugin-fs@^2.0.0-beta.3":
version "2.0.0-beta.3"
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-fs/-/plugin-fs-2.0.0-beta.3.tgz#461628704a10ce5177c49f3f65153aa9ecd8df3b"
integrity sha512-LBgA7S10NwcitHaugIfmCSkewz45vSz1VOpMHhzvE38i1r1KpuTSHlr3MZ0LLq93tH/lvhYZ+3LAml4Sriwthw==
dependencies:
"@tauri-apps/api" "2.0.0-beta.4"
"@tauri-apps/api" "2.0.0-beta.11"
"@tauri-apps/plugin-http@^2.0.0-beta.2":
version "2.0.0-beta.2"
@ -2224,12 +2229,12 @@
dependencies:
"@tauri-apps/api" "2.0.0-beta.4"
"@tauri-apps/plugin-updater@^2.0.0-beta.2":
version "2.0.0-beta.2"
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-updater/-/plugin-updater-2.0.0-beta.2.tgz#60002e54ad647a56db5e1b0b54e792f399d425a4"
integrity sha512-T8EkAXawbyV/6/Lcf1VVIWhtGuals63zKn+udYNqlC8CRM5iYQ+8bM8Nmy2E+pIzkkx93d1t6/8geFitLZPmKw==
"@tauri-apps/plugin-updater@^2.0.0-beta.3":
version "2.0.0-beta.3"
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-updater/-/plugin-updater-2.0.0-beta.3.tgz#d6403363e540005e4bd38ff8d0cd689f40db96d0"
integrity sha512-bD1ikPz80uK9YJKNYpYlA6StSp9lr0Ob1kGLG2XdmOgspv7SZLLNVzMORtKeqgepxwG99qdYGDDegT3Ll6+UlA==
dependencies:
"@tauri-apps/api" "2.0.0-beta.4"
"@tauri-apps/api" "2.0.0-beta.11"
"@testing-library/dom@^10.0.0":
version "10.0.0"
@ -2720,44 +2725,44 @@
"@types/babel__core" "^7.20.5"
react-refresh "^0.14.0"
"@vitest/expect@1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-1.5.0.tgz#961190510a2723bd4abf5540bcec0a4dfd59ef14"
integrity sha512-0pzuCI6KYi2SIC3LQezmxujU9RK/vwC1U9R0rLuGlNGcOuDWxqWKu6nUdFsX9tH1WU0SXtAxToOsEjeUn1s3hA==
"@vitest/expect@1.6.0":
version "1.6.0"
resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-1.6.0.tgz#0b3ba0914f738508464983f4d811bc122b51fb30"
integrity sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==
dependencies:
"@vitest/spy" "1.5.0"
"@vitest/utils" "1.5.0"
"@vitest/spy" "1.6.0"
"@vitest/utils" "1.6.0"
chai "^4.3.10"
"@vitest/runner@1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-1.5.0.tgz#1f7cb78ee4064e73e53d503a19c1b211c03dfe0c"
integrity sha512-7HWwdxXP5yDoe7DTpbif9l6ZmDwCzcSIK38kTSIt6CFEpMjX4EpCgT6wUmS0xTXqMI6E/ONmfgRKmaujpabjZQ==
"@vitest/runner@1.6.0":
version "1.6.0"
resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-1.6.0.tgz#a6de49a96cb33b0e3ba0d9064a3e8d6ce2f08825"
integrity sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==
dependencies:
"@vitest/utils" "1.5.0"
"@vitest/utils" "1.6.0"
p-limit "^5.0.0"
pathe "^1.1.1"
"@vitest/snapshot@1.5.0", "@vitest/snapshot@^1.2.2":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-1.5.0.tgz#cd2d611fd556968ce8fb6b356a09b4593c525947"
integrity sha512-qpv3fSEuNrhAO3FpH6YYRdaECnnRjg9VxbhdtPwPRnzSfHVXnNzzrpX4cJxqiwgRMo7uRMWDFBlsBq4Cr+rO3A==
"@vitest/snapshot@1.6.0", "@vitest/snapshot@^1.2.2":
version "1.6.0"
resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-1.6.0.tgz#deb7e4498a5299c1198136f56e6e0f692e6af470"
integrity sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==
dependencies:
magic-string "^0.30.5"
pathe "^1.1.1"
pretty-format "^29.7.0"
"@vitest/spy@1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-1.5.0.tgz#1369a1bec47f46f18eccfa45f1e8fbb9b5e15e77"
integrity sha512-vu6vi6ew5N5MMHJjD5PoakMRKYdmIrNJmyfkhRpQt5d9Ewhw9nZ5Aqynbi3N61bvk9UvZ5UysMT6ayIrZ8GA9w==
"@vitest/spy@1.6.0":
version "1.6.0"
resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-1.6.0.tgz#362cbd42ccdb03f1613798fde99799649516906d"
integrity sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==
dependencies:
tinyspy "^2.2.0"
"@vitest/utils@1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-1.5.0.tgz#90c9951f4516f6d595da24876b58e615f6c99863"
integrity sha512-BDU0GNL8MWkRkSRdNFvCUCAVOeHaUlVJ9Tx0TYBZyXaaOTmGtUFObzchCivIBrIwKzvZA7A9sCejVhXM2aY98A==
"@vitest/utils@1.6.0":
version "1.6.0"
resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-1.6.0.tgz#5c5675ca7d6f546a7b4337de9ae882e6c57896a1"
integrity sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==
dependencies:
diff-sequences "^29.6.3"
estree-walker "^3.0.3"
@ -7614,20 +7619,20 @@ react-refresh@^0.14.0:
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.0.tgz#4e02825378a5f227079554d4284889354e5f553e"
integrity sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==
react-router-dom@^6.22.3:
version "6.22.3"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.22.3.tgz#9781415667fd1361a475146c5826d9f16752a691"
integrity sha512-7ZILI7HjcE+p31oQvwbokjk6OA/bnFxrhJ19n82Ex9Ph8fNAq+Hm/7KchpMGlTgWhUxRHMMCut+vEtNpWpowKw==
react-router-dom@^6.23.1:
version "6.23.1"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.23.1.tgz#30cbf266669693e9492aa4fc0dde2541ab02322f"
integrity sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ==
dependencies:
"@remix-run/router" "1.15.3"
react-router "6.22.3"
"@remix-run/router" "1.16.1"
react-router "6.23.1"
react-router@6.22.3:
version "6.22.3"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.22.3.tgz#9d9142f35e08be08c736a2082db5f0c9540a885e"
integrity sha512-dr2eb3Mj5zK2YISHK++foM9w4eBnO23eKnZEDs7c880P6oKbrjz/Svg9+nxqtHQK+oMW4OtjZca0RqPglXxguQ==
react-router@6.23.1:
version "6.23.1"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.23.1.tgz#d08cbdbd9d6aedc13eea6e94bc6d9b29cb1c4be9"
integrity sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ==
dependencies:
"@remix-run/router" "1.15.3"
"@remix-run/router" "1.16.1"
react-textarea-autosize@^8.3.2:
version "8.5.2"
@ -8469,10 +8474,10 @@ thenify-all@^1.0.0:
dependencies:
any-promise "^1.0.0"
three@^0.163.0:
version "0.163.0"
resolved "https://registry.yarnpkg.com/three/-/three-0.163.0.tgz#cbfefbfd64a1353ab7cc8bf0fc396ddca1875a49"
integrity sha512-HlMgCb2TF/dTLRtknBnjUTsR8FsDqBY43itYop2+Zg822I+Kd0Ua2vs8CvfBVefXkBdNDrLMoRTGCIIpfCuDew==
three@^0.164.1:
version "0.164.1"
resolved "https://registry.yarnpkg.com/three/-/three-0.164.1.tgz#b742f76bd8dfd3736ba0d86a12dfddb73c5cdcc0"
integrity sha512-iC/hUBbl1vzFny7f5GtqzVXYjMJKaTPxiCxXfrvVdBi1Sf+jhd1CAkitiFwC7mIBFCo3MrDLJG97yisoaWig0w==
through@^2.3.8:
version "2.3.8"
@ -8857,10 +8862,10 @@ validate-npm-package-license@^3.0.4:
spdx-correct "^3.0.0"
spdx-expression-parse "^3.0.0"
vite-node@1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-1.5.0.tgz#7f74dadfecb15bca016c5ce5ef85e5cc4b82abf2"
integrity sha512-tV8h6gMj6vPzVCa7l+VGq9lwoJjW8Y79vst8QZZGiuRAfijU+EEWuc0kFpmndQrWhMMhet1jdSF+40KSZUqIIw==
vite-node@1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-1.6.0.tgz#2c7e61129bfecc759478fa592754fd9704aaba7f"
integrity sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==
dependencies:
cac "^6.7.14"
debug "^4.3.4"
@ -8910,16 +8915,16 @@ vitest-webgl-canvas-mock@^1.1.0:
cssfontparser "^1.2.1"
parse-color "^1.0.0"
vitest@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/vitest/-/vitest-1.5.0.tgz#6ebb396bd358650011a9c96c18fa614b668365c1"
integrity sha512-d8UKgR0m2kjdxDWX6911uwxout6GHS0XaGH1cksSIVVG8kRlE7G7aBw7myKQCvDI5dT4j7ZMa+l706BIORMDLw==
vitest@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/vitest/-/vitest-1.6.0.tgz#9d5ad4752a3c451be919e412c597126cffb9892f"
integrity sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==
dependencies:
"@vitest/expect" "1.5.0"
"@vitest/runner" "1.5.0"
"@vitest/snapshot" "1.5.0"
"@vitest/spy" "1.5.0"
"@vitest/utils" "1.5.0"
"@vitest/expect" "1.6.0"
"@vitest/runner" "1.6.0"
"@vitest/snapshot" "1.6.0"
"@vitest/spy" "1.6.0"
"@vitest/utils" "1.6.0"
acorn-walk "^8.3.2"
chai "^4.3.10"
debug "^4.3.4"
@ -8933,7 +8938,7 @@ vitest@^1.5.0:
tinybench "^2.5.1"
tinypool "^0.8.3"
vite "^5.0.0"
vite-node "1.5.0"
vite-node "1.6.0"
why-is-node-running "^2.2.2"
vscode-jsonrpc@8.2.0, vscode-jsonrpc@^8.1.0: