Merge branch 'main' into pierremtb/issue7349-transform-codemods-out-of-pipe

This commit is contained in:
Pierre Jacquier
2025-06-04 09:37:35 -04:00
46 changed files with 584 additions and 302 deletions

View File

@ -534,7 +534,7 @@ profile001 = startProfile(sketch001, at = [-484.34, 484.95])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
` `
const targetURL = `?create-file&name=test&units=mm&code=${encodeURIComponent(btoa(code))}&ask-open-desktop` const targetURL = `?create-file=true&name=test&units=mm&code=${encodeURIComponent(btoa(code))}&ask-open-desktop=true`
await page.goto(page.url() + targetURL) await page.goto(page.url() + targetURL)
expect(page.url()).toContain(targetURL) expect(page.url()).toContain(targetURL)
}) })

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 test.step('add new profile', async () => {
await toolbar.rectangleBtn.click() await toolbar.rectangleBtn.click()
await page.waitForTimeout(100) await page.waitForTimeout(200)
await rectStart() await rectStart()
await editor.expectEditor.toContain( await editor.expectEditor.toContain(
`profile005 = startProfile(sketch001, at = [15.68, -3.84])` `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 { getUtils } from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test' 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.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, page,
context,
homePage,
scene, 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) const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene() await test.step('Set up initial camera position', async () =>
await scene.connectionEstablished() await scene.moveCameraTo({
x: beforePosition[0],
y: beforePosition[1],
z: beforePosition[2],
}))
await u.openAndClearDebugPanel() await test.step('Do actions and watch for changes', async () =>
await u.closeKclCodePanel() u.doAndWaitForImageDiff(async () => {
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 mouseActions() await mouseActions()
await u.openAndClearDebugPanel() await u.openAndClearDebugPanel()
await u.closeDebugPanel() await u.closeDebugPanel()
await page.waitForTimeout(100) 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 u.openAndClearDebugPanel()
await page.getByTestId('cam-x-position').isVisible() await u.closeKclCodePanel()
const vals = await Promise.all([ await test.step('Pan', async () => {
page.getByTestId('cam-x-position').inputValue(), await bakeInRetries({
page.getByTestId('cam-y-position').inputValue(), mouseActions: async () => {
page.getByTestId('cam-z-position').inputValue(), await page.keyboard.down('Shift')
]) await page.mouse.move(600, 200)
const xError = Math.abs(Number(vals[0]) + xyz[0]) await page.mouse.down({ button: 'right' })
const yError = Math.abs(Number(vals[1]) + xyz[1]) // Gotcha: remove steps:2 from this 700,200 mouse move. This bricked the test on local host engine.
const zError = Math.abs(Number(vals[2]) + xyz[2]) 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) { await test.step('Zoom with scrollwheel', async () => {
if (cnt > 2) { const refreshCamValuesCmd: EngineCommand = {
console.log('xVal', vals[0], 'xError', xError) type: 'modeling_cmd_req',
console.log('yVal', vals[1], 'yError', yError) cmd_id: uuidv4(),
console.log('zVal', vals[2], 'zError', zError) cmd: {
type: 'default_camera_get_settings',
throw new Error('Camera position not as expected') },
} }
shouldRetry = true await bakeInRetries({
} mouseActions: async () => {
await page.getByRole('button', { name: 'Exit Sketch' }).click() await page.mouse.move(700, 400)
await page.waitForTimeout(100) await page.mouse.wheel(0, -150)
if (shouldRetry) await bakeInRetries(mouseActions, xyz, cnt + 1)
// 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() test(
expect(appLogoBBox).not.toBeNull() 'Can orbit camera reliably',
if (!appLogoBBox) throw new Error('app logo not found') {
await page.mouse.move( tag: '@web',
appLogoBBox.x + appLogoBBox.width / 2, },
appLogoBBox.y + appLogoBBox.height / 2 async ({ page, homePage, scene, cmdBar }) => {
) const u = await getUtils(page)
await page.waitForTimeout(100) const initialCamPosition: [number, number, number] = [0, 85, 85]
await page.mouse.move(600, 303)
await page.waitForTimeout(100)
await page.mouse.up({ button: 'right' })
}, [4, -10.5, -120])
await bakeInRetries(async () => { await homePage.goToModelingScene()
await page.keyboard.down('Shift') // this turns on the debug pane setting as well
await page.mouse.move(600, 200) await scene.settled(cmdBar)
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])
const camCommand: EngineCommand = { await u.openAndClearDebugPanel()
type: 'modeling_cmd_req', await u.closeKclCodePanel()
cmd_id: uuidv4(),
cmd: { await test.step('Test orbit with spherical mode', async () => {
type: 'default_camera_look_at', await bakeInRetries({
center: { x: 0, y: 0, z: 0 }, mouseActions: async () => {
vantage: { x: camPos[0], y: camPos[1], z: camPos[2] }, await page.mouse.move(700, 200)
up: { x: 0, y: 0, z: 1 }, 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 // TODO: fix after electron migration is merged
test('Zoom should be consistent when exiting or entering sketches', async ({ test('Zoom should be consistent when exiting or entering sketches', async ({

30
rust/Cargo.lock generated
View File

@ -627,26 +627,22 @@ dependencies = [
[[package]] [[package]]
name = "criterion" name = "criterion"
version = "0.5.1" version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" checksum = "3bf7af66b0989381bd0be551bd7cc91912a655a58c6918420c9527b1fd8b4679"
dependencies = [ dependencies = [
"anes", "anes",
"cast", "cast",
"ciborium", "ciborium",
"clap", "clap",
"criterion-plot", "criterion-plot",
"futures", "itertools 0.13.0",
"is-terminal",
"itertools 0.10.5",
"num-traits 0.2.19", "num-traits 0.2.19",
"once_cell",
"oorandom", "oorandom",
"plotters", "plotters",
"rayon", "rayon",
"regex", "regex",
"serde", "serde",
"serde_derive",
"serde_json", "serde_json",
"tinytemplate", "tinytemplate",
"tokio", "tokio",
@ -1815,7 +1811,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-bumper" name = "kcl-bumper"
version = "0.1.78" version = "0.1.79"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
@ -1826,7 +1822,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-derive-docs" name = "kcl-derive-docs"
version = "0.1.78" version = "0.1.79"
dependencies = [ dependencies = [
"Inflector", "Inflector",
"anyhow", "anyhow",
@ -1845,7 +1841,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-directory-test-macro" name = "kcl-directory-test-macro"
version = "0.1.78" version = "0.1.79"
dependencies = [ dependencies = [
"convert_case", "convert_case",
"proc-macro2", "proc-macro2",
@ -1855,7 +1851,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-language-server" name = "kcl-language-server"
version = "0.2.78" version = "0.2.79"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
@ -1876,7 +1872,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-language-server-release" name = "kcl-language-server-release"
version = "0.1.78" version = "0.1.79"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
@ -1896,7 +1892,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-lib" name = "kcl-lib"
version = "0.2.78" version = "0.2.79"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"approx 0.5.1", "approx 0.5.1",
@ -1973,7 +1969,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-python-bindings" name = "kcl-python-bindings"
version = "0.3.78" version = "0.3.79"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"kcl-lib", "kcl-lib",
@ -1988,7 +1984,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-test-server" name = "kcl-test-server"
version = "0.1.78" version = "0.1.79"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"hyper 0.14.32", "hyper 0.14.32",
@ -2001,7 +1997,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-to-core" name = "kcl-to-core"
version = "0.1.78" version = "0.1.79"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -2015,7 +2011,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-wasm-lib" name = "kcl-wasm-lib"
version = "0.1.78" version = "0.1.79"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bson", "bson",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
[package] [package]
name = "kcl-lib" name = "kcl-lib"
description = "KittyCAD Language implementation and tools" description = "KittyCAD Language implementation and tools"
version = "0.2.78" version = "0.2.79"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app" repository = "https://github.com/KittyCAD/modeling-app"
@ -130,7 +130,7 @@ tabled = ["dep:tabled"]
[dev-dependencies] [dev-dependencies]
approx = "0.5" approx = "0.5"
base64 = "0.22.1" base64 = "0.22.1"
criterion = { version = "0.5.1", features = ["async_tokio"] } criterion = { version = "0.6.0", features = ["async_tokio"] }
expectorate = "1.1.0" expectorate = "1.1.0"
handlebars = "6.3.2" handlebars = "6.3.2"
image = { version = "0.25.6", default-features = false, features = ["png"] } image = { version = "0.25.6", default-features = false, features = ["png"] }

View File

@ -1,9 +1,10 @@
use std::{ use std::{
fs, fs,
hint::black_box,
path::{Path, PathBuf}, 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"]; 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) { pub fn bench_parse(c: &mut Criterion) {
for (name, file) in [ 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 kcl_lib::kcl_lsp_server;
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
use tower_lsp::LanguageServer; 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. /// Occurs when client couldn't read from the WebSocket to the engine.
// #[derive(Debug)] // #[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum WebSocketReadError { pub enum WebSocketReadError {
/// Could not read a message due to WebSocket errors. /// Could not read a message due to WebSocket errors.
Read(tokio_tungstenite::tungstenite::Error), Read(tokio_tungstenite::tungstenite::Error),

View File

@ -1351,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 // Now that we've evaluated the first child expression in the pipeline, following child expressions
// should use the previous child expression for %. // 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. // 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. // Evaluate remaining elements.
let result = inner_execute_pipe_body(exec_state, body, ctx).await; let result = inner_execute_pipe_body(exec_state, body, ctx).await;
// Restore the previous pipe value. // 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)] #[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)] #[ts(export)]
#[serde(tag = "type")] #[serde(tag = "type")]
#[allow(clippy::large_enum_variant)]
pub enum Geometry { pub enum Geometry {
Sketch(Sketch), Sketch(Sketch),
Solid(Solid), Solid(Solid),
@ -52,6 +53,7 @@ impl Geometry {
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)] #[ts(export)]
#[serde(tag = "type")] #[serde(tag = "type")]
#[allow(clippy::large_enum_variant)]
pub enum GeometryWithImportedGeometry { pub enum GeometryWithImportedGeometry {
Sketch(Sketch), Sketch(Sketch),
Solid(Solid), Solid(Solid),

View File

@ -187,7 +187,7 @@ impl RuntimeType {
}; };
RuntimeType::Primitive(PrimitiveType::Number(ty)) 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::Tag => RuntimeType::Primitive(PrimitiveType::Tag),
AstPrimitiveType::ImportedGeometry => RuntimeType::Primitive(PrimitiveType::ImportedGeometry), AstPrimitiveType::ImportedGeometry => RuntimeType::Primitive(PrimitiveType::ImportedGeometry),
AstPrimitiveType::Function(_) => RuntimeType::Primitive(PrimitiveType::Function), 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>> { async fn completion(&self, params: CompletionParams) -> RpcResult<Option<CompletionResponse>> {
// ADAM: This is the entrypoint.
let mut completions = vec![CompletionItem { let mut completions = vec![CompletionItem {
label: PIPE_OPERATOR.to_string(), label: PIPE_OPERATOR.to_string(),
label_details: None, label_details: None,

View File

@ -228,7 +228,7 @@ impl PrimitiveType {
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
match self { match self {
PrimitiveType::Any => hasher.update(b"any"), 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::String => hasher.update(b"string"),
PrimitiveType::Number(suffix) => hasher.update(suffix.digestable_id()), PrimitiveType::Number(suffix) => hasher.update(suffix.digestable_id()),
PrimitiveType::Boolean => hasher.update(b"bool"), PrimitiveType::Boolean => hasher.update(b"bool"),

View File

@ -454,7 +454,7 @@ impl Node<Program> {
alpha: c.a, alpha: c.a,
}, },
}; };
if colors.borrow().iter().any(|c| *c == color) { if colors.borrow().contains(&color) {
return; return;
} }
colors.borrow_mut().push(color); colors.borrow_mut().push(color);
@ -3230,7 +3230,7 @@ pub enum PrimitiveType {
/// `fn`, type of functions. /// `fn`, type of functions.
Function(FunctionType), Function(FunctionType),
/// An identifier used as a type (not really a primitive type, but whatever). /// An identifier used as a type (not really a primitive type, but whatever).
Named(Node<Identifier>), Named { id: Node<Identifier> },
} }
impl PrimitiveType { impl PrimitiveType {
@ -3286,7 +3286,7 @@ impl fmt::Display for PrimitiveType {
} }
Ok(()) 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)| { (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); let mut result = Node::new(PrimitiveType::Boolean, ident.start, ident.end, ident.module_id);
result.inner = 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 result
}), }),
)) ))

View File

@ -3504,3 +3504,24 @@ mod var_ref_in_own_def {
super::execute(TEST_NAME, true).await 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)] #[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)] #[ts(export)]
#[serde(untagged)] #[serde(untagged)]
#[allow(clippy::large_enum_variant)]
pub enum SweepPath { pub enum SweepPath {
Sketch(Sketch), Sketch(Sketch),
Helix(Box<Helix>), 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

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@
use std::sync::Arc; use std::sync::Arc;
use gloo_utils::format::JsValueSerdeExt; 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::*; use wasm_bindgen::prelude::*;
#[wasm_bindgen] #[wasm_bindgen]
@ -56,7 +56,7 @@ impl Context {
return Ok(kcl_lib::ExecutorContext::new_mock( return Ok(kcl_lib::ExecutorContext::new_mock(
self.mock_engine.clone(), self.mock_engine.clone(),
self.fs.clone(), self.fs.clone(),
settings.into(), settings,
)); ));
} }
@ -74,18 +74,38 @@ impl Context {
program_ast_json: &str, program_ast_json: &str,
path: Option<String>, path: Option<String>,
settings: &str, settings: &str,
) -> Result<JsValue, String> { ) -> Result<JsValue, JsValue> {
console_error_panic_hook::set_once(); 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)?; async fn execute_typed(
match ctx.run_with_caching(program).await { &self,
// The serde-wasm-bindgen does not work here because of weird HashMap issues. program_ast_json: &str,
// DO NOT USE serde_wasm_bindgen::to_value it will break the frontend. path: Option<String>,
Ok(outcome) => JsValue::from_serde(&outcome).map_err(|e| e.to_string()), settings: &str,
Err(err) => Err(serde_json::to_string(&err).map_err(|serde_err| serde_err.to_string())?), ) -> 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. /// Reset the scene and bust the cache.

View File

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

View File

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

View File

@ -1,8 +1,11 @@
import { useEffect } from 'react' 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 { PATHS } from '@src/lib/paths'
import { IMMEDIATE_SIGN_IN_IF_NECESSARY_QUERY_PARAM } from '@src/lib/constants'
import { useAuthState } from '@src/lib/singletons' 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 * 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 navigate = useNavigate()
const location = useLocation() const location = useLocation()
const authState = useAuthState() 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. // Subscribe to the auth state of the app and navigate accordingly.
useEffect(() => { useEffect(() => {
@ -24,6 +31,11 @@ export function useAuthNavigation() {
authState.matches('loggedOut') && authState.matches('loggedOut') &&
!location.pathname.includes(PATHS.SIGN_IN) !location.pathname.includes(PATHS.SIGN_IN)
) { ) {
if (requestingImmediateSignInIfNecessary && !isDesktop()) {
window.location.href = generateSignInUrl()
return
}
navigate(PATHS.SIGN_IN + (location.search || '')) navigate(PATHS.SIGN_IN + (location.search || ''))
} }
}, [authState, location.pathname]) }, [authState, location.pathname])

View File

@ -4,6 +4,7 @@ import { useSearchParams } from 'react-router-dom'
import { base64ToString } from '@src/lib/base64' import { base64ToString } from '@src/lib/base64'
import type { ProjectsCommandSchema } from '@src/lib/commandBarConfigs/projectsCommandConfig' import type { ProjectsCommandSchema } from '@src/lib/commandBarConfigs/projectsCommandConfig'
import { import {
ASK_TO_OPEN_QUERY_PARAM,
CMD_GROUP_QUERY_PARAM, CMD_GROUP_QUERY_PARAM,
CMD_NAME_QUERY_PARAM, CMD_NAME_QUERY_PARAM,
CREATE_FILE_URL_PARAM, CREATE_FILE_URL_PARAM,
@ -36,8 +37,15 @@ export type CreateFileSchemaMethodOptional = Omit<
export function useQueryParamEffects() { export function useQueryParamEffects() {
const authState = useAuthState() const authState = useAuthState()
const [searchParams, setSearchParams] = useSearchParams() const [searchParams, setSearchParams] = useSearchParams()
const shouldInvokeCreateFile = searchParams.has(CREATE_FILE_URL_PARAM) const hasAskToOpen = !isDesktop() && searchParams.has(ASK_TO_OPEN_QUERY_PARAM)
// Let hasAskToOpen be handled by the OpenInDesktopAppHandler component first to avoid racing with it,
// only deal with other params after user decided to open in desktop or web.
// Without this the "Zoom to fit to shared model on web" test fails, although manually testing works due
// to different timings.
const shouldInvokeCreateFile =
!hasAskToOpen && searchParams.has(CREATE_FILE_URL_PARAM)
const shouldInvokeGenericCmd = const shouldInvokeGenericCmd =
!hasAskToOpen &&
searchParams.has(CMD_NAME_QUERY_PARAM) && searchParams.has(CMD_NAME_QUERY_PARAM) &&
searchParams.has(CMD_GROUP_QUERY_PARAM) searchParams.has(CMD_GROUP_QUERY_PARAM)

View File

@ -245,6 +245,38 @@ ${insertCode}
}) })
describe('testing getConstraintInfo', () => { describe('testing getConstraintInfo', () => {
describe('when user edits KCL to be invalid', () => {
const code = `part001 = startSketchOn(XZ)
|> startProfile(at = [0,]) // Missing y coordinate
|> line(end = [3, 4])`
test.each([
[
'startProfile',
[
// No constraints
],
],
])('testing %s when inputs are unconstrained', (functionName, expected) => {
const ast = assertParse(code)
const match = new RegExp(functionName).exec(code)
expect(match).toBeTruthy()
if (match === null) {
return
}
const start = code.indexOf(match[0])
expect(start).toBeGreaterThanOrEqual(0)
const sourceRange = topLevelRange(start, start + functionName.length)
if (err(ast)) return ast
const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
const callExp = getNodeFromPath<Node<CallExpressionKw>>(ast, pathToNode, [
'CallExpressionKw',
])
if (err(callExp)) return callExp
const result = getConstraintInfoKw(callExp.node, code, pathToNode)
expect(result).toEqual(expected)
})
})
describe('object notation', () => { describe('object notation', () => {
const code = `part001 = startSketchOn(-XZ) const code = `part001 = startSketchOn(-XZ)
|> startProfile(at = [0,0]) |> startProfile(at = [0,0])

View File

@ -1183,7 +1183,7 @@ export const startProfile: SketchLineHelperKw = {
return [] return []
} }
const argIndex = findKwArgAnyIndex([ARG_AT], callExp) const argIndex = findKwArgAnyIndex([ARG_AT], callExp)
if (argIndex === undefined) { if (argIndex === undefined || expr.elements.length < 2) {
return [] return []
} }
const pathToXYArray: PathToNode = [ const pathToXYArray: PathToNode = [
@ -1471,7 +1471,9 @@ export const circle: SketchLineHelperKw = {
key: ARG_RADIUS, key: ARG_RADIUS,
}, },
}, },
{ ]
if (centerInfo.expr.elements.length >= 2) {
constraints.push({
stdLibFnName: 'circle', stdLibFnName: 'circle',
type: 'xAbsolute', type: 'xAbsolute',
isConstrained: isNotLiteralArrayOrStatic(centerInfo.expr.elements[0]), isConstrained: isNotLiteralArrayOrStatic(centerInfo.expr.elements[0]),
@ -1489,8 +1491,8 @@ export const circle: SketchLineHelperKw = {
index: 0, index: 0,
key: ARG_CIRCLE_CENTER, key: ARG_CIRCLE_CENTER,
}, },
}, })
{ constraints.push({
stdLibFnName: 'circle', stdLibFnName: 'circle',
type: 'yAbsolute', type: 'yAbsolute',
isConstrained: isNotLiteralArrayOrStatic(centerInfo.expr.elements[1]), isConstrained: isNotLiteralArrayOrStatic(centerInfo.expr.elements[1]),
@ -1508,8 +1510,8 @@ export const circle: SketchLineHelperKw = {
index: 1, index: 1,
key: 'center', key: 'center',
}, },
}, })
] }
return constraints return constraints
}, },
} }
@ -2256,134 +2258,103 @@ export const circleThreePoint: SketchLineHelperKw = {
const pathToP3XArg: PathToNode = [...pathToP3ArrayExpression, [0, 'index']] const pathToP3XArg: PathToNode = [...pathToP3ArrayExpression, [0, 'index']]
const pathToP3YArg: PathToNode = [...pathToP3ArrayExpression, [1, 'index']] const pathToP3YArg: PathToNode = [...pathToP3ArrayExpression, [1, 'index']]
const constraints: (ConstrainInfo & { filterValue: string })[] = [ const constraints: (ConstrainInfo & { filterValue: string })[] = []
{ if (p1Details.expr.elements.length >= 2) {
const p1XArg = p1Details.expr.elements[0]
const p1YArg = p1Details.expr.elements[1]
constraints.push({
stdLibFnName: 'circleThreePoint', stdLibFnName: 'circleThreePoint',
type: 'xAbsolute', type: 'xAbsolute',
isConstrained: isNotLiteralArrayOrStatic(p1Details.expr.elements[0]), isConstrained: isNotLiteralArrayOrStatic(p1XArg),
sourceRange: [ sourceRange: topLevelRange(p1XArg.start, p1XArg.end),
p1Details.expr.elements[0].start,
p1Details.expr.elements[0].end,
0,
],
pathToNode: pathToP1XArg, pathToNode: pathToP1XArg,
value: code.slice( value: code.slice(p1XArg.start, p1XArg.end),
p1Details.expr.elements[0].start,
p1Details.expr.elements[0].end
),
argPosition: { argPosition: {
type: 'labeledArgArrayItem', type: 'labeledArgArrayItem',
index: 0, index: 0,
key: 'p1', key: 'p1',
}, },
filterValue: 'p1', filterValue: 'p1',
}, })
{ constraints.push({
stdLibFnName: 'circleThreePoint', stdLibFnName: 'circleThreePoint',
type: 'yAbsolute', type: 'yAbsolute',
isConstrained: isNotLiteralArrayOrStatic(p1Details.expr.elements[1]), isConstrained: isNotLiteralArrayOrStatic(p1YArg),
sourceRange: [ sourceRange: topLevelRange(p1YArg.start, p1YArg.end),
p1Details.expr.elements[1].start,
p1Details.expr.elements[1].end,
0,
],
pathToNode: pathToP1YArg, pathToNode: pathToP1YArg,
value: code.slice( value: code.slice(p1YArg.start, p1YArg.end),
p1Details.expr.elements[1].start,
p1Details.expr.elements[1].end
),
argPosition: { argPosition: {
type: 'labeledArgArrayItem', type: 'labeledArgArrayItem',
index: 1, index: 1,
key: 'p1', key: 'p1',
}, },
filterValue: 'p1', filterValue: 'p1',
}, })
{ }
if (p2Details.expr.elements.length >= 2) {
const p2XArg = p2Details.expr.elements[0]
const p2YArg = p2Details.expr.elements[1]
constraints.push({
stdLibFnName: 'circleThreePoint', stdLibFnName: 'circleThreePoint',
type: 'xAbsolute', type: 'xAbsolute',
isConstrained: isNotLiteralArrayOrStatic(p2Details.expr.elements[0]), isConstrained: isNotLiteralArrayOrStatic(p2XArg),
sourceRange: [ sourceRange: topLevelRange(p2XArg.start, p2XArg.end),
p2Details.expr.elements[0].start,
p2Details.expr.elements[0].end,
0,
],
pathToNode: pathToP2XArg, pathToNode: pathToP2XArg,
value: code.slice( value: code.slice(p2XArg.start, p2XArg.end),
p2Details.expr.elements[0].start,
p2Details.expr.elements[0].end
),
argPosition: { argPosition: {
type: 'labeledArgArrayItem', type: 'labeledArgArrayItem',
index: 0, index: 0,
key: 'p2', key: 'p2',
}, },
filterValue: 'p2', filterValue: 'p2',
}, })
{ constraints.push({
stdLibFnName: 'circleThreePoint', stdLibFnName: 'circleThreePoint',
type: 'yAbsolute', type: 'yAbsolute',
isConstrained: isNotLiteralArrayOrStatic(p2Details.expr.elements[1]), isConstrained: isNotLiteralArrayOrStatic(p2YArg),
sourceRange: [ sourceRange: topLevelRange(p2YArg.start, p2YArg.end),
p2Details.expr.elements[1].start,
p2Details.expr.elements[1].end,
0,
],
pathToNode: pathToP2YArg, pathToNode: pathToP2YArg,
value: code.slice( value: code.slice(p2YArg.start, p2YArg.end),
p2Details.expr.elements[1].start,
p2Details.expr.elements[1].end
),
argPosition: { argPosition: {
type: 'labeledArgArrayItem', type: 'labeledArgArrayItem',
index: 1, index: 1,
key: 'p2', key: 'p2',
}, },
filterValue: 'p2', filterValue: 'p2',
}, })
{ }
if (p3Details.expr.elements.length >= 2) {
const p3XArg = p3Details.expr.elements[0]
const p3YArg = p3Details.expr.elements[1]
constraints.push({
stdLibFnName: 'circleThreePoint', stdLibFnName: 'circleThreePoint',
type: 'xAbsolute', type: 'xAbsolute',
isConstrained: isNotLiteralArrayOrStatic(p3Details.expr.elements[0]), isConstrained: isNotLiteralArrayOrStatic(p3XArg),
sourceRange: [ sourceRange: topLevelRange(p3XArg.start, p3XArg.end),
p3Details.expr.elements[0].start,
p3Details.expr.elements[0].end,
0,
],
pathToNode: pathToP3XArg, pathToNode: pathToP3XArg,
value: code.slice( value: code.slice(p3XArg.start, p3XArg.end),
p3Details.expr.elements[0].start,
p3Details.expr.elements[0].end
),
argPosition: { argPosition: {
type: 'labeledArgArrayItem', type: 'labeledArgArrayItem',
index: 0, index: 0,
key: 'p3', key: 'p3',
}, },
filterValue: 'p3', filterValue: 'p3',
}, })
{ constraints.push({
stdLibFnName: 'circleThreePoint', stdLibFnName: 'circleThreePoint',
type: 'yAbsolute', type: 'yAbsolute',
isConstrained: isNotLiteralArrayOrStatic(p3Details.expr.elements[1]), isConstrained: isNotLiteralArrayOrStatic(p3YArg),
sourceRange: [ sourceRange: topLevelRange(p3YArg.start, p3YArg.end),
p3Details.expr.elements[1].start,
p3Details.expr.elements[1].end,
0,
],
pathToNode: pathToP3YArg, pathToNode: pathToP3YArg,
value: code.slice( value: code.slice(p3YArg.start, p3YArg.end),
p3Details.expr.elements[1].start,
p3Details.expr.elements[1].end
),
argPosition: { argPosition: {
type: 'labeledArgArrayItem', type: 'labeledArgArrayItem',
index: 1, index: 1,
key: 'p3', key: 'p3',
}, },
filterValue: 'p3', filterValue: 'p3',
}, })
] }
const finalConstraints: ConstrainInfo[] = [] const finalConstraints: ConstrainInfo[] = []
constraints.forEach((constraint) => { constraints.forEach((constraint) => {
if (!filterValue) { if (!filterValue) {

View File

@ -389,7 +389,20 @@ export function sketchFromKclValue(
} }
export const errFromErrWithOutputs = (e: any): KCLError => { 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( return new KCLError(
parsed.error.kind, parsed.error.kind,
parsed.error.details.msg, parsed.error.details.msg,

View File

@ -218,3 +218,6 @@ export const POOL_QUERY_PARAM = 'pool'
* @deprecated: supporting old share links with this. For new command URLs, use "cmd" * @deprecated: supporting old share links with this. For new command URLs, use "cmd"
*/ */
export const CREATE_FILE_URL_PARAM = 'create-file' 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 { APP_NAME } from '@src/lib/constants'
import { isDesktop } from '@src/lib/isDesktop' import { isDesktop } from '@src/lib/isDesktop'
import { openExternalBrowserIfDesktop } from '@src/lib/openWindow' import { openExternalBrowserIfDesktop } from '@src/lib/openWindow'
import { PATHS } from '@src/lib/paths'
import { Themes, getSystemTheme } from '@src/lib/theme' import { Themes, getSystemTheme } from '@src/lib/theme'
import { reportRejection } from '@src/lib/trap' import { reportRejection } from '@src/lib/trap'
import { toSync } from '@src/lib/utils' import { toSync } from '@src/lib/utils'
import { authActor, useSettings } from '@src/lib/singletons' import { authActor, useSettings } from '@src/lib/singletons'
import { APP_VERSION } from '@src/routes/utils' import { APP_VERSION, generateSignInUrl } from '@src/routes/utils'
const subtleBorder = const subtleBorder =
'border border-solid border-chalkboard-30 dark:border-chalkboard-80' 'border border-solid border-chalkboard-30 dark:border-chalkboard-80'
@ -36,11 +35,7 @@ const SignIn = () => {
const { const {
app: { theme }, app: { theme },
} = useSettings() } = useSettings()
const signInUrl = `${VITE_KC_SITE_BASE_URL}${ const signInUrl = generateSignInUrl()
PATHS.SIGN_IN
}?callbackUrl=${encodeURIComponent(
typeof window !== 'undefined' && window.location.href.replace('signin', '')
)}`
const kclSampleUrl = `${VITE_KC_SITE_BASE_URL}/docs/kcl-samples/car-wheel-assembly` const kclSampleUrl = `${VITE_KC_SITE_BASE_URL}/docs/kcl-samples/car-wheel-assembly`
const getThemeText = useCallback( 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 { isDesktop } from '@src/lib/isDesktop'
import { IS_PLAYWRIGHT_KEY } from '@src/lib/constants' import { IS_PLAYWRIGHT_KEY } from '@src/lib/constants'
import { PATHS } from '@src/lib/paths'
const isTestEnv = window?.localStorage.getItem(IS_PLAYWRIGHT_KEY) === 'true' 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}` 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', '')
)}`
}