Compare commits

...

12 Commits

Author SHA1 Message Date
f97bdaf8b7 Release KCL 79 (#7347) 2025-06-03 22:04:56 -04:00
3f3693e12d Type ascription produces two incompatible fields (#7355)
# Symptoms

This code produces a big ugly confusing error in the frontend, see #7340.

# Root cause

I added a new test case, with an unknown type. In `ast.snap` under `body[0].declaration.init.ty` there two different `type` fields in the AST node for the type's name, and they have conflicting values Primitive and Identifier.

<img width="602" alt="Screenshot 2025-06-03 at 4 04 55 PM" src="https://github.com/user-attachments/assets/913a0fa0-3e8d-473f-bb64-003d44915be0" />

# Solution

Change the `enum PrimitiveType` variant from `Named(Node<Identifier>)` to `Named { name: Node<Identifier> }` so that the fields nest differently.

Now the error correctly points out to the user that the type `NotARealType` can't be found. Much better error message that shows the user the problem.

# Alternative solutions

Stop the duplicated JSON fields altogether. I tried this previously in https://github.com/KittyCAD/modeling-app/pull/4369 but it was very involved, and I didn't think it was worth it. Maybe I should reopen that PR and solve this properly.

Closes #7340
2025-06-03 20:05:40 -04:00
73660d1db8 Bump criterion from 0.5.1 to 0.6.0 (#7357) 2025-06-04 09:34:34 +10:00
c373f33507 KCL: Fix format 2025-06-04 09:04:35 +10:00
2dc76a71cc Stabilize test: "Can edit a sketch with multiple profiles, dragging segments to edit them, and adding one new profile" (#7356) 2025-06-03 18:20:06 -04:00
ce42966f2b Upgrade to Rust 1.87 (#7346)
I ignored some new clippy lints about large differences between enum variants.
We can always revisit these later (the compiler suggests boxing them so
that the enum variants are similar size)
2025-06-03 17:32:24 -04:00
b47b9c9613 KCL: Emit proper errors in unexpected edge cases (#7351)
There's some bug in the frontend or KCL somewhere, which results in the TypeScript frontend sending an AST (serialized to JSON) to the KCL executor, but the JSON cannot be deserialized into an AST. If this happens, it's a bug in ZDS, not a user error. 

The problem is that this sort of error will cause the frontend to silently stop rendering KCL, and it won't show the user any errors. They need to open up the console and look at the error there, and even if they do, it's hard to understand.

This PR changes how we report these unexpected errors due to bugs in ZDS. ZDS should not silently stop working, it should at least print a half-decent error like this:

<img width="527" alt="nicer error" src="https://github.com/user-attachments/assets/1bb37a64-0915-4472-849c-d146f397356b" />

## Fix

Right now, the wasm library exports a function `execute`. It previous returned an error as a String if one occurred. The frontend assumed this error string would be JSON that matched the schema `KclErrorWithOutputs`. This was not always true! For example, if something couldn't be serialized to JSON, we'd take the raw Serde error and stringify that. It wouldn't match `KclErrorWithOutputs`.

Now I've changed `execute` so that if it errors, it'll returns a JsValue not a string. So that's one check (can this string be deserialized into a JSON object) that can be removed -- it'll return a JSON object directly now. The next check is "does this JSON object conform to the KclErrorWithOutputs schema". To prove that's correct, I changed `execute` to be a thin wrapper around `fn execute_typed` which returns `Result<ExecOutcome, KclErrorWithOutputs>`. Now we know the error will be the right type.
2025-06-03 15:37:17 -05:00
2af2144f89 Add query parameter to skip the embedded "sign in" view (#7352)
Add query parameter to skip sign-in view if not necessary
2025-06-03 16:23:23 -04:00
bd37c488ee Fix trackball, finally add an E2E test for it (#7341)
* Fix orbit style setting not updating in camControls

* Break apart camera movement tests, add trackball to orbit one

* I don't think zoom was actually testing changes, this fixes that

* test refactor: pass in expected cam pos, not its inverse

* Lints

* Lint fix broke the test, fix fix

* Gah biome whyyy did you format other test names like that?
2025-06-03 14:56:19 -04:00
a3551e4b2f Bump csscolorparser from 0.7.0 to 0.7.2 (#7344) 2025-06-03 11:50:09 -05:00
d3979edb41 KCL: Better error message when using var in its own definition (#7339)
I thought I did this in https://github.com/KittyCAD/modeling-app/pull/7325, but I forgot to actually set the better message.

Actually fixes, for real this time, https://github.com/KittyCAD/modeling-app/issues/6072 this time.
2025-06-03 16:46:28 +00:00
095a7a575b Bump tokio-tungstenite from 0.24.0 to 0.26.2 (#7329) 2025-06-03 10:16:22 -05:00
49 changed files with 714 additions and 284 deletions

View File

@ -2362,7 +2362,7 @@ profile004 = circleThreePoint(sketch001, p1 = [13.44, -6.8], p2 = [13.39, -2.07]
await test.step('add new profile', async () => {
await toolbar.rectangleBtn.click()
await page.waitForTimeout(100)
await page.waitForTimeout(200)
await rectStart()
await editor.expectEditor.toContain(
`profile005 = startProfile(sketch001, at = [15.68, -3.84])`

View File

@ -3,187 +3,250 @@ import { uuidv4 } from '@src/lib/utils'
import { getUtils } from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test'
import type { Page } from '@playwright/test'
import type { SceneFixture } from '@e2e/playwright/fixtures/sceneFixture'
test.describe('Testing Camera Movement', () => {
test('Can move camera reliably', async ({
/**
* hack that we're implemented our own retry instead of using retries built into playwright.
* however each of these camera drags can be flaky, because of udp
* and so putting them together means only one needs to fail to make this test extra flaky.
* this way we can retry within the test
* We could break them out into separate tests, but the longest past of the test is waiting
* for the stream to start, so it can be good to bundle related things together.
*/
const bakeInRetries = async ({
mouseActions,
afterPosition,
beforePosition,
retryCount = 0,
page,
context,
homePage,
scene,
}: {
mouseActions: () => Promise<void>
beforePosition: [number, number, number]
afterPosition: [number, number, number]
retryCount?: number
page: Page
scene: SceneFixture
}) => {
const acceptableCamError = 5
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await scene.connectionEstablished()
await test.step('Set up initial camera position', async () =>
await scene.moveCameraTo({
x: beforePosition[0],
y: beforePosition[1],
z: beforePosition[2],
}))
await u.openAndClearDebugPanel()
await u.closeKclCodePanel()
const camPos: [number, number, number] = [0, 85, 85]
const bakeInRetries = async (
mouseActions: any,
xyz: [number, number, number],
cnt = 0
) => {
// hack that we're implemented our own retry instead of using retries built into playwright.
// however each of these camera drags can be flaky, because of udp
// and so putting them together means only one needs to fail to make this test extra flaky.
// this way we can retry within the test
// We could break them out into separate tests, but the longest past of the test is waiting
// for the stream to start, so it can be good to bundle related things together.
const camCommand: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center: { x: 0, y: 0, z: 0 },
vantage: { x: camPos[0], y: camPos[1], z: camPos[2] },
up: { x: 0, y: 0, z: 1 },
},
}
const updateCamCommand: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
}
await u.sendCustomCmd(camCommand)
await page.waitForTimeout(100)
await u.sendCustomCmd(updateCamCommand)
await page.waitForTimeout(100)
// rotate
await u.closeDebugPanel()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(100)
// const yo = page.getByTestId('cam-x-position').inputValue()
await u.doAndWaitForImageDiff(async () => {
await test.step('Do actions and watch for changes', async () =>
u.doAndWaitForImageDiff(async () => {
await mouseActions()
await u.openAndClearDebugPanel()
await u.closeDebugPanel()
await page.waitForTimeout(100)
}, 300)
}, 300))
await u.openAndClearDebugPanel()
await expect(page.getByTestId('cam-x-position')).toBeAttached()
const vals = await Promise.all([
page.getByTestId('cam-x-position').inputValue(),
page.getByTestId('cam-y-position').inputValue(),
page.getByTestId('cam-z-position').inputValue(),
])
const errors = vals.map((v, i) => Math.abs(Number(v) - afterPosition[i]))
let shouldRetry = false
if (errors.some((e) => e > acceptableCamError)) {
if (retryCount > 2) {
console.log('xVal', vals[0], 'xError', errors[0])
console.log('yVal', vals[1], 'yError', errors[1])
console.log('zVal', vals[2], 'zError', errors[2])
throw new Error('Camera position not as expected', {
cause: {
vals,
errors,
},
})
}
shouldRetry = true
}
if (shouldRetry) {
await bakeInRetries({
mouseActions,
afterPosition: afterPosition,
beforePosition: beforePosition,
retryCount: retryCount + 1,
page,
scene,
})
}
}
test(
'Can pan and zoom camera reliably',
{
tag: '@web',
},
async ({ page, homePage, scene, cmdBar }) => {
const u = await getUtils(page)
const camInitialPosition: [number, number, number] = [0, 85, 85]
await homePage.goToModelingScene()
await scene.settled(cmdBar)
await u.openAndClearDebugPanel()
await page.getByTestId('cam-x-position').isVisible()
await u.closeKclCodePanel()
const vals = await Promise.all([
page.getByTestId('cam-x-position').inputValue(),
page.getByTestId('cam-y-position').inputValue(),
page.getByTestId('cam-z-position').inputValue(),
])
const xError = Math.abs(Number(vals[0]) + xyz[0])
const yError = Math.abs(Number(vals[1]) + xyz[1])
const zError = Math.abs(Number(vals[2]) + xyz[2])
await test.step('Pan', async () => {
await bakeInRetries({
mouseActions: async () => {
await page.keyboard.down('Shift')
await page.mouse.move(600, 200)
await page.mouse.down({ button: 'right' })
// Gotcha: remove steps:2 from this 700,200 mouse move. This bricked the test on local host engine.
await page.mouse.move(700, 200)
await page.mouse.up({ button: 'right' })
await page.keyboard.up('Shift')
await page.waitForTimeout(200)
},
afterPosition: [19, 85, 85],
beforePosition: camInitialPosition,
page,
scene,
})
})
let shouldRetry = false
await test.step('Zoom with click and drag', async () => {
await bakeInRetries({
mouseActions: async () => {
await page.keyboard.down('Control')
await page.mouse.move(700, 400)
await page.mouse.down({ button: 'right' })
await page.mouse.move(700, 300)
await page.mouse.up({ button: 'right' })
await page.keyboard.up('Control')
},
afterPosition: [0, 118, 118],
beforePosition: camInitialPosition,
page,
scene,
})
})
if (xError > 5 || yError > 5 || zError > 5) {
if (cnt > 2) {
console.log('xVal', vals[0], 'xError', xError)
console.log('yVal', vals[1], 'yError', yError)
console.log('zVal', vals[2], 'zError', zError)
throw new Error('Camera position not as expected')
await test.step('Zoom with scrollwheel', async () => {
const refreshCamValuesCmd: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
}
shouldRetry = true
}
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await page.waitForTimeout(100)
if (shouldRetry) await bakeInRetries(mouseActions, xyz, cnt + 1)
await bakeInRetries({
mouseActions: async () => {
await page.mouse.move(700, 400)
await page.mouse.wheel(0, -150)
// Scroll zooming doesn't update the debug pane's cam position values,
// so we have to force a refresh.
await u.openAndClearDebugPanel()
await u.sendCustomCmd(refreshCamValuesCmd)
await u.waitForCmdReceive('default_camera_get_settings')
await u.closeDebugPanel()
},
afterPosition: [0, 42.5, 42.5],
beforePosition: camInitialPosition,
page,
scene,
})
})
}
await bakeInRetries(async () => {
await page.mouse.move(700, 200)
await page.mouse.down({ button: 'right' })
await page.waitForTimeout(100)
)
const appLogoBBox = await page.getByTestId('app-logo').boundingBox()
expect(appLogoBBox).not.toBeNull()
if (!appLogoBBox) throw new Error('app logo not found')
await page.mouse.move(
appLogoBBox.x + appLogoBBox.width / 2,
appLogoBBox.y + appLogoBBox.height / 2
)
await page.waitForTimeout(100)
await page.mouse.move(600, 303)
await page.waitForTimeout(100)
await page.mouse.up({ button: 'right' })
}, [4, -10.5, -120])
test(
'Can orbit camera reliably',
{
tag: '@web',
},
async ({ page, homePage, scene, cmdBar }) => {
const u = await getUtils(page)
const initialCamPosition: [number, number, number] = [0, 85, 85]
await bakeInRetries(async () => {
await page.keyboard.down('Shift')
await page.mouse.move(600, 200)
await page.mouse.down({ button: 'right' })
// Gotcha: remove steps:2 from this 700,200 mouse move. This bricked the test on local host engine.
await page.mouse.move(700, 200)
await page.mouse.up({ button: 'right' })
await page.keyboard.up('Shift')
}, [-19, -85, -85])
await homePage.goToModelingScene()
// this turns on the debug pane setting as well
await scene.settled(cmdBar)
const camCommand: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center: { x: 0, y: 0, z: 0 },
vantage: { x: camPos[0], y: camPos[1], z: camPos[2] },
up: { x: 0, y: 0, z: 1 },
},
await u.openAndClearDebugPanel()
await u.closeKclCodePanel()
await test.step('Test orbit with spherical mode', async () => {
await bakeInRetries({
mouseActions: async () => {
await page.mouse.move(700, 200)
await page.mouse.down({ button: 'right' })
await page.waitForTimeout(100)
const appLogoBBox = await page.getByTestId('app-logo').boundingBox()
expect(appLogoBBox).not.toBeNull()
if (!appLogoBBox) throw new Error('app logo not found')
await page.mouse.move(
appLogoBBox.x + appLogoBBox.width / 2,
appLogoBBox.y + appLogoBBox.height / 2
)
await page.waitForTimeout(100)
await page.mouse.move(600, 303)
await page.waitForTimeout(100)
await page.mouse.up({ button: 'right' })
},
afterPosition: [-4, 10.5, 120],
beforePosition: initialCamPosition,
page,
scene,
})
})
await test.step('Test orbit with trackball mode', async () => {
await test.step('Set orbitMode to trackball', async () => {
await cmdBar.openCmdBar()
await cmdBar.selectOption({ name: 'camera orbit' }).click()
await cmdBar.selectOption({ name: 'trackball' }).click()
await expect(
page.getByText(`camera orbit to "trackball"`)
).toBeVisible()
})
await bakeInRetries({
mouseActions: async () => {
await page.mouse.move(700, 200)
await page.mouse.down({ button: 'right' })
await page.waitForTimeout(100)
const appLogoBBox = await page.getByTestId('app-logo').boundingBox()
expect(appLogoBBox).not.toBeNull()
if (!appLogoBBox) {
throw new Error('app logo not found')
}
await page.mouse.move(
appLogoBBox.x + appLogoBBox.width / 2,
appLogoBBox.y + appLogoBBox.height / 2
)
await page.waitForTimeout(100)
await page.mouse.move(600, 303)
await page.waitForTimeout(100)
await page.mouse.up({ button: 'right' })
},
afterPosition: [18.06, -42.79, 110.87],
beforePosition: initialCamPosition,
page,
scene,
})
})
}
const updateCamCommand: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
}
await u.sendCustomCmd(camCommand)
await page.waitForTimeout(100)
await u.sendCustomCmd(updateCamCommand)
await page.waitForTimeout(100)
await u.clearCommandLogs()
await u.closeDebugPanel()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(200)
// zoom
await u.doAndWaitForImageDiff(async () => {
await page.keyboard.down('Control')
await page.mouse.move(700, 400)
await page.mouse.down({ button: 'right' })
await page.mouse.move(700, 300)
await page.mouse.up({ button: 'right' })
await page.keyboard.up('Control')
await u.openDebugPanel()
await page.waitForTimeout(300)
await u.clearCommandLogs()
await u.closeDebugPanel()
}, 300)
// zoom with scroll
await u.openAndClearDebugPanel()
// TODO, it appears we don't get the cam setting back from the engine when the interaction is zoom into `backInRetries` once the information is sent back on zoom
// await expect(Math.abs(Number(await page.getByTestId('cam-x-position').inputValue()) + 12)).toBeLessThan(1.5)
// await expect(Math.abs(Number(await page.getByTestId('cam-y-position').inputValue()) - 85)).toBeLessThan(1.5)
// await expect(Math.abs(Number(await page.getByTestId('cam-z-position').inputValue()) - 85)).toBeLessThan(1.5)
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await bakeInRetries(async () => {
await page.mouse.move(700, 400)
await page.mouse.wheel(0, -100)
}, [0, -85, -85])
})
)
// TODO: fix after electron migration is merged
test('Zoom should be consistent when exiting or entering sketches', async ({

47
rust/Cargo.lock generated
View File

@ -627,26 +627,22 @@ dependencies = [
[[package]]
name = "criterion"
version = "0.5.1"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
checksum = "3bf7af66b0989381bd0be551bd7cc91912a655a58c6918420c9527b1fd8b4679"
dependencies = [
"anes",
"cast",
"ciborium",
"clap",
"criterion-plot",
"futures",
"is-terminal",
"itertools 0.10.5",
"itertools 0.13.0",
"num-traits 0.2.19",
"once_cell",
"oorandom",
"plotters",
"rayon",
"regex",
"serde",
"serde_derive",
"serde_json",
"tinytemplate",
"tokio",
@ -715,9 +711,9 @@ dependencies = [
[[package]]
name = "csscolorparser"
version = "0.7.0"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46f9a16a848a7fb95dd47ce387ac1ee9a6df879ba784b815537fcd388a1a8288"
checksum = "5fda6aace1fbef3aa217b27f4c8d7d071ef2a70a5ca51050b1f17d40299d3f16"
dependencies = [
"phf",
]
@ -1815,7 +1811,7 @@ dependencies = [
[[package]]
name = "kcl-bumper"
version = "0.1.78"
version = "0.1.79"
dependencies = [
"anyhow",
"clap",
@ -1826,7 +1822,7 @@ dependencies = [
[[package]]
name = "kcl-derive-docs"
version = "0.1.78"
version = "0.1.79"
dependencies = [
"Inflector",
"anyhow",
@ -1845,7 +1841,7 @@ dependencies = [
[[package]]
name = "kcl-directory-test-macro"
version = "0.1.78"
version = "0.1.79"
dependencies = [
"convert_case",
"proc-macro2",
@ -1855,7 +1851,7 @@ dependencies = [
[[package]]
name = "kcl-language-server"
version = "0.2.78"
version = "0.2.79"
dependencies = [
"anyhow",
"clap",
@ -1876,7 +1872,7 @@ dependencies = [
[[package]]
name = "kcl-language-server-release"
version = "0.1.78"
version = "0.1.79"
dependencies = [
"anyhow",
"clap",
@ -1896,7 +1892,7 @@ dependencies = [
[[package]]
name = "kcl-lib"
version = "0.2.78"
version = "0.2.79"
dependencies = [
"anyhow",
"approx 0.5.1",
@ -1973,7 +1969,7 @@ dependencies = [
[[package]]
name = "kcl-python-bindings"
version = "0.3.78"
version = "0.3.79"
dependencies = [
"anyhow",
"kcl-lib",
@ -1988,7 +1984,7 @@ dependencies = [
[[package]]
name = "kcl-test-server"
version = "0.1.78"
version = "0.1.79"
dependencies = [
"anyhow",
"hyper 0.14.32",
@ -2001,7 +1997,7 @@ dependencies = [
[[package]]
name = "kcl-to-core"
version = "0.1.78"
version = "0.1.79"
dependencies = [
"anyhow",
"async-trait",
@ -2015,7 +2011,7 @@ dependencies = [
[[package]]
name = "kcl-wasm-lib"
version = "0.1.78"
version = "0.1.79"
dependencies = [
"anyhow",
"bson",
@ -4200,9 +4196,9 @@ dependencies = [
[[package]]
name = "tokio-tungstenite"
version = "0.24.0"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9"
checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084"
dependencies = [
"futures-util",
"log",
@ -4456,21 +4452,20 @@ dependencies = [
[[package]]
name = "tungstenite"
version = "0.24.0"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a"
checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13"
dependencies = [
"byteorder",
"bytes",
"data-encoding",
"http 1.3.1",
"httparse",
"log",
"rand 0.8.5",
"rand 0.9.0",
"rustls",
"rustls-pki-types",
"sha1",
"thiserror 1.0.69",
"thiserror 2.0.12",
"utf-8",
]

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-bumper"
version = "0.1.78"
version = "0.1.79"
edition = "2021"
repository = "https://github.com/KittyCAD/modeling-api"
rust-version = "1.76"

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-derive-docs"
description = "A tool for generating documentation from Rust derive macros"
version = "0.1.78"
version = "0.1.79"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-directory-test-macro"
description = "A tool for generating tests from a directory of kcl files"
version = "0.1.78"
version = "0.1.79"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -1,6 +1,6 @@
[package]
name = "kcl-language-server-release"
version = "0.1.78"
version = "0.1.79"
edition = "2021"
authors = ["KittyCAD Inc <kcl@kittycad.io>"]
publish = false

View File

@ -2,7 +2,7 @@
name = "kcl-language-server"
description = "A language server for KCL."
authors = ["KittyCAD Inc <kcl@kittycad.io>"]
version = "0.2.78"
version = "0.2.79"
edition = "2021"
license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-lib"
description = "KittyCAD Language implementation and tools"
version = "0.2.78"
version = "0.2.79"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"
@ -31,7 +31,7 @@ clap = { version = "4.5.36", default-features = false, optional = true, features
"derive",
] }
convert_case = "0.8.0"
csscolorparser = "0.7.0"
csscolorparser = "0.7.2"
dashmap = { workspace = true }
dhat = { version = "0.3", optional = true }
fnv = "1.0.7"
@ -107,7 +107,7 @@ web-sys = { version = "0.3.76", features = ["console"] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
instant = "0.1.13"
tokio = { workspace = true, features = ["full"] }
tokio-tungstenite = { version = "0.24.0", features = [
tokio-tungstenite = { version = "0.26.2", features = [
"rustls-tls-native-roots",
] }
tower-lsp = { workspace = true, features = ["proposed", "default"] }
@ -130,7 +130,7 @@ tabled = ["dep:tabled"]
[dev-dependencies]
approx = "0.5"
base64 = "0.22.1"
criterion = { version = "0.5.1", features = ["async_tokio"] }
criterion = { version = "0.6.0", features = ["async_tokio"] }
expectorate = "1.1.0"
handlebars = "6.3.2"
image = { version = "0.25.6", default-features = false, features = ["png"] }

View File

@ -1,9 +1,10 @@
use std::{
fs,
hint::black_box,
path::{Path, PathBuf},
};
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use criterion::{criterion_group, criterion_main, Criterion};
const IGNORE_DIRS: [&str; 2] = ["step", "screenshots"];

View File

@ -1,4 +1,6 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use std::hint::black_box;
use criterion::{criterion_group, criterion_main, Criterion};
pub fn bench_parse(c: &mut Criterion) {
for (name, file) in [

View File

@ -1,4 +1,6 @@
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
use std::hint::black_box;
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use kcl_lib::kcl_lsp_server;
use tokio::runtime::Runtime;
use tower_lsp::LanguageServer;

View File

@ -67,6 +67,7 @@ pub struct TcpRead {
/// Occurs when client couldn't read from the WebSocket to the engine.
// #[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum WebSocketReadError {
/// Could not read a message due to WebSocket errors.
Read(tokio_tungstenite::tungstenite::Error),
@ -206,7 +207,7 @@ impl EngineConnection {
async fn inner_send_to_engine(request: WebSocketRequest, tcp_write: &mut WebSocketTcpWrite) -> Result<()> {
let msg = serde_json::to_string(&request).map_err(|e| anyhow!("could not serialize json: {e}"))?;
tcp_write
.send(WsMsg::Text(msg))
.send(WsMsg::Text(msg.into()))
.await
.map_err(|e| anyhow!("could not send json over websocket: {e}"))?;
Ok(())
@ -216,19 +217,17 @@ impl EngineConnection {
async fn inner_send_to_engine_binary(request: WebSocketRequest, tcp_write: &mut WebSocketTcpWrite) -> Result<()> {
let msg = bson::to_vec(&request).map_err(|e| anyhow!("could not serialize bson: {e}"))?;
tcp_write
.send(WsMsg::Binary(msg))
.send(WsMsg::Binary(msg.into()))
.await
.map_err(|e| anyhow!("could not send json over websocket: {e}"))?;
Ok(())
}
pub async fn new(ws: reqwest::Upgraded) -> Result<EngineConnection> {
let wsconfig = tokio_tungstenite::tungstenite::protocol::WebSocketConfig {
let wsconfig = tokio_tungstenite::tungstenite::protocol::WebSocketConfig::default()
// 4294967296 bytes, which is around 4.2 GB.
max_message_size: Some(usize::MAX),
max_frame_size: Some(usize::MAX),
..Default::default()
};
.max_message_size(Some(usize::MAX))
.max_frame_size(Some(usize::MAX));
let ws_stream = tokio_tungstenite::WebSocketStream::from_raw_socket(
ws,

View File

@ -304,9 +304,10 @@ impl ExecutorContext {
let annotations = &variable_declaration.outer_attrs;
// During the evaluation of the variable's LHS, set context that this is all happening inside a variable
// During the evaluation of the variable's RHS, set context that this is all happening inside a variable
// declaration, for the given name. This helps improve user-facing error messages.
exec_state.mod_local.being_declared = Some(variable_declaration.inner.name().to_owned());
let lhs = variable_declaration.inner.name().to_owned();
exec_state.mod_local.being_declared = Some(lhs);
let rhs_result = self
.execute_expr(
&variable_declaration.declaration.init,
@ -645,7 +646,12 @@ impl ExecutorContext {
Expr::Literal(literal) => KclValue::from_literal((**literal).clone(), exec_state),
Expr::TagDeclarator(tag) => tag.execute(exec_state).await?,
Expr::Name(name) => {
let value = name.get_result(exec_state, self).await?.clone();
let being_declared = exec_state.mod_local.being_declared.clone();
let value = name
.get_result(exec_state, self)
.await
.map_err(|e| var_in_own_ref_err(e, &being_declared))?
.clone();
if let KclValue::Module { value: module_id, meta } = value {
self.exec_module_for_result(
module_id,
@ -751,6 +757,24 @@ impl ExecutorContext {
}
}
/// If the error is about an undefined name, and that name matches the name being defined,
/// make the error message more specific.
fn var_in_own_ref_err(e: KclError, being_declared: &Option<String>) -> KclError {
let KclError::UndefinedValue { name, mut details } = e else {
return e;
};
// TODO after June 26th: replace this with a let-chain,
// which will be available in Rust 1.88
// https://rust-lang.github.io/rfcs/2497-if-let-chains.html
match (&being_declared, &name) {
(Some(name0), Some(name1)) if name0 == name1 => {
details.message = format!("You can't use `{name0}` because you're currently trying to define it. Use a different variable here instead.");
}
_ => {}
}
KclError::UndefinedValue { details, name }
}
impl Node<AscribedExpression> {
#[async_recursion]
pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
@ -1327,7 +1351,7 @@ pub(crate) async fn execute_pipe_body(
// Now that we've evaluated the first child expression in the pipeline, following child expressions
// should use the previous child expression for %.
// This means there's no more need for the previous pipe_value from the parent AST node above this one.
let previous_pipe_value = std::mem::replace(&mut exec_state.mod_local.pipe_value, Some(output));
let previous_pipe_value = exec_state.mod_local.pipe_value.replace(output);
// Evaluate remaining elements.
let result = inner_execute_pipe_body(exec_state, body, ctx).await;
// Restore the previous pipe value.

View File

@ -24,6 +24,7 @@ type Point3D = kcmc::shared::Point3d<f64>;
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
#[allow(clippy::large_enum_variant)]
pub enum Geometry {
Sketch(Sketch),
Solid(Solid),
@ -52,6 +53,7 @@ impl Geometry {
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
#[allow(clippy::large_enum_variant)]
pub enum GeometryWithImportedGeometry {
Sketch(Sketch),
Solid(Solid),

View File

@ -187,7 +187,7 @@ impl RuntimeType {
};
RuntimeType::Primitive(PrimitiveType::Number(ty))
}
AstPrimitiveType::Named(name) => Self::from_alias(&name.name, exec_state, source_range)?,
AstPrimitiveType::Named { id } => Self::from_alias(&id.name, exec_state, source_range)?,
AstPrimitiveType::Tag => RuntimeType::Primitive(PrimitiveType::Tag),
AstPrimitiveType::ImportedGeometry => RuntimeType::Primitive(PrimitiveType::ImportedGeometry),
AstPrimitiveType::Function(_) => RuntimeType::Primitive(PrimitiveType::Function),

View File

@ -1189,7 +1189,6 @@ impl LanguageServer for Backend {
}
async fn completion(&self, params: CompletionParams) -> RpcResult<Option<CompletionResponse>> {
// ADAM: This is the entrypoint.
let mut completions = vec![CompletionItem {
label: PIPE_OPERATOR.to_string(),
label_details: None,

View File

@ -228,7 +228,7 @@ impl PrimitiveType {
let mut hasher = Sha256::new();
match self {
PrimitiveType::Any => hasher.update(b"any"),
PrimitiveType::Named(id) => hasher.update(id.compute_digest()),
PrimitiveType::Named { id } => hasher.update(id.compute_digest()),
PrimitiveType::String => hasher.update(b"string"),
PrimitiveType::Number(suffix) => hasher.update(suffix.digestable_id()),
PrimitiveType::Boolean => hasher.update(b"bool"),

View File

@ -454,7 +454,7 @@ impl Node<Program> {
alpha: c.a,
},
};
if colors.borrow().iter().any(|c| *c == color) {
if colors.borrow().contains(&color) {
return;
}
colors.borrow_mut().push(color);
@ -529,7 +529,7 @@ impl Node<Program> {
let new_color = csscolorparser::Color::new(color.red, color.green, color.blue, color.alpha);
Ok(Some(ColorPresentation {
// The label will be what they replace the color with.
label: new_color.to_hex_string(),
label: new_color.to_css_hex(),
text_edit: None,
additional_text_edits: None,
}))
@ -3230,7 +3230,7 @@ pub enum PrimitiveType {
/// `fn`, type of functions.
Function(FunctionType),
/// An identifier used as a type (not really a primitive type, but whatever).
Named(Node<Identifier>),
Named { id: Node<Identifier> },
}
impl PrimitiveType {
@ -3286,7 +3286,7 @@ impl fmt::Display for PrimitiveType {
}
Ok(())
}
PrimitiveType::Named(n) => write!(f, "{}", n.name),
PrimitiveType::Named { id: n } => write!(f, "{}", n.name),
}
}
}

View File

@ -2938,7 +2938,7 @@ fn primitive_type(i: &mut TokenSlice) -> ModalResult<Node<PrimitiveType>> {
(identifier, opt(delimited(open_paren, uom_for_type, close_paren))).map(|(ident, suffix)| {
let mut result = Node::new(PrimitiveType::Boolean, ident.start, ident.end, ident.module_id);
result.inner =
PrimitiveType::primitive_from_str(&ident.name, suffix).unwrap_or(PrimitiveType::Named(ident));
PrimitiveType::primitive_from_str(&ident.name, suffix).unwrap_or(PrimitiveType::Named { id: ident });
result
}),
))

View File

@ -3504,3 +3504,24 @@ mod var_ref_in_own_def {
super::execute(TEST_NAME, true).await
}
}
mod ascription_unknown_type {
const TEST_NAME: &str = "ascription_unknown_type";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[tokio::test(flavor = "multi_thread")]
async fn unparse() {
super::unparse(TEST_NAME).await
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, true).await
}
}

View File

@ -21,6 +21,7 @@ use crate::{
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
pub enum SweepPath {
Sketch(Sketch),
Helix(Box<Helix>),

View File

@ -0,0 +1,32 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Artifact commands ascription_unknown_type.kcl
---
[
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "edge_lines_visible",
"hidden": false
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "object_visible",
"object_id": "[uuid]",
"hidden": true
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "object_visible",
"object_id": "[uuid]",
"hidden": true
}
}
]

View File

@ -0,0 +1,6 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Artifact graph flowchart ascription_unknown_type.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,3 @@
```mermaid
flowchart LR
```

View File

@ -0,0 +1,67 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Result of parsing ascription_unknown_type.kcl
---
{
"Ok": {
"body": [
{
"commentStart": 0,
"declaration": {
"commentStart": 0,
"end": 0,
"id": {
"commentStart": 0,
"end": 0,
"name": "z",
"start": 0,
"type": "Identifier"
},
"init": {
"commentStart": 0,
"end": 0,
"expr": {
"commentStart": 0,
"end": 0,
"raw": "10",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 10.0,
"suffix": "None"
}
},
"start": 0,
"ty": {
"commentStart": 0,
"end": 0,
"id": {
"commentStart": 0,
"end": 0,
"name": "NotARealType",
"start": 0,
"type": "Identifier"
},
"p_type": "Named",
"start": 0,
"type": "Primitive"
},
"type": "AscribedExpression",
"type": "AscribedExpression"
},
"start": 0,
"type": "VariableDeclarator"
},
"end": 0,
"kind": "const",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
}
],
"commentStart": 0,
"end": 0,
"start": 0
}
}

View File

@ -0,0 +1,12 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Error from executing ascription_unknown_type.kcl
---
KCL Semantic error
× semantic: Unknown type: NotARealType
╭────
1 │ z = 10: NotARealType
· ─┬
· ╰── tests/ascription_unknown_type/input.kcl
╰────

View File

@ -0,0 +1 @@
z = 10: NotARealType

View File

@ -0,0 +1,5 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Operations executed ascription_unknown_type.kcl
---
[]

View File

@ -0,0 +1,5 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Result of unparsing ascription_unknown_type.kcl
---
z = 10: NotARealType

View File

@ -28,5 +28,30 @@ description: Artifact commands var_ref_in_own_def.kcl
"object_id": "[uuid]",
"hidden": true
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "make_plane",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"x_axis": {
"x": 1.0,
"y": 0.0,
"z": 0.0
},
"y_axis": {
"x": 0.0,
"y": 1.0,
"z": 0.0
},
"size": 60.0,
"clobber": false,
"hide": true
}
}
]

View File

@ -1,3 +1,5 @@
```mermaid
flowchart LR
1["Plane<br>[95, 112, 0]"]
%% [ProgramBodyItem { index: 0 }, VariableDeclarationDeclaration, VariableDeclarationInit, PipeBodyItem { index: 0 }]
```

View File

@ -13,47 +13,141 @@ description: Result of parsing var_ref_in_own_def.kcl
"id": {
"commentStart": 0,
"end": 0,
"name": "x",
"name": "sketch001",
"start": 0,
"type": "Identifier"
},
"init": {
"callee": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"body": [
{
"callee": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "startSketchOn",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name"
},
"commentStart": 0,
"end": 0,
"name": "cos",
"start": 0,
"type": "Identifier"
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "XY",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name",
"type": "Name"
}
},
"path": [],
"start": 0,
"type": "Name"
},
{
"arguments": [
{
"type": "LabeledArg",
"label": null,
"arg": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "sketch001",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name",
"type": "Name"
}
}
],
"callee": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "startProfileAt",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name"
},
"commentStart": 0,
"end": 0,
"start": 0,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": {
"commentStart": 0,
"elements": [
{
"commentStart": 0,
"end": 0,
"raw": "20",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 20.0,
"suffix": "None"
}
},
{
"argument": {
"commentStart": 0,
"end": 0,
"raw": "20",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 20.0,
"suffix": "None"
}
},
"commentStart": 0,
"end": 0,
"operator": "-",
"start": 0,
"type": "UnaryExpression",
"type": "UnaryExpression"
}
],
"end": 0,
"start": 0,
"type": "ArrayExpression",
"type": "ArrayExpression"
}
}
],
"commentStart": 0,
"end": 0,
"start": 0,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "x",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name",
"type": "Name"
}
"type": "PipeExpression",
"type": "PipeExpression"
},
"start": 0,
"type": "VariableDeclarator"
@ -61,7 +155,7 @@ description: Result of parsing var_ref_in_own_def.kcl
"end": 0,
"kind": "const",
"preComments": [
"// This won't work, because `x` is being referenced in its own definition."
"// This won't work, because `sketch001` is being referenced in its own definition."
],
"start": 0,
"type": "VariableDeclaration",

View File

@ -4,10 +4,11 @@ description: Error from executing var_ref_in_own_def.kcl
---
KCL UndefinedValue error
× undefined value: `x` is not defined
╭─[2:9]
1 │ // This won't work, because `x` is being referenced in its own definition.
2 │ x = cos(x)
· ┬
· ╰── tests/var_ref_in_own_def/input.kcl
× undefined value: You can't use `sketch001` because you're currently trying
│ to define it. Use a different variable here instead.
╭─[3:32]
2 │ sketch001 = startSketchOn(XY)
3 │ |> startProfileAt([20, -20], sketch001)
· ────┬────
· ╰── tests/var_ref_in_own_def/input.kcl
╰────

View File

@ -1,2 +1,3 @@
// This won't work, because `x` is being referenced in its own definition.
x = cos(x)
// This won't work, because `sketch001` is being referenced in its own definition.
sketch001 = startSketchOn(XY)
|> startProfileAt([20, -20], sketch001)

View File

@ -2,4 +2,18 @@
source: kcl-lib/src/simulation_tests.rs
description: Operations executed var_ref_in_own_def.kcl
---
[]
[
{
"type": "StdLibCall",
"name": "startSketchOn",
"unlabeledArg": {
"value": {
"type": "Plane",
"artifact_id": "[uuid]"
},
"sourceRange": []
},
"labeledArgs": {},
"sourceRange": []
}
]

View File

@ -2,5 +2,6 @@
source: kcl-lib/src/simulation_tests.rs
description: Result of unparsing var_ref_in_own_def.kcl
---
// This won't work, because `x` is being referenced in its own definition.
x = cos(x)
// This won't work, because `sketch001` is being referenced in its own definition.
sketch001 = startSketchOn(XY)
|> startProfileAt([20, -20], sketch001)

View File

@ -1,6 +1,6 @@
[package]
name = "kcl-python-bindings"
version = "0.3.78"
version = "0.3.79"
edition = "2021"
repository = "https://github.com/kittycad/modeling-app"
exclude = ["tests/*", "files/*", "venv/*"]

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-test-server"
description = "A test server for KCL"
version = "0.1.78"
version = "0.1.79"
edition = "2021"
license = "MIT"

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-to-core"
description = "Utility methods to convert kcl to engine core executable tests"
version = "0.1.78"
version = "0.1.79"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -1,6 +1,6 @@
[package]
name = "kcl-wasm-lib"
version = "0.1.78"
version = "0.1.79"
edition = "2021"
repository = "https://github.com/KittyCAD/modeling-app"
rust-version = "1.83"

View File

@ -3,7 +3,7 @@
use std::sync::Arc;
use gloo_utils::format::JsValueSerdeExt;
use kcl_lib::{wasm_engine::FileManager, EngineManager, Program};
use kcl_lib::{wasm_engine::FileManager, EngineManager, ExecOutcome, KclError, KclErrorWithOutputs, Program};
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
@ -56,7 +56,7 @@ impl Context {
return Ok(kcl_lib::ExecutorContext::new_mock(
self.mock_engine.clone(),
self.fs.clone(),
settings.into(),
settings,
));
}
@ -74,18 +74,38 @@ impl Context {
program_ast_json: &str,
path: Option<String>,
settings: &str,
) -> Result<JsValue, String> {
) -> Result<JsValue, JsValue> {
console_error_panic_hook::set_once();
let program: Program = serde_json::from_str(program_ast_json).map_err(|e| e.to_string())?;
self.execute_typed(program_ast_json, path, settings)
.await
.and_then(|outcome| JsValue::from_serde(&outcome).map_err(|e| {
// The serde-wasm-bindgen does not work here because of weird HashMap issues.
// DO NOT USE serde_wasm_bindgen::to_value it will break the frontend.
KclErrorWithOutputs::no_outputs(KclError::internal(
format!("Could not serialize successful KCL result. This is a bug in KCL and not in your code, please report this to Zoo. Details: {e}"),
))}))
.map_err(|e: KclErrorWithOutputs| JsValue::from_serde(&e).unwrap())
}
let ctx = self.create_executor_ctx(settings, path, false)?;
match ctx.run_with_caching(program).await {
// The serde-wasm-bindgen does not work here because of weird HashMap issues.
// DO NOT USE serde_wasm_bindgen::to_value it will break the frontend.
Ok(outcome) => JsValue::from_serde(&outcome).map_err(|e| e.to_string()),
Err(err) => Err(serde_json::to_string(&err).map_err(|serde_err| serde_err.to_string())?),
}
async fn execute_typed(
&self,
program_ast_json: &str,
path: Option<String>,
settings: &str,
) -> Result<ExecOutcome, KclErrorWithOutputs> {
let program: Program = serde_json::from_str(program_ast_json).map_err(|e| {
let err = KclError::internal(
format!("Could not deserialize KCL AST. This is a bug in KCL and not in your code, please report this to Zoo. Details: {e}"),
);
KclErrorWithOutputs::no_outputs(err)
})?;
let ctx = self
.create_executor_ctx(settings, path, false)
.map_err(|e| KclErrorWithOutputs::no_outputs(KclError::internal(
format!("Could not create KCL executor context. This is a bug in KCL and not in your code, please report this to Zoo. Details: {e}"),
)))?;
ctx.run_with_caching(program).await
}
/// Reset the scene and bust the cache.

View File

@ -1,3 +1,3 @@
[toolchain]
channel = "1.86"
channel = "1.87"
components = ["clippy", "rustfmt"]

View File

@ -1315,7 +1315,7 @@ export class CameraControls {
)
if (
initialInteractionType === 'rotate' &&
this.engineCommandManager.settings.cameraOrbit === 'trackball'
this.getSettings?.().modeling.cameraOrbit.current === 'trackball'
) {
return 'rotatetrackball'
}

View File

@ -1,8 +1,11 @@
import { useEffect } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
import { isDesktop } from '@src/lib/isDesktop'
import { PATHS } from '@src/lib/paths'
import { IMMEDIATE_SIGN_IN_IF_NECESSARY_QUERY_PARAM } from '@src/lib/constants'
import { useAuthState } from '@src/lib/singletons'
import { generateSignInUrl } from '@src/routes/utils'
/**
* A simple hook that listens to the auth state of the app and navigates
@ -12,6 +15,10 @@ export function useAuthNavigation() {
const navigate = useNavigate()
const location = useLocation()
const authState = useAuthState()
const [searchParams] = useSearchParams()
const requestingImmediateSignInIfNecessary = searchParams.has(
IMMEDIATE_SIGN_IN_IF_NECESSARY_QUERY_PARAM
)
// Subscribe to the auth state of the app and navigate accordingly.
useEffect(() => {
@ -24,6 +31,11 @@ export function useAuthNavigation() {
authState.matches('loggedOut') &&
!location.pathname.includes(PATHS.SIGN_IN)
) {
if (requestingImmediateSignInIfNecessary && !isDesktop()) {
window.location.href = generateSignInUrl()
return
}
navigate(PATHS.SIGN_IN + (location.search || ''))
}
}, [authState, location.pathname])

View File

@ -389,7 +389,20 @@ export function sketchFromKclValue(
}
export const errFromErrWithOutputs = (e: any): KCLError => {
const parsed: KclErrorWithOutputs = JSON.parse(e.toString())
// `e` is any, so let's figure out something useful to do with it.
const parsed: KclErrorWithOutputs = (() => {
// No need to parse, it's already an object.
if (typeof e === 'object') {
return e
}
// It's a string, so parse it.
if (typeof e === 'string') {
return JSON.parse(e)
}
// It can be converted to a string, then parsed.
return JSON.parse(e.toString())
})()
return new KCLError(
parsed.error.kind,
parsed.error.details.msg,

View File

@ -217,3 +217,6 @@ export const POOL_QUERY_PARAM = 'pool'
* @deprecated: supporting old share links with this. For new command URLs, use "cmd"
*/
export const CREATE_FILE_URL_PARAM = 'create-file'
/** A query parameter to skip the sign-on view if unnecessary. */
export const IMMEDIATE_SIGN_IN_IF_NECESSARY_QUERY_PARAM =
'immediate-sign-in-if-necessary'

View File

@ -10,12 +10,11 @@ import { VITE_KC_API_BASE_URL, VITE_KC_SITE_BASE_URL } from '@src/env'
import { APP_NAME } from '@src/lib/constants'
import { isDesktop } from '@src/lib/isDesktop'
import { openExternalBrowserIfDesktop } from '@src/lib/openWindow'
import { PATHS } from '@src/lib/paths'
import { Themes, getSystemTheme } from '@src/lib/theme'
import { reportRejection } from '@src/lib/trap'
import { toSync } from '@src/lib/utils'
import { authActor, useSettings } from '@src/lib/singletons'
import { APP_VERSION } from '@src/routes/utils'
import { APP_VERSION, generateSignInUrl } from '@src/routes/utils'
const subtleBorder =
'border border-solid border-chalkboard-30 dark:border-chalkboard-80'
@ -36,11 +35,7 @@ const SignIn = () => {
const {
app: { theme },
} = useSettings()
const signInUrl = `${VITE_KC_SITE_BASE_URL}${
PATHS.SIGN_IN
}?callbackUrl=${encodeURIComponent(
typeof window !== 'undefined' && window.location.href.replace('signin', '')
)}`
const signInUrl = generateSignInUrl()
const kclSampleUrl = `${VITE_KC_SITE_BASE_URL}/docs/kcl-samples/car-wheel-assembly`
const getThemeText = useCallback(

View File

@ -1,6 +1,7 @@
import { NODE_ENV } from '@src/env'
import { NODE_ENV, VITE_KC_SITE_BASE_URL } from '@src/env'
import { isDesktop } from '@src/lib/isDesktop'
import { IS_PLAYWRIGHT_KEY } from '@src/lib/constants'
import { PATHS } from '@src/lib/paths'
const isTestEnv = window?.localStorage.getItem(IS_PLAYWRIGHT_KEY) === 'true'
@ -27,3 +28,11 @@ export function getReleaseUrl(version: string = APP_VERSION) {
return `https://github.com/KittyCAD/modeling-app/releases/tag/v${version}`
}
export function generateSignInUrl() {
return `${VITE_KC_SITE_BASE_URL}${
PATHS.SIGN_IN
}?callbackUrl=${encodeURIComponent(
typeof window !== 'undefined' && window.location.href.replace('signin', '')
)}`
}