Compare commits

..

13 Commits

51 changed files with 1791 additions and 526 deletions

View File

@ -3,17 +3,18 @@
NODE_ENV=development NODE_ENV=development
DEV=true DEV=true
# App
VITE_KC_API_WS_MODELING_URL=wss://api.dev.zoo.dev/ws/modeling/commands VITE_KC_API_WS_MODELING_URL=wss://api.dev.zoo.dev/ws/modeling/commands
VITE_KITTYCAD_API_BASE_URL=https://api.dev.zoo.dev VITE_KC_API_BASE_URL=https://api.dev.zoo.dev
VITE_KC_SITE_BASE_URL=https://dev.zoo.dev VITE_KC_SITE_BASE_URL=https://dev.zoo.dev
VITE_KC_SITE_APP_URL=https://app.dev.zoo.dev VITE_KC_SITE_APP_URL=https://app.dev.zoo.dev
VITE_KC_SKIP_AUTH=false
VITE_KC_CONNECTION_TIMEOUT_MS=5000 VITE_KC_CONNECTION_TIMEOUT_MS=5000
#VITE_WASM_URL="optional override of Wasm URL if not on default port 3000" #VITE_WASM_URL="optional way of overriding the wasm url, particular for unit tests which need this if you running not on the default 3000 port"
#VITE_KITTYCAD_API_TOKEN="required for testing, optional to skip auth in the app" #VITE_KC_DEV_TOKEN="optional token to skip auth in the app"
FAIL_ON_CONSOLE_ERRORS=true #token="required token for playwright. TODO: clean up env vars in #3973"
# KCL
RUST_BACKTRACE=1 RUST_BACKTRACE=1
PYO3_PYTHON=/usr/local/bin/python3 PYO3_PYTHON=/usr/local/bin/python3
#KITTYCAD_API_TOKEN=$VITE_KITTYCAD_API_TOKEN #KITTYCAD_API_TOKEN="required token for engine testing"
FAIL_ON_CONSOLE_ERRORS=true

View File

@ -1,8 +1,7 @@
NODE_ENV=production NODE_ENV=production
# App
VITE_KC_API_WS_MODELING_URL=wss://api.zoo.dev/ws/modeling/commands VITE_KC_API_WS_MODELING_URL=wss://api.zoo.dev/ws/modeling/commands
VITE_KITTYCAD_API_BASE_URL=https://api.zoo.dev VITE_KC_API_BASE_URL=https://api.zoo.dev
VITE_KC_SITE_BASE_URL=https://zoo.dev VITE_KC_SITE_BASE_URL=https://zoo.dev
VITE_KC_SITE_APP_URL=https://app.zoo.dev VITE_KC_SITE_APP_URL=https://app.zoo.dev
VITE_KC_SKIP_AUTH=false
VITE_KC_CONNECTION_TIMEOUT_MS=15000 VITE_KC_CONNECTION_TIMEOUT_MS=15000

View File

@ -157,7 +157,7 @@ jobs:
timeout_minutes: 5 timeout_minutes: 5
max_attempts: 5 max_attempts: 5
env: env:
VITE_KITTYCAD_API_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
TAB_API_URL: ${{ secrets.TAB_API_URL }} TAB_API_URL: ${{ secrets.TAB_API_URL }}
TAB_API_KEY: ${{ secrets.TAB_API_KEY }} TAB_API_KEY: ${{ secrets.TAB_API_KEY }}
CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }} CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
@ -169,7 +169,7 @@ jobs:
if: always() if: always()
run: npm run test:snapshots -- --last-failed --update-snapshots run: npm run test:snapshots -- --last-failed --update-snapshots
env: env:
VITE_KITTYCAD_API_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
TAB_API_URL: ${{ secrets.TAB_API_URL }} TAB_API_URL: ${{ secrets.TAB_API_URL }}
TAB_API_KEY: ${{ secrets.TAB_API_KEY }} TAB_API_KEY: ${{ secrets.TAB_API_KEY }}
CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }} CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
@ -284,7 +284,7 @@ jobs:
timeout_minutes: 5 timeout_minutes: 5
max_attempts: 5 max_attempts: 5
env: env:
VITE_KITTYCAD_API_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
TAB_API_URL: ${{ secrets.TAB_API_URL }} TAB_API_URL: ${{ secrets.TAB_API_URL }}
TAB_API_KEY: ${{ secrets.TAB_API_KEY }} TAB_API_KEY: ${{ secrets.TAB_API_KEY }}
CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }} CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
@ -410,7 +410,7 @@ jobs:
max_attempts: 9 max_attempts: 9
env: env:
FAIL_ON_CONSOLE_ERRORS: true FAIL_ON_CONSOLE_ERRORS: true
VITE_KITTYCAD_API_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
TAB_API_URL: ${{ secrets.TAB_API_URL }} TAB_API_URL: ${{ secrets.TAB_API_URL }}
TAB_API_KEY: ${{ secrets.TAB_API_KEY }} TAB_API_KEY: ${{ secrets.TAB_API_KEY }}
CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }} CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }}

View File

@ -62,7 +62,7 @@ jobs:
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }} if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
run: xvfb-run -a npm run test:unit run: xvfb-run -a npm run test:unit
env: env:
VITE_KITTYCAD_API_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
- name: Check for changes - name: Check for changes
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }} if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}

View File

@ -65,7 +65,7 @@ If you're not a Zoo employee you won't be able to access the dev environment, yo
### Development environment variables ### Development environment variables
The Copilot LSP plugin in the editor requires a Zoo API token to run. In production, we authenticate this with a token via cookie in the browser and device auth token in the desktop environment, but this token is inaccessible in the dev browser version because the cookie is considered "cross-site" (from `localhost` to `zoo.dev`). There is an optional environment variable called `VITE_KITTYCAD_API_TOKEN` that you can populate with a dev token in a `.env.development.local` file to not check it into Git, which will use that token instead of other methods for the LSP service. The Copilot LSP plugin in the editor requires a Zoo API token to run. In production, we authenticate this with a token via cookie in the browser and device auth token in the desktop environment, but this token is inaccessible in the dev browser version because the cookie is considered "cross-site" (from `localhost` to `zoo.dev`). There is an optional environment variable called `VITE_KC_DEV_TOKEN` that you can populate with a dev token in a `.env.development.local` file to not check it into Git, which will use that token instead of other methods for the LSP service.
### Developing in Chrome ### Developing in Chrome
@ -96,7 +96,7 @@ To package the app for your platform with electron-builder, run `npm run tronb:p
Prepare these system dependencies: Prepare these system dependencies:
- Set `$VITE_KITTYCAD_API_TOKEN` from https://zoo.dev/account/api-tokens - Set $token from https://zoo.dev/account/api-tokens
#### Snapshot tests (Google Chrome on Ubuntu only) #### Snapshot tests (Google Chrome on Ubuntu only)
@ -259,7 +259,7 @@ If the application needs to overwrite the known file on disk use this pattern. T
- `npm run circular-deps:overwrite` - `npm run circular-deps:overwrite`
- `npm run url-checker:overwrite` - `npm run url-checker:overwrite`
#### Diff baseline and current #### Diff baseline and current
These commands will write a /tmp/ file on disk and compare it to the known file in the repository. This command will also be used in the CI CD pipeline for automated checks These commands will write a /tmp/ file on disk and compare it to the known file in the repository. This command will also be used in the CI CD pipeline for automated checks

View File

@ -49,7 +49,7 @@ RUST_SOURCES := $(wildcard rust/**/*.rs)
REACT_SOURCES := $(wildcard src/*.tsx) $(wildcard src/**/*.tsx) REACT_SOURCES := $(wildcard src/*.tsx) $(wildcard src/**/*.tsx)
TYPESCRIPT_SOURCES := tsconfig.* $(wildcard src/*.ts) $(wildcard src/**/*.ts) TYPESCRIPT_SOURCES := tsconfig.* $(wildcard src/*.ts) $(wildcard src/**/*.ts)
VITE_SOURCES := $(wildcard vite.*) $(wildcard vite/**/*.tsx) .env* VITE_SOURCES := $(wildcard vite.*) $(wildcard vite/**/*.tsx)
.PHONY: build .PHONY: build
build: install public/kcl_wasm_lib_bg.wasm public/kcl-samples/manifest.json .vite/build/main.js build: install public/kcl_wasm_lib_bg.wasm public/kcl-samples/manifest.json .vite/build/main.js

View File

@ -1,5 +1,6 @@
import { expect, test } from '@e2e/playwright/zoo-test' import { expect, test } from '@e2e/playwright/zoo-test'
// test file is for testing auth functionality
test.describe('Authentication tests', () => { test.describe('Authentication tests', () => {
test( test(
`The user can sign out and back in`, `The user can sign out and back in`,
@ -12,12 +13,22 @@ test.describe('Authentication tests', () => {
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.projectSection.waitFor() await homePage.projectSection.waitFor()
// This is only needed as an override to test-utils' setup() for this test
await page.addInitScript(() => {
localStorage.setItem('TOKEN_PERSIST_KEY', '')
})
await test.step('Click on sign out and expect sign in page', async () => { await test.step('Click on sign out and expect sign in page', async () => {
await toolbar.userSidebarButton.click() await toolbar.userSidebarButton.click()
await toolbar.signOutButton.click() await toolbar.signOutButton.click()
await expect(signInPage.signInButton).toBeVisible() await expect(signInPage.signInButton).toBeVisible()
}) })
await test.step("Refresh doesn't log the user back in", async () => {
await page.reload()
await expect(signInPage.signInButton).toBeVisible()
})
await test.step('Click on sign in and cancel, click again and expect different code', async () => { await test.step('Click on sign in and cancel, click again and expect different code', async () => {
await signInPage.signInButton.click() await signInPage.signInButton.click()
await expect(signInPage.userCode).toBeVisible() await expect(signInPage.userCode).toBeVisible()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

View File

@ -17,7 +17,7 @@ import dotenv from 'dotenv'
const NODE_ENV = process.env.NODE_ENV || 'development' const NODE_ENV = process.env.NODE_ENV || 'development'
dotenv.config({ path: [`.env.${NODE_ENV}.local`, `.env.${NODE_ENV}`] }) dotenv.config({ path: [`.env.${NODE_ENV}.local`, `.env.${NODE_ENV}`] })
export const token = process.env.VITE_KITTYCAD_API_TOKEN || '' export const token = process.env.token || ''
import type { ProjectConfiguration } from '@rust/kcl-lib/bindings/ProjectConfiguration' import type { ProjectConfiguration } from '@rust/kcl-lib/bindings/ProjectConfiguration'

View File

@ -1105,6 +1105,270 @@ part002 = startSketchOn(XZ)
// checking the count of the overlays is a good proxy check that the client sketch scene is in a good state // checking the count of the overlays is a good proxy check that the client sketch scene is in a good state
await expect(page.getByTestId('segment-overlay')).toHaveCount(3) await expect(page.getByTestId('segment-overlay')).toHaveCount(3)
}) })
test.describe('Sketch scaling on first constraint', () => {
test('Should scale entire sketch when constraining first dimension with scale checkbox enabled', async ({
page,
homePage,
scene,
cmdBar,
}) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`sketch001 = startSketchOn(XZ)
profile001 = startProfile(sketch001, at = [100, 100])
|> line(end = [200, 0])
|> line(end = [0, 200])
|> line(end = [-200, 0])
|> close()
profile002 = startProfile(sketch001, at = [400, 400])
|> circle(center = [0, 0], radius = 50)`
)
})
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await scene.settled(cmdBar)
// Click on the first line segment to select it
await page.getByText('line(end = [200, 0])').click()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
// Wait for overlays to populate
await page.waitForTimeout(1000)
// Click on the first line segment in the sketch
const line1 = await u.getSegmentBodyCoords(`[data-overlay-index="1"]`)
await page.mouse.click(line1.x, line1.y)
await page.waitForTimeout(100)
// Open constraints menu and click length constraint
await page
.getByRole('button', {
name: 'constraints: open menu',
})
.click()
await page.getByTestId('constraint-length').click()
// Verify the scale sketch checkbox is present and enabled
const scaleCheckbox = page.getByTestId('scale-sketch-checkbox')
await expect(scaleCheckbox).toBeVisible()
await expect(scaleCheckbox).toBeEnabled()
await expect(scaleCheckbox).toBeChecked() // Should be checked by default since no constraints exist
// Enter new value (100, which is half of original 200)
await page
.getByTestId('cmd-bar-arg-value')
.getByRole('textbox')
.fill('100')
// Ensure scale checkbox is still checked
await expect(scaleCheckbox).toBeChecked()
// Submit the constraint
await page
.getByRole('button', {
name: 'arrow right Continue',
})
.click()
// Wait for the changes to be applied
await page.waitForTimeout(1000)
// Verify the constraint was applied with a variable
await expect(page.locator('.cm-content')).toContainText('length001 = 100')
await expect(page.locator('.cm-content')).toContainText(
'line(end = [length001, 0])'
)
// Verify scaling occurred - all dimensions should be scaled by 0.5 (100/200)
// Original: line(end = [0, 200]) -> Scaled: line(end = [0, 100])
await expect(page.locator('.cm-content')).toContainText(
'line(end = [0, 100])'
)
// Original: line(end = [-200, 0]) -> Scaled: line(end = [-100, 0])
await expect(page.locator('.cm-content')).toContainText(
'line(end = [-100, 0])'
)
// Original: startProfile(at = [100, 100]) -> Scaled: startProfile(at = [50, 50])
await expect(page.locator('.cm-content')).toContainText(
'startProfile(sketch001, at = [50, 50])'
)
// Original: startProfile(at = [400, 400]) -> Scaled: startProfile(at = [200, 200])
await expect(page.locator('.cm-content')).toContainText(
'startProfile(sketch001, at = [200, 200])'
)
// Original: radius = 50 -> Scaled: radius = 25
await expect(page.locator('.cm-content')).toContainText('radius = 25')
})
test('Should not scale sketch when constraining with scale checkbox disabled', async ({
page,
homePage,
scene,
cmdBar,
}) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`sketch001 = startSketchOn(XZ)
profile001 = startProfile(sketch001, at = [100, 100])
|> line(end = [200, 0])
|> line(end = [0, 200])
|> close()`
)
})
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await scene.settled(cmdBar)
// Click on the first line segment to select it
await page.getByText('line(end = [200, 0])').click()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
// Wait for overlays to populate
await page.waitForTimeout(1000)
// Click on the first line segment in the sketch
const line1 = await u.getSegmentBodyCoords(`[data-overlay-index="1"]`)
await page.mouse.click(line1.x, line1.y)
await page.waitForTimeout(100)
// Open constraints menu and click length constraint
await page
.getByRole('button', {
name: 'constraints: open menu',
})
.click()
await page.getByTestId('constraint-length').click()
// Verify the scale sketch checkbox is present and enabled
const scaleCheckbox = page.getByTestId('scale-sketch-checkbox')
await expect(scaleCheckbox).toBeVisible()
await expect(scaleCheckbox).toBeEnabled()
await expect(scaleCheckbox).toBeChecked()
// Uncheck the scale checkbox
await scaleCheckbox.click()
await expect(scaleCheckbox).not.toBeChecked()
// Enter new value (100, which is half of original 200)
await page
.getByTestId('cmd-bar-arg-value')
.getByRole('textbox')
.fill('100')
// Submit the constraint
await page
.getByRole('button', {
name: 'arrow right Continue',
})
.click()
// Wait for the changes to be applied
await page.waitForTimeout(1000)
// Verify the constraint was applied with a variable
await expect(page.locator('.cm-content')).toContainText('length001 = 100')
await expect(page.locator('.cm-content')).toContainText(
'line(end = [length001, 0])'
)
// Verify NO scaling occurred - other dimensions should remain unchanged
await expect(page.locator('.cm-content')).toContainText(
'line(end = [0, 200])'
) // Should remain 200, not 100
await expect(page.locator('.cm-content')).toContainText(
'startProfile(sketch001, at = [100, 100])'
) // Should remain [100, 100]
})
test('Should disable scale checkbox when sketch already has constraints', async ({
page,
homePage,
scene,
cmdBar,
}) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`length_var = 150
sketch001 = startSketchOn(XZ)
profile001 = startProfile(sketch001, at = [100, 100])
|> line(end = [length_var, 0])
|> line(end = [0, 200])
|> close()`
)
})
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await scene.settled(cmdBar)
// Click on the second line segment (the one without constraints)
await page.getByText('line(end = [0, 200])').click()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
// Wait for overlays to populate
await page.waitForTimeout(1000)
// Click on the second line segment in the sketch
const line2 = await u.getSegmentBodyCoords(`[data-overlay-index="2"]`)
await page.mouse.click(line2.x, line2.y)
await page.waitForTimeout(100)
// Open constraints menu and click length constraint
await page
.getByRole('button', {
name: 'constraints: open menu',
})
.click()
await page.getByTestId('constraint-length').click()
// Verify the scale sketch checkbox is present but disabled/unchecked
// because the sketch already has constraints (length_var)
const scaleCheckbox = page.getByTestId('scale-sketch-checkbox')
await expect(scaleCheckbox).toBeVisible()
await expect(scaleCheckbox).not.toBeChecked() // Should be unchecked because constraints exist
// Enter new value
await page
.getByTestId('cmd-bar-arg-value')
.getByRole('textbox')
.fill('100')
// Submit the constraint
await page
.getByRole('button', {
name: 'arrow right Continue',
})
.click()
// Wait for the changes to be applied
await page.waitForTimeout(1000)
// Verify the constraint was applied
await expect(page.locator('.cm-content')).toContainText('length002 = 100')
// Verify existing constraint and values remain unchanged
await expect(page.locator('.cm-content')).toContainText(
'length_var = 150'
)
await expect(page.locator('.cm-content')).toContainText(
'line(end = [length_var, 0])'
)
})
})
}) })
test.describe('Electron constraint tests', () => { test.describe('Electron constraint tests', () => {
test( test(

7
interface.d.ts vendored
View File

@ -72,13 +72,16 @@ export interface IElectronAPI {
} }
process: { process: {
env: { env: {
BASE_URL: string
IS_PLAYWRIGHT: string IS_PLAYWRIGHT: string
VITE_KITTYCAD_API_TOKEN: string VITE_KC_DEV_TOKEN: string
VITE_KC_API_WS_MODELING_URL: string VITE_KC_API_WS_MODELING_URL: string
VITE_KITTYCAD_API_BASE_URL: string VITE_KC_API_BASE_URL: string
VITE_KC_SITE_BASE_URL: string VITE_KC_SITE_BASE_URL: string
VITE_KC_SITE_APP_URL: string VITE_KC_SITE_APP_URL: string
VITE_KC_SKIP_AUTH: string
VITE_KC_CONNECTION_TIMEOUT_MS: string VITE_KC_CONNECTION_TIMEOUT_MS: string
VITE_KC_DEV_TOKEN: string
NODE_ENV: string NODE_ENV: string
PROD: string PROD: string
DEV: string DEV: string

234
package-lock.json generated
View File

@ -149,7 +149,7 @@
"ts-node": "^10.0.0", "ts-node": "^10.0.0",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"typescript-eslint": "^8.30.1", "typescript-eslint": "^8.30.1",
"vite": "^5.4.19", "vite": "^5.4.18",
"vite-plugin-package-version": "^1.1.0", "vite-plugin-package-version": "^1.1.0",
"vite-plugin-top-level-await": "^1.5.0", "vite-plugin-top-level-await": "^1.5.0",
"vite-tsconfig-paths": "^4.3.2", "vite-tsconfig-paths": "^4.3.2",
@ -3652,9 +3652,9 @@
} }
}, },
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz",
"integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", "integrity": "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@ -3668,9 +3668,9 @@
} }
}, },
"node_modules/@esbuild/android-arm": { "node_modules/@esbuild/android-arm": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.3.tgz",
"integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", "integrity": "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -3684,9 +3684,9 @@
} }
}, },
"node_modules/@esbuild/android-arm64": { "node_modules/@esbuild/android-arm64": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.3.tgz",
"integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", "integrity": "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -3700,9 +3700,9 @@
} }
}, },
"node_modules/@esbuild/android-x64": { "node_modules/@esbuild/android-x64": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.3.tgz",
"integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", "integrity": "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -3716,9 +3716,9 @@
} }
}, },
"node_modules/@esbuild/darwin-arm64": { "node_modules/@esbuild/darwin-arm64": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz",
"integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", "integrity": "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -3732,9 +3732,9 @@
} }
}, },
"node_modules/@esbuild/darwin-x64": { "node_modules/@esbuild/darwin-x64": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.3.tgz",
"integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", "integrity": "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -3748,9 +3748,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-arm64": { "node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.3.tgz",
"integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", "integrity": "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -3764,9 +3764,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-x64": { "node_modules/@esbuild/freebsd-x64": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.3.tgz",
"integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", "integrity": "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -3780,9 +3780,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm": { "node_modules/@esbuild/linux-arm": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.3.tgz",
"integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", "integrity": "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -3796,9 +3796,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm64": { "node_modules/@esbuild/linux-arm64": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.3.tgz",
"integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", "integrity": "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -3812,9 +3812,9 @@
} }
}, },
"node_modules/@esbuild/linux-ia32": { "node_modules/@esbuild/linux-ia32": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.3.tgz",
"integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", "integrity": "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -3828,9 +3828,9 @@
} }
}, },
"node_modules/@esbuild/linux-loong64": { "node_modules/@esbuild/linux-loong64": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.3.tgz",
"integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", "integrity": "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@ -3844,9 +3844,9 @@
} }
}, },
"node_modules/@esbuild/linux-mips64el": { "node_modules/@esbuild/linux-mips64el": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.3.tgz",
"integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", "integrity": "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==",
"cpu": [ "cpu": [
"mips64el" "mips64el"
], ],
@ -3860,9 +3860,9 @@
} }
}, },
"node_modules/@esbuild/linux-ppc64": { "node_modules/@esbuild/linux-ppc64": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.3.tgz",
"integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", "integrity": "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@ -3876,9 +3876,9 @@
} }
}, },
"node_modules/@esbuild/linux-riscv64": { "node_modules/@esbuild/linux-riscv64": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.3.tgz",
"integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", "integrity": "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@ -3892,9 +3892,9 @@
} }
}, },
"node_modules/@esbuild/linux-s390x": { "node_modules/@esbuild/linux-s390x": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.3.tgz",
"integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", "integrity": "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@ -3908,9 +3908,9 @@
} }
}, },
"node_modules/@esbuild/linux-x64": { "node_modules/@esbuild/linux-x64": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.3.tgz",
"integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", "integrity": "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -3924,9 +3924,9 @@
} }
}, },
"node_modules/@esbuild/netbsd-arm64": { "node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.3.tgz",
"integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", "integrity": "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -3940,9 +3940,9 @@
} }
}, },
"node_modules/@esbuild/netbsd-x64": { "node_modules/@esbuild/netbsd-x64": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.3.tgz",
"integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", "integrity": "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -3956,9 +3956,9 @@
} }
}, },
"node_modules/@esbuild/openbsd-arm64": { "node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.3.tgz",
"integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", "integrity": "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -3972,9 +3972,9 @@
} }
}, },
"node_modules/@esbuild/openbsd-x64": { "node_modules/@esbuild/openbsd-x64": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.3.tgz",
"integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", "integrity": "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -3988,9 +3988,9 @@
} }
}, },
"node_modules/@esbuild/sunos-x64": { "node_modules/@esbuild/sunos-x64": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.3.tgz",
"integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", "integrity": "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -4004,9 +4004,9 @@
} }
}, },
"node_modules/@esbuild/win32-arm64": { "node_modules/@esbuild/win32-arm64": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.3.tgz",
"integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", "integrity": "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -4020,9 +4020,9 @@
} }
}, },
"node_modules/@esbuild/win32-ia32": { "node_modules/@esbuild/win32-ia32": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.3.tgz",
"integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", "integrity": "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -4036,9 +4036,9 @@
} }
}, },
"node_modules/@esbuild/win32-x64": { "node_modules/@esbuild/win32-x64": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.3.tgz",
"integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", "integrity": "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -13219,9 +13219,9 @@
"optional": true "optional": true
}, },
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz",
"integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", "integrity": "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"bin": { "bin": {
@ -13231,31 +13231,31 @@
"node": ">=18" "node": ">=18"
}, },
"optionalDependencies": { "optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.4", "@esbuild/aix-ppc64": "0.25.3",
"@esbuild/android-arm": "0.25.4", "@esbuild/android-arm": "0.25.3",
"@esbuild/android-arm64": "0.25.4", "@esbuild/android-arm64": "0.25.3",
"@esbuild/android-x64": "0.25.4", "@esbuild/android-x64": "0.25.3",
"@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-arm64": "0.25.3",
"@esbuild/darwin-x64": "0.25.4", "@esbuild/darwin-x64": "0.25.3",
"@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.3",
"@esbuild/freebsd-x64": "0.25.4", "@esbuild/freebsd-x64": "0.25.3",
"@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm": "0.25.3",
"@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-arm64": "0.25.3",
"@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-ia32": "0.25.3",
"@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-loong64": "0.25.3",
"@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-mips64el": "0.25.3",
"@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-ppc64": "0.25.3",
"@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-riscv64": "0.25.3",
"@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-s390x": "0.25.3",
"@esbuild/linux-x64": "0.25.4", "@esbuild/linux-x64": "0.25.3",
"@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.3",
"@esbuild/netbsd-x64": "0.25.4", "@esbuild/netbsd-x64": "0.25.3",
"@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.3",
"@esbuild/openbsd-x64": "0.25.4", "@esbuild/openbsd-x64": "0.25.3",
"@esbuild/sunos-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.3",
"@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-arm64": "0.25.3",
"@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-ia32": "0.25.3",
"@esbuild/win32-x64": "0.25.4" "@esbuild/win32-x64": "0.25.3"
} }
}, },
"node_modules/escalade": { "node_modules/escalade": {
@ -25132,10 +25132,11 @@
"optional": true "optional": true
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "5.4.19", "version": "5.4.18",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.18.tgz",
"integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", "integrity": "sha512-1oDcnEp3lVyHCuQ2YFelM4Alm2o91xNoMncRm1U7S+JdYfYOvbiGZ3/CxGttrOu2M/KcGz7cRC2DoNUA6urmMA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "^0.21.3", "esbuild": "^0.21.3",
"postcss": "^8.4.43", "postcss": "^8.4.43",
@ -26639,25 +26640,10 @@
"vscode-uri": "^3.1.0" "vscode-uri": "^3.1.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^24.0.7", "@types/node": "^22.14.1",
"ts-node": "^10.9.2" "ts-node": "^10.9.2"
} }
}, },
"packages/codemirror-lsp-client/node_modules/@types/node": {
"version": "24.0.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.10.tgz",
"integrity": "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==",
"dev": true,
"dependencies": {
"undici-types": "~7.8.0"
}
},
"packages/codemirror-lsp-client/node_modules/undici-types": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
"integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
"dev": true
},
"rust/kcl-language-server": { "rust/kcl-language-server": {
"version": "0.0.0", "version": "0.0.0",
"license": "MIT", "license": "MIT",
@ -26675,7 +26661,7 @@
"@vscode/test-electron": "^2.4.1", "@vscode/test-electron": "^2.4.1",
"@vscode/vsce": "^3.3.2", "@vscode/vsce": "^3.3.2",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"esbuild": "^0.25.4", "esbuild": "^0.25.3",
"glob": "^11.0.1", "glob": "^11.0.1",
"mocha": "^11.1.0", "mocha": "^11.1.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"

View File

@ -227,7 +227,7 @@
"ts-node": "^10.0.0", "ts-node": "^10.0.0",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"typescript-eslint": "^8.30.1", "typescript-eslint": "^8.30.1",
"vite": "^5.4.19", "vite": "^5.4.18",
"vite-plugin-package-version": "^1.1.0", "vite-plugin-package-version": "^1.1.0",
"vite-plugin-top-level-await": "^1.5.0", "vite-plugin-top-level-await": "^1.5.0",
"vite-tsconfig-paths": "^4.3.2", "vite-tsconfig-paths": "^4.3.2",

View File

@ -123,7 +123,7 @@
"@vscode/test-electron": "^2.4.1", "@vscode/test-electron": "^2.4.1",
"@vscode/vsce": "^3.3.2", "@vscode/vsce": "^3.3.2",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"esbuild": "^0.25.4", "esbuild": "^0.25.3",
"glob": "^11.0.1", "glob": "^11.0.1",
"mocha": "^11.1.0", "mocha": "^11.1.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"

View File

@ -10,76 +10,71 @@ DATA;
NAMED_UNIT(*) NAMED_UNIT(*)
SI_UNIT($, .METRE.) SI_UNIT($, .METRE.)
); );
#2 = ( #2 = UNCERTAINTY_MEASURE_WITH_UNIT(0.00001, #1, 'DISTANCE_ACCURACY_VALUE', $);
NAMED_UNIT(*) #3 = (
PLANE_ANGLE_UNIT()
SI_UNIT($, .RADIAN.)
);
#3 = UNCERTAINTY_MEASURE_WITH_UNIT(0.00001, #1, 'DISTANCE_ACCURACY_VALUE', $);
#4 = (
GEOMETRIC_REPRESENTATION_CONTEXT(3) GEOMETRIC_REPRESENTATION_CONTEXT(3)
GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#3)) GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#2))
GLOBAL_UNIT_ASSIGNED_CONTEXT((#1, #2)) GLOBAL_UNIT_ASSIGNED_CONTEXT((#1))
REPRESENTATION_CONTEXT('', '3D') REPRESENTATION_CONTEXT('', '3D')
); );
#5 = CARTESIAN_POINT('NONE', (0.015, -0.01, -0.005)); #4 = CARTESIAN_POINT('NONE', (0.015, -0.01, -0.005));
#6 = VERTEX_POINT('NONE', #5); #5 = VERTEX_POINT('NONE', #4);
#7 = CARTESIAN_POINT('NONE', (0.015, 0, -0.005)); #6 = CARTESIAN_POINT('NONE', (0.015, 0, -0.005));
#8 = VERTEX_POINT('NONE', #7); #7 = VERTEX_POINT('NONE', #6);
#9 = DIRECTION('NONE', (1, 0, -0)); #8 = DIRECTION('NONE', (1, 0, -0));
#10 = DIRECTION('NONE', (0, 1, 0)); #9 = DIRECTION('NONE', (0, 1, 0));
#11 = CARTESIAN_POINT('NONE', (0.005, -0.01, -0.005)); #10 = CARTESIAN_POINT('NONE', (0.005, -0.01, -0.005));
#12 = AXIS2_PLACEMENT_3D('NONE', #11, #10, #9); #11 = AXIS2_PLACEMENT_3D('NONE', #10, #9, #8);
#13 = CIRCLE('NONE', #12, 0.01); #12 = CIRCLE('NONE', #11, 0.01);
#14 = DIRECTION('NONE', (0, 1, 0)); #13 = DIRECTION('NONE', (0, 1, 0));
#15 = VECTOR('NONE', #14, 1); #14 = VECTOR('NONE', #13, 1);
#16 = CARTESIAN_POINT('NONE', (0.015, -0.01, -0.005)); #15 = CARTESIAN_POINT('NONE', (0.015, -0.01, -0.005));
#17 = LINE('NONE', #16, #15); #16 = LINE('NONE', #15, #14);
#18 = DIRECTION('NONE', (1, 0, -0)); #17 = DIRECTION('NONE', (1, 0, -0));
#19 = DIRECTION('NONE', (0, 1, 0)); #18 = DIRECTION('NONE', (0, 1, 0));
#20 = CARTESIAN_POINT('NONE', (0.005, 0, -0.005)); #19 = CARTESIAN_POINT('NONE', (0.005, 0, -0.005));
#21 = AXIS2_PLACEMENT_3D('NONE', #20, #19, #18); #20 = AXIS2_PLACEMENT_3D('NONE', #19, #18, #17);
#22 = CIRCLE('NONE', #21, 0.01); #21 = CIRCLE('NONE', #20, 0.01);
#23 = EDGE_CURVE('NONE', #6, #6, #13, .T.); #22 = EDGE_CURVE('NONE', #5, #5, #12, .T.);
#24 = EDGE_CURVE('NONE', #6, #8, #17, .T.); #23 = EDGE_CURVE('NONE', #5, #7, #16, .T.);
#25 = EDGE_CURVE('NONE', #8, #8, #22, .T.); #24 = EDGE_CURVE('NONE', #7, #7, #21, .T.);
#26 = CARTESIAN_POINT('NONE', (0.005, -0.005, -0.005)); #25 = CARTESIAN_POINT('NONE', (0.005, -0.005, -0.005));
#27 = DIRECTION('NONE', (0, 1, 0)); #26 = DIRECTION('NONE', (0, 1, 0));
#28 = DIRECTION('NONE', (1, 0, -0)); #27 = DIRECTION('NONE', (1, 0, -0));
#29 = AXIS2_PLACEMENT_3D('NONE', #26, #27, #28); #28 = AXIS2_PLACEMENT_3D('NONE', #25, #26, #27);
#30 = CYLINDRICAL_SURFACE('NONE', #29, 0.01); #29 = CYLINDRICAL_SURFACE('NONE', #28, 0.01);
#31 = CARTESIAN_POINT('NONE', (0, -0.01, -0)); #30 = CARTESIAN_POINT('NONE', (0, -0.01, -0));
#32 = DIRECTION('NONE', (0, 1, 0)); #31 = DIRECTION('NONE', (0, 1, 0));
#33 = AXIS2_PLACEMENT_3D('NONE', #31, #32, $); #32 = AXIS2_PLACEMENT_3D('NONE', #30, #31, $);
#34 = PLANE('NONE', #33); #33 = PLANE('NONE', #32);
#35 = CARTESIAN_POINT('NONE', (0, 0, -0)); #34 = CARTESIAN_POINT('NONE', (0, 0, -0));
#36 = DIRECTION('NONE', (0, 1, 0)); #35 = DIRECTION('NONE', (0, 1, 0));
#37 = AXIS2_PLACEMENT_3D('NONE', #35, #36, $); #36 = AXIS2_PLACEMENT_3D('NONE', #34, #35, $);
#38 = PLANE('NONE', #37); #37 = PLANE('NONE', #36);
#39 = ORIENTED_EDGE('NONE', *, *, #23, .T.); #38 = ORIENTED_EDGE('NONE', *, *, #22, .T.);
#40 = ORIENTED_EDGE('NONE', *, *, #25, .F.); #39 = ORIENTED_EDGE('NONE', *, *, #24, .F.);
#41 = EDGE_LOOP('NONE', (#39)); #40 = EDGE_LOOP('NONE', (#38));
#42 = FACE_BOUND('NONE', #41, .T.); #41 = FACE_BOUND('NONE', #40, .T.);
#43 = EDGE_LOOP('NONE', (#40)); #42 = EDGE_LOOP('NONE', (#39));
#44 = FACE_BOUND('NONE', #43, .T.); #43 = FACE_BOUND('NONE', #42, .T.);
#45 = ADVANCED_FACE('NONE', (#42, #44), #30, .T.); #44 = ADVANCED_FACE('NONE', (#41, #43), #29, .T.);
#46 = ORIENTED_EDGE('NONE', *, *, #23, .F.); #45 = ORIENTED_EDGE('NONE', *, *, #22, .F.);
#47 = EDGE_LOOP('NONE', (#46)); #46 = EDGE_LOOP('NONE', (#45));
#48 = FACE_BOUND('NONE', #47, .T.); #47 = FACE_BOUND('NONE', #46, .T.);
#49 = ADVANCED_FACE('NONE', (#48), #34, .F.); #48 = ADVANCED_FACE('NONE', (#47), #33, .F.);
#50 = ORIENTED_EDGE('NONE', *, *, #25, .T.); #49 = ORIENTED_EDGE('NONE', *, *, #24, .T.);
#51 = EDGE_LOOP('NONE', (#50)); #50 = EDGE_LOOP('NONE', (#49));
#52 = FACE_BOUND('NONE', #51, .T.); #51 = FACE_BOUND('NONE', #50, .T.);
#53 = ADVANCED_FACE('NONE', (#52), #38, .T.); #52 = ADVANCED_FACE('NONE', (#51), #37, .T.);
#54 = CLOSED_SHELL('NONE', (#45, #49, #53)); #53 = CLOSED_SHELL('NONE', (#44, #48, #52));
#55 = MANIFOLD_SOLID_BREP('NONE', #54); #54 = MANIFOLD_SOLID_BREP('NONE', #53);
#56 = APPLICATION_CONTEXT('configuration controlled 3D design of mechanical parts and assemblies'); #55 = APPLICATION_CONTEXT('configuration controlled 3D design of mechanical parts and assemblies');
#57 = PRODUCT_DEFINITION_CONTEXT('part definition', #56, 'design'); #56 = PRODUCT_DEFINITION_CONTEXT('part definition', #55, 'design');
#58 = PRODUCT('UNIDENTIFIED_PRODUCT', 'NONE', $, ()); #57 = PRODUCT('UNIDENTIFIED_PRODUCT', 'NONE', $, ());
#59 = PRODUCT_DEFINITION_FORMATION('', $, #58); #58 = PRODUCT_DEFINITION_FORMATION('', $, #57);
#60 = PRODUCT_DEFINITION('design', $, #59, #57); #59 = PRODUCT_DEFINITION('design', $, #58, #56);
#61 = PRODUCT_DEFINITION_SHAPE('NONE', $, #60); #60 = PRODUCT_DEFINITION_SHAPE('NONE', $, #59);
#62 = ADVANCED_BREP_SHAPE_REPRESENTATION('NONE', (#55), #4); #61 = ADVANCED_BREP_SHAPE_REPRESENTATION('NONE', (#54), #3);
#63 = SHAPE_DEFINITION_REPRESENTATION(#61, #62); #62 = SHAPE_DEFINITION_REPRESENTATION(#60, #61);
ENDSEC; ENDSEC;
END-ISO-10303-21; END-ISO-10303-21;

View File

@ -994,39 +994,6 @@ impl Node<MemberExpression> {
// Check the property and object match -- e.g. ints for arrays, strs for objects. // Check the property and object match -- e.g. ints for arrays, strs for objects.
match (object, property, self.computed) { match (object, property, self.computed) {
(KclValue::Plane { value: plane }, Property::String(property), false) => match property.as_str() {
"yAxis" => {
let (p, u) = plane.info.y_axis.as_3_dims();
Ok(KclValue::array_from_point3d(
p,
NumericType::Known(crate::exec::UnitType::Length(u)),
vec![meta],
))
}
"xAxis" => {
let (p, u) = plane.info.x_axis.as_3_dims();
Ok(KclValue::array_from_point3d(
p,
NumericType::Known(crate::exec::UnitType::Length(u)),
vec![meta],
))
}
"origin" => {
let (p, u) = plane.info.origin.as_3_dims();
Ok(KclValue::array_from_point3d(
p,
NumericType::Known(crate::exec::UnitType::Length(u)),
vec![meta],
))
}
other => Err(KclError::new_undefined_value(
KclErrorDetails::new(
format!("Property '{other}' not found in plane"),
vec![self.clone().into()],
),
None,
)),
},
(KclValue::Object { value: map, meta: _ }, Property::String(property), false) => { (KclValue::Object { value: map, meta: _ }, Property::String(property), false) => {
if let Some(value) = map.get(&property) { if let Some(value) = map.get(&property) {
Ok(value.to_owned()) Ok(value.to_owned())
@ -1046,22 +1013,7 @@ impl Node<MemberExpression> {
vec![self.clone().into()], vec![self.clone().into()],
))) )))
} }
(KclValue::Object { value: map, .. }, p @ Property::UInt(i), _) => { (KclValue::Object { .. }, p, _) => {
if i == 0
&& let Some(value) = map.get("x")
{
return Ok(value.to_owned());
}
if i == 1
&& let Some(value) = map.get("y")
{
return Ok(value.to_owned());
}
if i == 2
&& let Some(value) = map.get("z")
{
return Ok(value.to_owned());
}
let t = p.type_name(); let t = p.type_name();
let article = article_for(t); let article = article_for(t);
Err(KclError::new_semantic(KclErrorDetails::new( Err(KclError::new_semantic(KclErrorDetails::new(
@ -2253,12 +2205,4 @@ y = x[0mm + 1]
"#; "#;
parse_execute(ast).await.unwrap_err(); parse_execute(ast).await.unwrap_err();
} }
#[tokio::test(flavor = "multi_thread")]
async fn getting_property_of_plane() {
// let ast = include_str!("../../tests/inputs/planestuff.kcl");
let ast = std::fs::read_to_string("tests/inputs/planestuff.kcl").unwrap();
parse_execute(&ast).await.unwrap();
}
} }

View File

@ -921,12 +921,6 @@ impl Point3d {
units: UnitLen::Unknown, units: UnitLen::Unknown,
} }
} }
pub fn as_3_dims(&self) -> ([f64; 3], UnitLen) {
let p = [self.x, self.y, self.z];
let u = self.units;
(p, u)
}
} }
impl From<[TyF64; 3]> for Point3d { impl From<[TyF64; 3]> for Point3d {

View File

@ -458,31 +458,6 @@ impl KclValue {
} }
} }
/// Put the point into a KCL point.
pub fn array_from_point3d(p: [f64; 3], ty: NumericType, meta: Vec<Metadata>) -> Self {
let [x, y, z] = p;
Self::HomArray {
value: vec![
Self::Number {
value: x,
meta: meta.clone(),
ty,
},
Self::Number {
value: y,
meta: meta.clone(),
ty,
},
Self::Number {
value: z,
meta: meta.clone(),
ty,
},
],
ty: ty.into(),
}
}
pub(crate) fn as_usize(&self) -> Option<usize> { pub(crate) fn as_usize(&self) -> Option<usize> {
match self { match self {
KclValue::Number { value, .. } => crate::try_f64_to_usize(*value), KclValue::Number { value, .. } => crate::try_f64_to_usize(*value),

View File

@ -1,60 +0,0 @@
// There are 3 ways to define a plane in KCL, according to https://zoo.dev/docs/kcl-std/types/std-types-Plane
// - A default plane
// - Modifying a default plane e.g. via offsetPlane
// - Defining your own struct
// This file tests they all work equivalently.
// Define a plane using struct representation.
myPlane = {
origin = { x = 0, y = 0, z = 0 },
xAxis = { x = 1, y = 0, z = 0 },
yAxis = { x = 0, y = 1, z = 0 },
}
// Prove we can get its axes and origin.
ax = myPlane.xAxis
assert(ax[0], isEqualTo = 1)
assert(ax[1], isEqualTo = 0)
assert(ax[2], isEqualTo = 0)
ay = myPlane.yAxis
assert(ay[0], isEqualTo = 0)
assert(ay[1], isEqualTo = 1)
assert(ay[2], isEqualTo = 0)
aorigin = myPlane.origin
assert(aorigin[0], isEqualTo = 0)
assert(aorigin[1], isEqualTo = 0)
assert(aorigin[2], isEqualTo = 0)
// Define a plane using standard planes.
myOtherPlane = XY
// Prove we can get its axes and origin.
axOther = myOtherPlane.xAxis
assert(axOther[0], isEqualTo = 1)
assert(axOther[1], isEqualTo = 0)
assert(axOther[2], isEqualTo = 0)
ayOther = myOtherPlane.yAxis
assert(ayOther[0], isEqualTo = 0)
assert(ayOther[1], isEqualTo = 1)
assert(ayOther[2], isEqualTo = 0)
aoriginOther = myOtherPlane.origin
assert(aoriginOther[0], isEqualTo = 0)
assert(aoriginOther[1], isEqualTo = 0)
assert(aoriginOther[2], isEqualTo = 0)
// Define a plane using a plane-modifying function like offsetPlane.
myAlternatePlane = offsetPlane(XY, offset = 0)
// Prove we can get its axes and origin.
axAlternate = myAlternatePlane.xAxis
assert(axAlternate[0], isEqualTo = 1)
assert(axAlternate[1], isEqualTo = 0)
assert(axAlternate[2], isEqualTo = 0)
ayAlternate = myAlternatePlane.yAxis
assert(ayAlternate[0], isEqualTo = 0)
assert(ayAlternate[1], isEqualTo = 1)
assert(ayAlternate[2], isEqualTo = 0)
aoriginAlternate = myAlternatePlane.origin
assert(aoriginAlternate[0], isEqualTo = 0)
assert(aoriginAlternate[1], isEqualTo = 0)
assert(aoriginAlternate[2], isEqualTo = 0)

View File

@ -4,8 +4,6 @@
URL STATUS URL STATUS
000 https://${BASE_URL} 000 https://${BASE_URL}
405 https://api.dev.zoo.dev/oauth2/token/revoke
401 https://api.dev.zoo.dev/users
301 https://discord.gg/JQEpHR7Nt2 301 https://discord.gg/JQEpHR7Nt2
404 https://github.com/KittyCAD/engine/issues/3528 404 https://github.com/KittyCAD/engine/issues/3528
404 https://github.com/KittyCAD/modeling-app/commit/${ref} 404 https://github.com/KittyCAD/modeling-app/commit/${ref}

View File

@ -1026,7 +1026,7 @@ export class SceneEntities {
const sketch = sketchFromPathToNode({ const sketch = sketchFromPathToNode({
pathToNode: sketchEntryNodePath, pathToNode: sketchEntryNodePath,
variables: this.kclManager.variables, variables: this.kclManager.variables,
kclManager: this.kclManager, ast: this.kclManager.ast,
}) })
if (err(sketch)) return Promise.reject(sketch) if (err(sketch)) return Promise.reject(sketch)
if (!sketch) return Promise.reject(new Error('No sketch found')) if (!sketch) return Promise.reject(new Error('No sketch found'))
@ -1171,12 +1171,64 @@ export class SceneEntities {
codeManager: this.codeManager, codeManager: this.codeManager,
}) })
// Check if we need to update sketchNodePaths due to CallExpression -> PipeExpression transformation
let updatedSketchNodePaths = [...sketchNodePaths]
let updatedSketchEntryNodePath = sketchEntryNodePath
let needsSelectionUpdate = false
// Check if the sketch entry was converted from CallExpression to PipeExpression
const _nodeAfter = getNodeFromPath<VariableDeclaration>(
modifiedAst,
sketchEntryNodePath,
'VariableDeclaration'
)
if (!err(_nodeAfter)) {
const initAfter = _nodeAfter.node?.declaration?.init
if (initAfter?.type === 'PipeExpression') {
const INIT_PATH_STEP_INDEX = 3
const pathNeedingUpdatingIndex = sketchNodePaths.findIndex(
(pathToNode) =>
pathToNode.length === INIT_PATH_STEP_INDEX + 1 &&
pathToNode.every(
(step, index) =>
step?.[0] === sketchEntryNodePath?.[index]?.[0]
)
)
if (pathNeedingUpdatingIndex !== -1) {
const updatedPath: PathToNode = [
...sketchNodePaths[pathNeedingUpdatingIndex],
['body', 'PipeExpression'],
[0, 'CallExpressionKw'],
]
updatedSketchNodePaths[pathNeedingUpdatingIndex] = updatedPath
updatedSketchEntryNodePath = updatedPath
needsSelectionUpdate = true
}
}
}
if (intersectsProfileStart) { if (intersectsProfileStart) {
this.sceneInfra.modelingSend({ type: 'Close sketch' }) this.sceneInfra.modelingSend({ type: 'Close sketch' })
} else { } else {
// Send selection update if paths were modified
if (needsSelectionUpdate) {
this.sceneInfra.modelingSend({
type: 'Set selection',
data: {
selectionType: 'completeSelection',
selection: {
graphSelections: [],
otherSelections: [],
},
updatedSketchEntryNodePath,
updatedSketchNodePaths,
},
})
}
await this.setupDraftSegment( await this.setupDraftSegment(
sketchEntryNodePath, sketchEntryNodePath,
sketchNodePaths, updatedSketchNodePaths,
planeNodePath, planeNodePath,
forward, forward,
up, up,
@ -2530,7 +2582,7 @@ export class SceneEntities {
const sketch = sketchFromPathToNode({ const sketch = sketchFromPathToNode({
pathToNode, pathToNode,
variables: this.kclManager.variables, variables: this.kclManager.variables,
kclManager: this.kclManager, ast: this.kclManager.ast,
}) })
if (trap(sketch)) return if (trap(sketch)) return
if (!sketch) { if (!sketch) {
@ -3784,14 +3836,14 @@ function prepareTruncatedAst(
function sketchFromPathToNode({ function sketchFromPathToNode({
pathToNode, pathToNode,
variables, variables,
kclManager, ast,
}: { }: {
pathToNode: PathToNode pathToNode: PathToNode
variables: VariableMap variables: VariableMap
kclManager: KclManager ast: Node<Program>
}): Sketch | null | Error { }): Sketch | null | Error {
const _varDec = getNodeFromPath<VariableDeclarator>( const _varDec = getNodeFromPath<VariableDeclarator>(
kclManager.ast, ast,
pathToNode, pathToNode,
'VariableDeclarator' 'VariableDeclarator'
) )
@ -3808,6 +3860,141 @@ function sketchFromPathToNode({
return sg return sg
} }
export function scaleProfiles({
ast,
pathsToProfile,
factor,
variables,
}: {
ast: Node<Program>
pathsToProfile: PathToNode[]
factor: number
variables: VariableMap
}) {
let clonedAst = structuredClone(ast)
for (const pathToProfile of pathsToProfile) {
const profile = sketchFromPathToNode({
pathToNode: pathToProfile,
variables: variables,
ast,
})
if (err(profile)) return profile
if (!profile) return Error('Profile not found')
// Scale the startProfile 'at' parameter
const scaledStartAt: [number, number] = [
profile.start.from[0] * factor,
profile.start.from[1] * factor,
]
if (
profile.paths?.[0]?.type !== 'Circle' &&
profile.paths?.[0]?.type !== 'CircleThreePoint'
) {
const startProfileResult = changeSketchArguments(
clonedAst,
variables,
{
type: 'path',
pathToNode: pathToProfile,
},
{
type: 'straight-segment',
from: [0, 0], // not used for startProfile
to: scaledStartAt,
}
)
if (trap(startProfileResult)) return startProfileResult
clonedAst = startProfileResult.modifiedAst
}
// Scale the path segments
for (let pathIndex = 0; pathIndex < profile.paths.length; pathIndex++) {
const path = profile.paths[pathIndex]
let input: Parameters<typeof changeSketchArguments>[3] | undefined =
undefined
const pathToSegment = getNodePathFromSourceRange(
clonedAst,
sourceRangeFromRust(path.__geoMeta.sourceRange)
)
if (!pathToSegment) {
console.log(
'Could not find path for segment:',
path.type,
'sourceRange:',
path.__geoMeta.sourceRange
)
}
const scaleTuple = (tuple: [number, number]): [number, number] => [
tuple[0] * factor,
tuple[1] * factor,
]
const previous = profile.paths[pathIndex - 1]
if (
path.type === 'ToPoint' ||
path.type === 'TangentialArcTo' ||
path.type === 'TangentialArc'
) {
input = {
type: 'straight-segment',
from: scaleTuple(path.from),
to: scaleTuple(path.to),
previousEndTangent: previous
? findTangentDirectionPath(previous)
: undefined,
}
} else if (
path.type === 'ArcThreePoint' ||
path.type === 'CircleThreePoint'
) {
input = {
type: 'circle-three-point-segment',
p1: scaleTuple(path.p1),
p2: scaleTuple(path.p2),
p3: scaleTuple(path.p3),
}
} else if (path.type === 'Circle') {
input = {
type: 'arc-segment',
from: scaleTuple(path.from),
to: scaleTuple(path.to),
center: scaleTuple(path.center),
radius: path.radius * factor,
ccw: path.ccw,
}
} else {
// Skip close calls - they don't have dimensions to scale
console.log('Unhandled path type:', path.type)
continue
}
if (input && pathToSegment) {
try {
const changeSketchResult = changeSketchArguments(
clonedAst,
variables,
{
type: 'path',
pathToNode: pathToSegment,
},
input
)
if (!err(changeSketchResult)) {
clonedAst = changeSketchResult.modifiedAst
}
} catch (error) {
console.log('Error scaling segment:', path.type, 'error:', error)
}
}
}
}
const pResult = parse(recast(clonedAst))
if (trap(pResult) || !resultIsOk(pResult)) {
return Error('Unexpected compilation error')
}
return {
modifiedAst: pResult.program,
}
}
function colorSegment(object: any, color: number) { function colorSegment(object: any, color: number) {
const segmentHead = getParentGroup(object, [ARROWHEAD, PROFILE_START]) const segmentHead = getParentGroup(object, [ARROWHEAD, PROFILE_START])
if (segmentHead) { if (segmentHead) {
@ -3837,7 +4024,7 @@ export function getSketchQuaternion(
const sketch = sketchFromPathToNode({ const sketch = sketchFromPathToNode({
pathToNode: sketchPathToNode, pathToNode: sketchPathToNode,
variables: kclManager.variables, variables: kclManager.variables,
kclManager, ast: kclManager.ast,
}) })
if (err(sketch)) return sketch if (err(sketch)) return sketch
const zAxis = const zAxis =
@ -3895,7 +4082,7 @@ function getSketchesInfo({
const sketch = sketchFromPathToNode({ const sketch = sketchFromPathToNode({
pathToNode: path, pathToNode: path,
variables, variables,
kclManager, ast: kclManager.ast,
}) })
if (err(sketch)) continue if (err(sketch)) continue
if (!sketch) continue if (!sketch) continue
@ -3974,3 +4161,44 @@ function findTangentDirection(segmentGroup: Group) {
} }
return tangentDirection return tangentDirection
} }
// implements the same as, but for a Path instead of segment Group
function findTangentDirectionPath(path: Path): Coords2d | undefined {
let tangentDirection: Coords2d | undefined
if (path.type === 'TangentialArcTo' || path.type === 'TangentialArc') {
// For tangential arcs with center, calculate tangent at the end point
const tangentAngle =
deg2Rad(getAngle(path.center, path.to)) +
(Math.PI / 2) * (path.ccw ? 1 : -1)
tangentDirection = [Math.cos(tangentAngle), Math.sin(tangentAngle)]
} else if (path.type === 'Arc') {
// For regular arcs, calculate tangent at the end point
const tangentAngle =
deg2Rad(getAngle(path.center, path.to)) +
(Math.PI / 2) * (path.ccw ? 1 : -1)
tangentDirection = [Math.cos(tangentAngle), Math.sin(tangentAngle)]
} else if (path.type === 'ArcThreePoint') {
// For three-point arcs, we need to calculate the center first
// This is more complex, so for now we'll skip tangent calculation
console.warn(
'ArcThreePoint tangent direction calculation not implemented yet'
)
} else if (path.type === 'ToPoint') {
// For straight lines, the tangent is the direction from start to end
const to = path.to as Coords2d
const from = path.from as Coords2d
tangentDirection = subVec(to, from)
const normalized = normalizeVec(tangentDirection)
if (normalized) {
tangentDirection = normalized
}
} else {
console.warn(
'Unsupported path type for tangent direction calculation: ',
path.type
)
}
return tangentDirection
}

View File

@ -31,6 +31,8 @@ import { roundOff, roundOffWithUnits } from '@src/lib/utils'
import { varMentions } from '@src/lib/varCompletionExtension' import { varMentions } from '@src/lib/varCompletionExtension'
import { useSettings } from '@src/lib/singletons' import { useSettings } from '@src/lib/singletons'
import { commandBarActor, useCommandBarState } from '@src/lib/singletons' import { commandBarActor, useCommandBarState } from '@src/lib/singletons'
import { doesProfileHaveAnyConstrainedDimension } from '@src/lang/queryAst'
import type { PathToNode } from '@src/lang/wasm'
import styles from './CommandBarKclInput.module.css' import styles from './CommandBarKclInput.module.css'
@ -50,6 +52,7 @@ function CommandBarKclInput({
stepBack: () => void stepBack: () => void
onSubmit: (event: unknown) => void onSubmit: (event: unknown) => void
}) { }) {
// console.log('arg', arg)
const commandBarState = useCommandBarState() const commandBarState = useCommandBarState()
const previouslySetValue = commandBarState.context.argumentsToSubmit[ const previouslySetValue = commandBarState.context.argumentsToSubmit[
arg.name arg.name
@ -109,6 +112,32 @@ function CommandBarKclInput({
arg.createVariable === 'force' || arg.createVariable === 'force' ||
false false
) )
// Check if this is the "Constrain with named value" command
const isConstrainWithNamedValueCommand =
commandBarState.context.selectedCommand?.name ===
'Constrain with named value'
const sketchDetails = argMachineContext?.sketchDetails
// Checkbox should be enabled (clickable) when it's the right command
const shouldEnableScaleCheckbox = isConstrainWithNamedValueCommand
// Checkbox should be checked by default when ALL profiles have no constrained dimensions
const shouldCheckScaleByDefault = useMemo(() => {
if (!isConstrainWithNamedValueCommand || !sketchDetails?.sketchNodePaths) {
return false
}
// Check if ALL profiles have no constrained dimensions (meaning we can safely scale)
return sketchDetails.sketchNodePaths.every((pathToProfile: PathToNode) => {
const hasConstrainedDimension = doesProfileHaveAnyConstrainedDimension(
pathToProfile,
kclManager.ast
)
return !hasConstrainedDimension // We want profiles with NO constrained dimensions
})
}, [isConstrainWithNamedValueCommand, sketchDetails, kclManager.ast])
const [scaleSketch, setScaleSketch] = useState(shouldCheckScaleByDefault)
const [canSubmit, setCanSubmit] = useState(true) const [canSubmit, setCanSubmit] = useState(true)
useHotkeys('mod + k, mod + /', () => commandBarActor.send({ type: 'Close' })) useHotkeys('mod + k, mod + /', () => commandBarActor.send({ type: 'Close' }))
const editorRef = useRef<HTMLDivElement>(null) const editorRef = useRef<HTMLDivElement>(null)
@ -225,6 +254,23 @@ function CommandBarKclInput({
) )
}, [calcResult, createNewVariable, isNewVariableNameUnique, isExecuting]) }, [calcResult, createNewVariable, isNewVariableNameUnique, isExecuting])
// Update scale checkbox when the condition changes
useEffect(() => {
setScaleSketch(shouldCheckScaleByDefault)
}, [shouldCheckScaleByDefault])
// Store scaleSketch value in command bar context for "Constrain with named value" command
useEffect(() => {
if (isConstrainWithNamedValueCommand) {
commandBarActor.send({
type: 'Set additional data',
data: {
scaleSketch,
},
})
}
}, [scaleSketch, isConstrainWithNamedValueCommand])
function handleSubmit(e?: React.FormEvent<HTMLFormElement>) { function handleSubmit(e?: React.FormEvent<HTMLFormElement>) {
e?.preventDefault() e?.preventDefault()
if (!canSubmit || valueNode === null) { if (!canSubmit || valueNode === null) {
@ -237,26 +283,26 @@ function CommandBarKclInput({
return return
} }
onSubmit( const commandValue = createNewVariable
createNewVariable ? ({
? ({ valueAst: valueNode,
valueAst: valueNode, valueText: value,
valueText: value, valueCalculated: calcResult,
valueCalculated: calcResult, variableName: newVariableName,
variableName: newVariableName, insertIndex: newVariableInsertIndex,
insertIndex: newVariableInsertIndex, variableIdentifierAst: createLocalName(newVariableName),
variableIdentifierAst: createLocalName(newVariableName), variableDeclarationAst: createVariableDeclaration(
variableDeclarationAst: createVariableDeclaration( newVariableName,
newVariableName, valueNode
valueNode ),
), } satisfies KclCommandValue)
} satisfies KclCommandValue) : ({
: ({ valueAst: valueNode,
valueAst: valueNode, valueText: value,
valueText: value, valueCalculated: calcResult,
valueCalculated: calcResult, } satisfies KclCommandValue)
} satisfies KclCommandValue)
) onSubmit(commandValue)
} }
return ( return (
@ -359,6 +405,34 @@ function CommandBarKclInput({
)} )}
</div> </div>
)} )}
{isConstrainWithNamedValueCommand && (
<div className="flex items-baseline gap-4 mx-4">
<input
type="checkbox"
id="scale-sketch-checkbox"
data-testid="scale-sketch-checkbox"
checked={scaleSketch}
disabled={!shouldEnableScaleCheckbox}
onChange={(e) => {
setScaleSketch(e.target.checked)
}}
className="bg-chalkboard-10 dark:bg-chalkboard-80"
/>
<label
htmlFor="scale-sketch-checkbox"
className={`text-blue border-none bg-transparent font-sm flex gap-1 items-center pl-0 pr-1 ${
!shouldEnableScaleCheckbox ? 'opacity-50 cursor-not-allowed' : ''
}`}
>
Scale sketch
</label>
{!shouldEnableScaleCheckbox && (
<span className="text-sm text-chalkboard-60 dark:text-chalkboard-50">
(disabled - sketch has constrained dimensions)
</span>
)}
</div>
)}
</form> </form>
) )
} }

View File

@ -7,7 +7,7 @@ import {
LanguageServerClient, LanguageServerClient,
LspWorkerEventType, LspWorkerEventType,
} from '@kittycad/codemirror-lsp-client' } from '@kittycad/codemirror-lsp-client'
import { TEST } from '@src/env' import { TEST, VITE_KC_API_BASE_URL } from '@src/env'
import React, { createContext, useContext, useMemo, useState } from 'react' import React, { createContext, useContext, useMemo, useState } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import type * as LSP from 'vscode-languageserver-protocol' import type * as LSP from 'vscode-languageserver-protocol'
@ -28,7 +28,6 @@ import type { FileEntry } from '@src/lib/project'
import { codeManager } from '@src/lib/singletons' import { codeManager } from '@src/lib/singletons'
import { err } from '@src/lib/trap' import { err } from '@src/lib/trap'
import { useToken } from '@src/lib/singletons' import { useToken } from '@src/lib/singletons'
import { withAPIBaseURL } from '@src/lib/withBaseURL'
function getWorkspaceFolders(): LSP.WorkspaceFolder[] { function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
return [] return []
@ -86,7 +85,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const initEvent: KclWorkerOptions = { const initEvent: KclWorkerOptions = {
wasmUrl: wasmUrl(), wasmUrl: wasmUrl(),
token: token, token: token,
apiBaseUrl: withAPIBaseURL(''), apiBaseUrl: VITE_KC_API_BASE_URL,
} }
lspWorker.postMessage({ lspWorker.postMessage({
worker: LspWorker.Kcl, worker: LspWorker.Kcl,
@ -179,7 +178,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const initEvent: CopilotWorkerOptions = { const initEvent: CopilotWorkerOptions = {
wasmUrl: wasmUrl(), wasmUrl: wasmUrl(),
token: token, token: token,
apiBaseUrl: withAPIBaseURL(''), apiBaseUrl: VITE_KC_API_BASE_URL,
} }
lspWorker.postMessage({ lspWorker.postMessage({
worker: LspWorker.Copilot, worker: LspWorker.Copilot,

View File

@ -8,14 +8,13 @@ export const NODE_ENV = env.NODE_ENV as string | undefined
export const VITE_KC_API_WS_MODELING_URL = env.VITE_KC_API_WS_MODELING_URL as export const VITE_KC_API_WS_MODELING_URL = env.VITE_KC_API_WS_MODELING_URL as
| string | string
| undefined | undefined
export const VITE_KITTYCAD_API_BASE_URL = env.VITE_KITTYCAD_API_BASE_URL export const VITE_KC_API_BASE_URL = env.VITE_KC_API_BASE_URL
export const VITE_KC_SITE_BASE_URL = env.VITE_KC_SITE_BASE_URL export const VITE_KC_SITE_BASE_URL = env.VITE_KC_SITE_BASE_URL
export const VITE_KC_SITE_APP_URL = env.VITE_KC_SITE_APP_URL export const VITE_KC_SITE_APP_URL = env.VITE_KC_SITE_APP_URL
export const VITE_KC_SKIP_AUTH = env.VITE_KC_SKIP_AUTH as string | undefined
export const VITE_KC_CONNECTION_TIMEOUT_MS = export const VITE_KC_CONNECTION_TIMEOUT_MS =
env.VITE_KC_CONNECTION_TIMEOUT_MS as string | undefined env.VITE_KC_CONNECTION_TIMEOUT_MS as string | undefined
export const VITE_KITTYCAD_API_TOKEN = env.VITE_KITTYCAD_API_TOKEN as export const VITE_KC_DEV_TOKEN = env.VITE_KC_DEV_TOKEN as string | undefined
| string
| undefined
export const PROD = env.PROD as string | undefined export const PROD = env.PROD as string | undefined
export const TEST = env.TEST as string | undefined export const TEST = env.TEST as string | undefined
export const DEV = env.DEV as string | undefined export const DEV = env.DEV as string | undefined

View File

@ -18,3 +18,7 @@ export const ARG_INTERIOR_ABSOLUTE = 'interiorAbsolute'
export const ARG_AT = 'at' export const ARG_AT = 'at'
export const ARG_LEG = 'leg' export const ARG_LEG = 'leg'
export const ARG_HYPOTENUSE = 'hypotenuse' export const ARG_HYPOTENUSE = 'hypotenuse'
export const ARG_P1 = 'p1'
export const ARG_P2 = 'p2'
export const ARG_P3 = 'p3'
export const ARG_DIAMETER = 'diameter'

View File

@ -25,12 +25,13 @@ import type { Artifact } from '@src/lang/std/artifactGraph'
import { codeRefFromRange } from '@src/lang/std/artifactGraph' import { codeRefFromRange } from '@src/lang/std/artifactGraph'
import { topLevelRange } from '@src/lang/util' import { topLevelRange } from '@src/lang/util'
import type { Identifier, Literal, LiteralValue } from '@src/lang/wasm' import type { Identifier, Literal, LiteralValue } from '@src/lang/wasm'
import { assertParse, recast } from '@src/lang/wasm' import { assertParse, recast, parse, resultIsOk } from '@src/lang/wasm'
import { initPromise } from '@src/lang/wasmUtils' import { initPromise } from '@src/lang/wasmUtils'
import { enginelessExecutor } from '@src/lib/testHelpers' import { enginelessExecutor } from '@src/lib/testHelpers'
import { err } from '@src/lib/trap' import { err } from '@src/lib/trap'
import { deleteFromSelection } from '@src/lang/modifyAst/deleteFromSelection' import { deleteFromSelection } from '@src/lang/modifyAst/deleteFromSelection'
import { assertNotErr } from '@src/unitTestUtils' import { assertNotErr } from '@src/unitTestUtils'
import { scaleProfiles } from '@src/clientSideScene/sceneEntities'
beforeAll(async () => { beforeAll(async () => {
await initPromise await initPromise
@ -917,3 +918,223 @@ extrude001 = extrude(part001, length = 5)
expect(result instanceof Error).toBe(true) expect(result instanceof Error).toBe(true)
}) })
}) })
describe('testing sketch scaling', () => {
it('can scale sketch by half simple case', async () => {
const basicSketch = `sketch002 = startSketchOn(XZ)
profile006 = startProfile(sketch002, at = [114.64, 124.18])
|> line(end = [173.02, 306.77])
|> line(end = [175.14, -282.35])
|> tangentialArc(end = [-52.01, -194.25])`
const ast = assertParse(basicSketch)
const result = await enginelessExecutor(ast)
const searchSnippet = 'startProfile(sketch002, at = [114.64, 124.18])'
const startIndex = basicSketch.indexOf(searchSnippet)
const range = topLevelRange(startIndex, startIndex + searchSnippet.length)
const pathToProfile = getNodePathFromSourceRange(ast, range)
if (err(result)) throw result
const scaledProfile = scaleProfiles({
ast,
factor: 0.5,
variables: result.variables,
pathsToProfile: [pathToProfile],
})
if (err(scaledProfile)) throw scaledProfile
const modifiedAst = scaledProfile.modifiedAst
const newCode = recast(modifiedAst)
if (err(newCode)) throw newCode
expect(newCode).toBe(`sketch002 = startSketchOn(XZ)
profile006 = startProfile(sketch002, at = [57.32, 62.09])
|> line(end = [86.51, 153.39])
|> line(end = [87.57, -141.18])
|> tangentialArc(end = [-26, -97.12])
`)
})
it('can scale sketch more complex', async () => {
let code = `sketch001 = startSketchOn(YZ)
profile001 = startProfile(sketch001, at = [100, 101])
|> line(end = [102, 103])
|> line(endAbsolute = [104, 105])
|> angledLine(angle = 206, length = 106)
|> angledLine(angle = -208, lengthX = 107)
|> angledLine(angle = 210, lengthY = 108)
|> angledLine(angle = 212, endAbsoluteX = 109)
|> angledLine(angle = 214, endAbsoluteY = 110)
|> arc(interiorAbsolute = [111, 112], endAbsolute = [113, 114])
|> tangentialArc(end = [115, -116])
|> tangentialArc(endAbsolute = [117, 118])
|> tangentialArc(angle = 224, radius = 119)
|> tangentialArc(angle = 226, diameter = 120)
profile002 = startProfile(sketch001, at = [-121, 122])
|> angledLine(angle = 130, length = 123, tag = $rectangleSegmentA001)
|> angledLine(angle = segAng(rectangleSegmentA001) - 232, length = 124)
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
profile003 = circle(sketch001, center = [-125, -126], radius = 127)
profile004 = circleThreePoint(
sketch001,
p1 = [128, 129],
p2 = [130, 131],
p3 = [132, 133],
)
profile005 = circle(sketch001, center = [-134, -135], diameter = 136)`
let ast = assertParse(code)
const result = await enginelessExecutor(ast)
const searchSnippets = [
'startProfile(sketch001, at = [100, 101])',
'startProfile(sketch001, at = [-121, 122])',
'circle(sketch001, center = [-125, -126], radius = 127)',
'circleThreePoint(',
'circle(sketch001, center = [-134, -135], diameter = 136)',
]
const ranges = searchSnippets.map((searchSnippet) => {
const startIndex = code.indexOf(searchSnippet)
return topLevelRange(startIndex, startIndex + searchSnippet.length)
})
const pathsToProfiles = ranges.map((range) =>
getNodePathFromSourceRange(ast, range)
)
if (err(result)) throw result
const scaledProfile = scaleProfiles({
ast,
factor: 0.5,
variables: result.variables,
pathsToProfile: pathsToProfiles,
})
if (err(scaledProfile)) throw scaledProfile
const pResult = parse(recast(scaledProfile.modifiedAst))
if (err(pResult) || !resultIsOk(pResult)) return
ast = pResult.program
const newCode = recast(ast)
if (err(newCode)) throw newCode
expect(newCode).toBe(`sketch001 = startSketchOn(YZ)
profile001 = startProfile(sketch001, at = [50, 50.5])
|> line(end = [51, 51.5])
|> line(endAbsolute = [52, 52.5])
|> angledLine(angle = -154, length = 53)
|> angledLine(angle = 152, lengthX = 53.5)
|> angledLine(angle = -150, lengthY = 54)
|> angledLine(angle = 32, endAbsoluteX = 54.5)
|> angledLine(angle = -146, endAbsoluteY = 55)
|> arc(interiorAbsolute = [55.5, 56], endAbsolute = [56.5, 57])
|> tangentialArc(end = [57.5, -58])
|> tangentialArc(endAbsolute = [58.5, 59])
|> tangentialArc(angle = 224deg, radius = 59.5)
|> tangentialArc(angle = 226deg, diameter = 60)
profile002 = startProfile(sketch001, at = [-60.5, 61])
|> angledLine(angle = 130, length = 61.5, tag = $rectangleSegmentA001)
|> angledLine(angle = segAng(rectangleSegmentA001) - 232, length = 62)
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
profile003 = circle(sketch001, center = [-62.5, -63], radius = 63.5)
profile004 = circleThreePoint(
sketch001,
p1 = [64, 64.5],
p2 = [65, 65.5],
p3 = [66, 66.5],
)
profile005 = circle(sketch001, center = [-67, -67.5], diameter = 68)
`)
})
it("make sure it doesn't stomp constraints", async () => {
let code = `sketch001 = startSketchOn(YZ)
profile001 = startProfile(sketch001, at = [100 + 0, 101])
|> line(end = [102, 103 + 0])
|> line(endAbsolute = [104 + 0, 105])
|> angledLine(angle = 206, length = 106 + 0)
|> angledLine(angle = -208 + 0, lengthX = 107)
|> angledLine(angle = 210, lengthY = 108 + 0)
|> angledLine(angle = 212 + 0, endAbsoluteX = 109)
|> angledLine(angle = 214, endAbsoluteY = 110 + 0)
|> arc(interiorAbsolute = [111 + 0, 112], endAbsolute = [113, 114 + 0])
|> tangentialArc(end = [115, -116 + 0])
|> tangentialArc(endAbsolute = [117 + 0, 118])
|> tangentialArc(angle = 224, radius = 119 + 0)
|> tangentialArc(angle = 226 + 0, diameter = 120)
profile002 = startProfile(sketch001, at = [-121 + 0, 122])
|> angledLine(angle = 130, length = 123 + 0, tag = $rectangleSegmentA001)
|> angledLine(angle = segAng(rectangleSegmentA001) - 232, length = 124)
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
profile003 = circle(sketch001, center = [-125 + 0, -126], radius = 127 + 0)
profile004 = circleThreePoint(
sketch001,
p1 = [128, 129 + 0],
p2 = [130 + 0, 131],
p3 = [133, 132],
)
profile005 = circle(sketch001, center = [-134, -135 + 0], diameter = 136)
`
let ast = assertParse(code)
const result = await enginelessExecutor(ast)
const searchSnippets = [
'startProfile(sketch001, at = [100 + 0, 101])',
'startProfile(sketch001, at = [-121 + 0, 122])',
'circle(sketch001, center = [-125 + 0, -126], radius = 127 + 0)',
'circleThreePoint(',
'circle(sketch001, center = [-134, -135 + 0], diameter = 136)',
]
const ranges = searchSnippets.map((searchSnippet) => {
const startIndex = code.indexOf(searchSnippet)
return topLevelRange(startIndex, startIndex + searchSnippet.length)
})
const pathsToProfiles = ranges.map((range) =>
getNodePathFromSourceRange(ast, range)
)
if (err(result)) throw result
const scaledProfile = scaleProfiles({
ast,
factor: 0.5,
variables: result.variables,
pathsToProfile: pathsToProfiles,
})
if (err(scaledProfile)) throw scaledProfile
const pResult = parse(recast(scaledProfile.modifiedAst))
if (err(pResult) || !resultIsOk(pResult)) return
ast = pResult.program
const newCode = recast(ast)
if (err(newCode)) throw newCode
expect(newCode).toBe(`sketch001 = startSketchOn(YZ)
profile001 = startProfile(sketch001, at = [100 + 0, 50.5])
|> line(end = [51, 103 + 0])
|> line(endAbsolute = [104 + 0, 52.5])
|> angledLine(angle = -154, length = 106 + 0)
|> angledLine(angle = -208 + 0, lengthX = 53.5)
|> angledLine(angle = -150, lengthY = 108 + 0)
|> angledLine(angle = 212 + 0, endAbsoluteX = 54.5)
|> angledLine(angle = -146, endAbsoluteY = 110 + 0)
|> arc(interiorAbsolute = [111 + 0, 56], endAbsolute = [56.5, 114 + 0])
|> tangentialArc(end = [57.5, -116 + 0])
|> tangentialArc(endAbsolute = [117 + 0, 59])
|> tangentialArc(angle = 224deg, radius = 119 + 0)
|> tangentialArc(angle = 226 + 0, diameter = 60)
profile002 = startProfile(sketch001, at = [-121 + 0, 61])
|> angledLine(angle = 130, length = 123 + 0, tag = $rectangleSegmentA001)
|> angledLine(angle = segAng(rectangleSegmentA001) - 232, length = 62)
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
profile003 = circle(sketch001, center = [-125 + 0, -63], radius = 127 + 0)
profile004 = circleThreePoint(
sketch001,
p1 = [64, 129 + 0],
p2 = [130 + 0, 65.5],
p3 = [66.5, 66],
)
profile005 = circle(sketch001, center = [-67, -135 + 0], diameter = 68)
`)
})
})

View File

@ -1,4 +1,4 @@
import { VITE_KITTYCAD_API_TOKEN } from '@src/env' import { VITE_KC_DEV_TOKEN } from '@src/env'
import { createLiteral } from '@src/lang/create' import { createLiteral } from '@src/lang/create'
import type { import type {
@ -40,9 +40,10 @@ import { isOverlap } from '@src/lib/utils'
beforeAll(async () => { beforeAll(async () => {
await initPromise await initPromise
// THESE TEST WILL FAIL without VITE_KC_DEV_TOKEN set in .env.development.local
await new Promise((resolve) => { await new Promise((resolve) => {
engineCommandManager.start({ engineCommandManager.start({
token: VITE_KITTYCAD_API_TOKEN, token: VITE_KC_DEV_TOKEN,
width: 256, width: 256,
height: 256, height: 256,
setMediaStream: () => {}, setMediaStream: () => {},

View File

@ -4,15 +4,16 @@ import { initPromise } from '@src/lang/wasmUtils'
import { err } from '@src/lib/trap' import { err } from '@src/lib/trap'
import type { Selection } from '@src/lib/selections' import type { Selection } from '@src/lib/selections'
import { engineCommandManager, kclManager } from '@src/lib/singletons' import { engineCommandManager, kclManager } from '@src/lib/singletons'
import { VITE_KITTYCAD_API_TOKEN } from '@src/env' import { VITE_KC_DEV_TOKEN } from '@src/env'
import { modifyAstWithTagsForSelection } from '@src/lang/modifyAst/tagManagement' import { modifyAstWithTagsForSelection } from '@src/lang/modifyAst/tagManagement'
beforeAll(async () => { beforeAll(async () => {
await initPromise await initPromise
// THESE TEST WILL FAIL without VITE_KC_DEV_TOKEN set in .env.development.local
await new Promise((resolve) => { await new Promise((resolve) => {
engineCommandManager.start({ engineCommandManager.start({
token: VITE_KITTYCAD_API_TOKEN, token: VITE_KC_DEV_TOKEN,
width: 256, width: 256,
height: 256, height: 256,
setMediaStream: () => {}, setMediaStream: () => {},

View File

@ -10,6 +10,7 @@ import {
createPipeSubstitution, createPipeSubstitution,
} from '@src/lang/create' } from '@src/lang/create'
import { import {
doesProfileHaveAnyConstrainedDimension,
doesSceneHaveExtrudedSketch, doesSceneHaveExtrudedSketch,
doesSceneHaveSweepableSketch, doesSceneHaveSweepableSketch,
findAllPreviousVariables, findAllPreviousVariables,
@ -35,6 +36,95 @@ beforeAll(async () => {
await initPromise await initPromise
}) })
describe('doesProfileHaveConstrainDimension', () => {
const code = `sketch001 = startSketchOn(YZ)
profile001 = startProfile(sketch001, at = [100, 101])
|> line(end = [102, 103])
|> line(endAbsolute = [104, 105])
|> angledLine(angle = 206, length = 106)
|> angledLine(angle = -208, lengthX = 107)
|> angledLine(angle = 210, lengthY = 108)
|> angledLine(angle = 212, endAbsoluteX = 109)
|> angledLine(angle = 214, endAbsoluteY = 110)
|> arc(interiorAbsolute = [111, 112], endAbsolute = [113, 114])
|> tangentialArc(end = [115, -116])
|> tangentialArc(endAbsolute = [117, 118])
|> tangentialArc(angle = 224, radius = 119)
|> tangentialArc(angle = 226, diameter = 120)
profile002 = startProfile(sketch001, at = [-121, 122])
|> angledLine(angle = 130, length = 123, tag = $rectangleSegmentA001)
|> angledLine(angle = segAng(rectangleSegmentA001) - 232, length = 124)
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
profile003 = circle(sketch001, center = [-125, -126], radius = 127)
profile004 = circleThreePoint(
sketch001,
p1 = [128, 129],
p2 = [130, 131],
p3 = [132, 133],
)
profile005 = circle(sketch001, center = [-134, -135], diameter = 136)`
const profileSearchStrings = [
{
profileSearchString: 'profile001 = startProfile',
replaceCases: { start: 100, end: 120 },
},
{
profileSearchString: 'profile002 = startProfile',
replaceCases: { start: 121, end: 124 },
},
{
profileSearchString: 'profile003 = circle',
replaceCases: { start: 125, end: 127 },
},
{
profileSearchString: 'profile004 = circleThreePoint',
replaceCases: { start: 128, end: 133 },
},
{
profileSearchString: 'profile005 = circle',
replaceCases: { start: 134, end: 136 },
},
] as const
it('should return false for all profiles (no constrained dimensions detected)', () => {
const ast = assertParse(code)
profileSearchStrings.forEach((profile) => {
const profileStart = code.indexOf(profile.profileSearchString)
const profilePath = getNodePathFromSourceRange(
ast,
topLevelRange(profileStart, profileStart)
)
expect(
doesProfileHaveAnyConstrainedDimension(profilePath, ast)
).toBeFalsy()
})
})
it('should true false when adding constraints for each Profile all profiles (no constrained dimensions detected)', () => {
profileSearchStrings.forEach((profile) => {
for (
let i = profile.replaceCases.start;
i <= profile.replaceCases.end;
i++
) {
const modifiedCode = code.replaceAll(String(i), `${i} + 5`)
const ast = assertParse(modifiedCode)
const profileStart = modifiedCode.indexOf(profile.profileSearchString)
const profilePath = getNodePathFromSourceRange(
ast,
topLevelRange(profileStart, profileStart)
)
expect(
doesProfileHaveAnyConstrainedDimension(profilePath, ast)
).toBeTruthy()
}
// })
})
})
})
describe('findAllPreviousVariables', () => { describe('findAllPreviousVariables', () => {
it('should find all previous variables', async () => { it('should find all previous variables', async () => {
const code = `baseThick = 1 const code = `baseThick = 1

View File

@ -55,6 +55,23 @@ import type { OpKclValue, Operation } from '@rust/kcl-lib/bindings/Operation'
import { ARG_INDEX_FIELD, LABELED_ARG_FIELD } from '@src/lang/queryAstConstants' import { ARG_INDEX_FIELD, LABELED_ARG_FIELD } from '@src/lang/queryAstConstants'
import type { KclCommandValue } from '@src/lib/commandTypes' import type { KclCommandValue } from '@src/lib/commandTypes'
import type { UnaryExpression } from 'typescript' import type { UnaryExpression } from 'typescript'
import {
ARG_END_ABSOLUTE,
ARG_END,
ARG_LENGTH,
ARG_CIRCLE_CENTER,
ARG_RADIUS,
ARG_LENGTH_X,
ARG_LENGTH_Y,
ARG_END_ABSOLUTE_X,
ARG_END_ABSOLUTE_Y,
ARG_INTERIOR_ABSOLUTE,
ARG_AT,
ARG_P1,
ARG_P2,
ARG_P3,
ARG_DIAMETER,
} from '@src/lang/constants'
import type { NumericType } from '@rust/kcl-lib/bindings/NumericType' import type { NumericType } from '@rust/kcl-lib/bindings/NumericType'
/** /**
@ -1293,3 +1310,146 @@ export const getPathNormalisedForTruncatedAst = (
Number(nodePathWithCorrectedIndexForTruncatedAst[1][0]) - minIndex Number(nodePathWithCorrectedIndexForTruncatedAst[1][0]) - minIndex
return nodePathWithCorrectedIndexForTruncatedAst return nodePathWithCorrectedIndexForTruncatedAst
} }
export function doesProfileHaveAnyConstrainedDimension(
profilePath: PathToNode,
ast: Node<Program>
): boolean {
// Get the profile node from the path
const profileNodeResult = getNodeFromPath<Node<VariableDeclaration>>(
ast,
profilePath,
'VariableDeclaration'
)
if (err(profileNodeResult)) return false
const profileNode = profileNodeResult.node
// Single value dimension parameters to check (excluding angle as per requirements)
const singleValueLengthParams = new Set([
ARG_DIAMETER,
ARG_RADIUS,
ARG_LENGTH,
ARG_LENGTH_X,
ARG_LENGTH_Y,
ARG_END_ABSOLUTE_X,
ARG_END_ABSOLUTE_Y,
])
// Tuple value dimension parameters to check
const tupleValueParams = new Set([
ARG_CIRCLE_CENTER,
ARG_P1,
ARG_P2,
ARG_P3,
ARG_AT,
ARG_END,
ARG_END_ABSOLUTE,
ARG_INTERIOR_ABSOLUTE,
])
let hasConstrainedDimension = false
// Traverse the profile node to find all call expressions and their arguments
traverse(profileNode, {
enter: (node: any) => {
if (node.type === 'CallExpressionKw' && node.arguments) {
for (const arg of node.arguments) {
if (arg.type === 'LabeledArg' && arg.label?.name) {
const paramName = arg.label.name
// Check if this parameter is in our whitelist
if (
singleValueLengthParams.has(paramName) ||
tupleValueParams.has(paramName)
) {
// Special case: endAbsolute = [profileStartX(%), profileStartY(%)]
// This should NOT count as constrained
if (
paramName === ARG_END_ABSOLUTE &&
arg.arg.type === 'ArrayExpression' &&
arg.arg.elements.length === 2
) {
const [first, second] = arg.arg.elements
if (
first.type === 'CallExpressionKw' &&
second.type === 'CallExpressionKw' &&
first.callee.type === 'Name' &&
second.callee.type === 'Name' &&
first.callee.name.name === 'profileStartX' &&
second.callee.name.name === 'profileStartY'
) {
// This is the special case - don't count as constrained
continue
}
}
// Special case: angledLine length = -segLen(rectangleSegmentA001)
// This should NOT count as constrained
if (
node.callee?.type === 'Name' &&
node.callee.name.name === 'angledLine' &&
paramName === ARG_LENGTH
) {
let callExpr = null
// Check if it's a direct call expression or unary expression with call expression
if (arg.arg.type === 'CallExpressionKw') {
callExpr = arg.arg
} else if (
arg.arg.type === 'UnaryExpression' &&
arg.arg.argument?.type === 'CallExpressionKw'
) {
callExpr = arg.arg.argument
}
if (
callExpr &&
callExpr.callee?.type === 'Name' &&
callExpr.callee.name.name === 'segLen' &&
callExpr.unlabeled?.type === 'Name' &&
callExpr.unlabeled.name.name.startsWith('rectangleSegmentA')
) {
// This is the special case - don't count as constrained
continue
}
}
// Check if the argument value is non-static
if (!isStaticValue(arg.arg)) {
hasConstrainedDimension = true
break
}
}
}
}
// If we found a constrained dimension, we can break out of the outer loop too
if (hasConstrainedDimension) {
return false // This stops the traversal
}
}
},
})
return hasConstrainedDimension
}
// Helper function to check if a node represents a static/constant value (literal, array of literals, or negative literal)
function isStaticValue(node: any): boolean {
if (node.type === 'Literal') {
return true
}
if (node.type === 'ArrayExpression') {
// Array is literal if all elements are literals
return node.elements.every((element: any) => isStaticValue(element))
}
if (node.type === 'UnaryExpression' && node.operator === '-') {
// Negative literal numbers
return isStaticValue(node.argument)
}
// All other node types (Name, CallExpression, BinaryExpression, etc.) are non-literal
return false
}

View File

@ -1,5 +1,5 @@
import type { Models } from '@kittycad/lib' import type { Models } from '@kittycad/lib'
import { VITE_KC_API_WS_MODELING_URL, VITE_KITTYCAD_API_TOKEN } from '@src/env' import { VITE_KC_API_WS_MODELING_URL, VITE_KC_DEV_TOKEN } from '@src/env'
import { jsAppSettings } from '@src/lib/settings/settingsUtils' import { jsAppSettings } from '@src/lib/settings/settingsUtils'
import { BSON } from 'bson' import { BSON } from 'bson'
@ -400,7 +400,7 @@ class EngineConnection extends EventTarget {
this.send({ this.send({
type: 'headers', type: 'headers',
headers: { headers: {
Authorization: `Bearer ${VITE_KITTYCAD_API_TOKEN}`, Authorization: `Bearer ${VITE_KC_DEV_TOKEN}`,
}, },
}) })
} }

View File

@ -24,6 +24,7 @@ import {
ARG_TAG, ARG_TAG,
DETERMINING_ARGS, DETERMINING_ARGS,
ARG_INTERIOR_ABSOLUTE, ARG_INTERIOR_ABSOLUTE,
ARG_DIAMETER,
} from '@src/lang/constants' } from '@src/lang/constants'
import { import {
createArrayExpression, createArrayExpression,
@ -1424,13 +1425,27 @@ export const circle: SketchLineHelperKw = {
const { node: callExpression } = nodeMeta const { node: callExpression } = nodeMeta
// All function arguments, except the tag
const functionArguments = callExpression.arguments
.map((arg) => arg.label?.name)
.filter((n) => n && n !== ARG_TAG)
const newCenter = createArrayExpression([ const newCenter = createArrayExpression([
createLiteral(roundOff(center[0])), createLiteral(roundOff(center[0])),
createLiteral(roundOff(center[1])), createLiteral(roundOff(center[1])),
]) ])
mutateKwArg(ARG_CIRCLE_CENTER, callExpression, newCenter) mutateKwArg(ARG_CIRCLE_CENTER, callExpression, newCenter)
const newRadius = createLiteral(roundOff(radius))
mutateKwArg(ARG_RADIUS, callExpression, newRadius) // Check if the circle uses diameter or radius
const isDiameter = functionArguments.includes(ARG_DIAMETER)
if (isDiameter) {
const newDiameter = createLiteral(roundOff(radius * 2))
mutateKwArg(ARG_DIAMETER, callExpression, newDiameter)
} else {
const newRadius = createLiteral(roundOff(radius))
mutateKwArg(ARG_RADIUS, callExpression, newRadius)
}
return { return {
modifiedAst: _node, modifiedAst: _node,
pathToNode, pathToNode,
@ -4168,7 +4183,14 @@ const tangentialArcHelpers = {
.map((arg) => arg.label?.name) .map((arg) => arg.label?.name)
.filter((n) => n && n !== ARG_TAG) .filter((n) => n && n !== ARG_TAG)
if (areArraysEqual(functionArguments, [ARG_ANGLE, ARG_RADIUS])) { const isDiameter = areArraysEqual(functionArguments, [
ARG_ANGLE,
ARG_DIAMETER,
])
if (
areArraysEqual(functionArguments, [ARG_ANGLE, ARG_RADIUS]) ||
isDiameter
) {
// Using length and radius -> convert "from", "to" to the matching length and radius // Using length and radius -> convert "from", "to" to the matching length and radius
const previousEndTangent = input.previousEndTangent const previousEndTangent = input.previousEndTangent
if (previousEndTangent) { if (previousEndTangent) {
@ -4219,11 +4241,19 @@ const tangentialArcHelpers = {
const radius = distance2d(center, from) const radius = distance2d(center, from)
mutateKwArg( if (!isDiameter) {
ARG_RADIUS, mutateKwArg(
callExpression, ARG_RADIUS,
createLiteral(roundOff(radius, 2)) callExpression,
) createLiteral(roundOff(radius, 2))
)
} else {
mutateKwArg(
ARG_DIAMETER,
callExpression,
createLiteral(roundOff(radius * 2, 2))
)
}
const angleValue = createLiteralMaybeSuffix({ const angleValue = createLiteralMaybeSuffix({
value: roundOff(angle, 2), value: roundOff(angle, 2),
suffix: 'Deg', suffix: 'Deg',

View File

@ -172,6 +172,7 @@ export type ModelingCommandSchema = {
variableName: string variableName: string
} }
namedValue: KclCommandValue namedValue: KclCommandValue
scaleSketch?: boolean
} }
'Prompt-to-edit': { 'Prompt-to-edit': {
prompt: string prompt: string

View File

@ -1,3 +1,4 @@
import { VITE_KC_API_BASE_URL } from '@src/env'
import { UAParser } from 'ua-parser-js' import { UAParser } from 'ua-parser-js'
import type { OsInfo } from '@rust/kcl-lib/bindings/OsInfo' import type { OsInfo } from '@rust/kcl-lib/bindings/OsInfo'
@ -10,7 +11,6 @@ import { isDesktop } from '@src/lib/isDesktop'
import type RustContext from '@src/lib/rustContext' import type RustContext from '@src/lib/rustContext'
import screenshot from '@src/lib/screenshot' import screenshot from '@src/lib/screenshot'
import { APP_VERSION } from '@src/routes/utils' import { APP_VERSION } from '@src/routes/utils'
import { withAPIBaseURL } from '@src/lib/withBaseURL'
/* eslint-disable suggest-no-throw/suggest-no-throw -- /* eslint-disable suggest-no-throw/suggest-no-throw --
* All the throws in CoreDumpManager are intentional and should be caught and handled properly * All the throws in CoreDumpManager are intentional and should be caught and handled properly
@ -35,7 +35,7 @@ export class CoreDumpManager {
codeManager: CodeManager codeManager: CodeManager
rustContext: RustContext rustContext: RustContext
token: string | undefined token: string | undefined
baseUrl: string = withAPIBaseURL('') baseUrl: string = VITE_KC_API_BASE_URL
constructor( constructor(
engineCommandManager: EngineCommandManager, engineCommandManager: EngineCommandManager,

View File

@ -26,7 +26,6 @@ import { err } from '@src/lib/trap'
import type { DeepPartial } from '@src/lib/types' import type { DeepPartial } from '@src/lib/types'
import { getInVariableCase } from '@src/lib/utils' import { getInVariableCase } from '@src/lib/utils'
import { IS_STAGING } from '@src/routes/utils' import { IS_STAGING } from '@src/routes/utils'
import { withAPIBaseURL } from '@src/lib/withBaseURL'
export async function renameProjectDirectory( export async function renameProjectDirectory(
projectPath: string, projectPath: string,
@ -698,9 +697,7 @@ export const readTokenFile = async () => {
export const writeTokenFile = async (token: string) => { export const writeTokenFile = async (token: string) => {
const tokenFilePath = await getTokenFilePath() const tokenFilePath = await getTokenFilePath()
if (err(token)) return Promise.reject(token) if (err(token)) return Promise.reject(token)
const result = window.electron.writeFile(tokenFilePath, token) return window.electron.writeFile(tokenFilePath, token)
console.log('token written to disk')
return result
} }
export const writeTelemetryFile = async (content: string) => { export const writeTelemetryFile = async (content: string) => {
@ -725,9 +722,12 @@ export const setState = async (state: Project | undefined): Promise<void> => {
appStateStore = state appStateStore = state
} }
export const getUser = async (token: string): Promise<Models['User_type']> => { export const getUser = async (
token: string,
hostname: string
): Promise<Models['User_type']> => {
try { try {
const user = await fetch(withAPIBaseURL('/users/me'), { const user = await fetch(`${hostname}/users/me`, {
headers: new Headers({ headers: new Headers({
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
}), }),

View File

@ -1,4 +1,4 @@
import { VITE_KC_SITE_APP_URL } from '@src/env' import { VITE_KC_API_BASE_URL, VITE_KC_SITE_APP_URL } from '@src/env'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { stringToBase64 } from '@src/lib/base64' import { stringToBase64 } from '@src/lib/base64'
@ -7,7 +7,6 @@ import {
CREATE_FILE_URL_PARAM, CREATE_FILE_URL_PARAM,
} from '@src/lib/constants' } from '@src/lib/constants'
import { err } from '@src/lib/trap' import { err } from '@src/lib/trap'
import { withAPIBaseURL } from '@src/lib/withBaseURL'
export interface FileLinkParams { export interface FileLinkParams {
code: string code: string
@ -97,7 +96,7 @@ export async function createShortlink(
if (password) { if (password) {
body.password = password body.password = password
} }
const response = await fetch(withAPIBaseURL('/user/shortlinks'), { const response = await fetch(`${VITE_KC_API_BASE_URL}/user/shortlinks`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-type': 'application/json', 'Content-type': 'application/json',

View File

@ -1,7 +1,7 @@
import type { SelectionRange } from '@codemirror/state' import type { SelectionRange } from '@codemirror/state'
import { EditorSelection, Transaction } from '@codemirror/state' import { EditorSelection, Transaction } from '@codemirror/state'
import type { Models } from '@kittycad/lib' import type { Models } from '@kittycad/lib'
import { VITE_KC_SITE_BASE_URL } from '@src/env' import { VITE_KC_API_BASE_URL, VITE_KC_SITE_BASE_URL } from '@src/env'
import { diffLines } from 'diff' import { diffLines } from 'diff'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import type { TextToCadMultiFileIteration_type } from '@kittycad/lib/dist/types/src/models' import type { TextToCadMultiFileIteration_type } from '@kittycad/lib/dist/types/src/models'
@ -28,7 +28,6 @@ import { uuidv4 } from '@src/lib/utils'
import type { File as KittyCadLibFile } from '@kittycad/lib/dist/types/src/models' import type { File as KittyCadLibFile } from '@kittycad/lib/dist/types/src/models'
import type { FileMeta } from '@src/lib/types' import type { FileMeta } from '@src/lib/types'
import type { RequestedKCLFile } from '@src/machines/systemIO/utils' import type { RequestedKCLFile } from '@src/machines/systemIO/utils'
import { withAPIBaseURL } from '@src/lib/withBaseURL'
type KclFileMetaMap = { type KclFileMetaMap = {
[execStateFileNamesIndex: number]: Extract<FileMeta, { type: 'kcl' }> [execStateFileNamesIndex: number]: Extract<FileMeta, { type: 'kcl' }>
@ -78,7 +77,7 @@ async function submitTextToCadRequest(
}) })
const response = await fetch( const response = await fetch(
withAPIBaseURL('/ml/text-to-cad/multi-file/iteration'), `${VITE_KC_API_BASE_URL}/ml/text-to-cad/multi-file/iteration`,
{ {
method: 'POST', method: 'POST',
headers: { headers: {
@ -305,7 +304,7 @@ export async function getPromptToEditResult(
id: string, id: string,
token?: string token?: string
): Promise<Models['TextToCadMultiFileIteration_type'] | Error> { ): Promise<Models['TextToCadMultiFileIteration_type'] | Error> {
const url = withAPIBaseURL(`/async/operations/${id}`) const url = VITE_KC_API_BASE_URL + '/async/operations/' + id
const data: Models['TextToCadMultiFileIteration_type'] | Error = const data: Models['TextToCadMultiFileIteration_type'] | Error =
await crossPlatformFetch( await crossPlatformFetch(
url, url,
@ -336,6 +335,14 @@ export async function doPromptEdit({
const toastId = toast.loading('Submitting to Text-to-CAD API...') const toastId = toast.loading('Submitting to Text-to-CAD API...')
let submitResult let submitResult
// work around for @kittycad/lib not really being built for the browser
;(window as any).process = {
env: {
ZOO_API_TOKEN: token,
ZOO_HOST: VITE_KC_API_BASE_URL,
},
}
try { try {
submitResult = await submitPromptToEditToQueue({ submitResult = await submitPromptToEditToQueue({
prompt, prompt,

View File

@ -1,4 +1,4 @@
import { withAPIBaseURL } from '@src/lib/withBaseURL' import { VITE_KC_API_BASE_URL } from '@src/env'
import EditorManager from '@src/editor/manager' import EditorManager from '@src/editor/manager'
import { KclManager } from '@src/lang/KclSingleton' import { KclManager } from '@src/lang/KclSingleton'
@ -171,7 +171,7 @@ const appMachine = setup({
systemId: BILLING, systemId: BILLING,
input: { input: {
...BILLING_CONTEXT_DEFAULTS, ...BILLING_CONTEXT_DEFAULTS,
urlUserService: withAPIBaseURL(''), urlUserService: VITE_KC_API_BASE_URL,
}, },
}), }),
], ],

View File

@ -1,4 +1,5 @@
import type { Models } from '@kittycad/lib' import type { Models } from '@kittycad/lib'
import { VITE_KC_API_BASE_URL } from '@src/env'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import type { NavigateFunction } from 'react-router-dom' import type { NavigateFunction } from 'react-router-dom'
import { import {
@ -18,7 +19,6 @@ import { err, reportRejection } from '@src/lib/trap'
import { toSync } from '@src/lib/utils' import { toSync } from '@src/lib/utils'
import { getAllSubDirectoriesAtProjectRoot } from '@src/machines/systemIO/snapshotContext' import { getAllSubDirectoriesAtProjectRoot } from '@src/machines/systemIO/snapshotContext'
import { joinOSPaths } from '@src/lib/paths' import { joinOSPaths } from '@src/lib/paths'
import { withAPIBaseURL } from '@src/lib/withBaseURL'
export async function submitTextToCadPrompt( export async function submitTextToCadPrompt(
prompt: string, prompt: string,
@ -32,7 +32,7 @@ export async function submitTextToCadPrompt(
kcl_version: kclManager.kclVersion, kcl_version: kclManager.kclVersion,
} }
// Glb has a smaller footprint than gltf, should we want to render it. // Glb has a smaller footprint than gltf, should we want to render it.
const url = withAPIBaseURL('/ai/text-to-cad/glb?kcl=true') const url = VITE_KC_API_BASE_URL + '/ai/text-to-cad/glb?kcl=true'
const data: Models['TextToCad_type'] | Error = await crossPlatformFetch( const data: Models['TextToCad_type'] | Error = await crossPlatformFetch(
url, url,
{ {
@ -58,7 +58,7 @@ export async function getTextToCadResult(
id: string, id: string,
token?: string token?: string
): Promise<Models['TextToCad_type'] | Error> { ): Promise<Models['TextToCad_type'] | Error> {
const url = withAPIBaseURL(`/user/text-to-cad/${id}`) const url = VITE_KC_API_BASE_URL + '/user/text-to-cad/' + id
const data: Models['TextToCad_type'] | Error = await crossPlatformFetch( const data: Models['TextToCad_type'] | Error = await crossPlatformFetch(
url, url,
{ {

View File

@ -1,13 +1,14 @@
import type { Models } from '@kittycad/lib/dist/types/src' import type { Models } from '@kittycad/lib/dist/types/src'
import { VITE_KC_API_BASE_URL } from '@src/env'
import crossPlatformFetch from '@src/lib/crossPlatformFetch' import crossPlatformFetch from '@src/lib/crossPlatformFetch'
import { withAPIBaseURL } from '@src/lib/withBaseURL'
export async function sendTelemetry( export async function sendTelemetry(
id: string, id: string,
feedback: Models['MlFeedback_type'], feedback: Models['MlFeedback_type'],
token?: string token?: string
): Promise<void> { ): Promise<void> {
const url = withAPIBaseURL(`/user/text-to-cad/${id}?feedback=${feedback}`) const url =
VITE_KC_API_BASE_URL + '/user/text-to-cad/' + id + '?feedback=' + feedback
await crossPlatformFetch( await crossPlatformFetch(
url, url,
{ {

View File

@ -1,34 +0,0 @@
import { withAPIBaseURL } from '@src/lib/withBaseURL'
describe('withBaseURL', () => {
/**
* running in the development environment
* the .env.development should load
*/
describe('withAPIBaseUrl', () => {
it('should return base url', () => {
const expected = 'https://api.dev.zoo.dev'
const actual = withAPIBaseURL('')
expect(actual).toBe(expected)
})
it('should return base url with /users', () => {
const expected = 'https://api.dev.zoo.dev/users'
const actual = withAPIBaseURL('/users')
expect(actual).toBe(expected)
})
it('should return a longer base url with /oauth2/token/revoke', () => {
const expected = 'https://api.dev.zoo.dev/oauth2/token/revoke'
const actual = withAPIBaseURL('/oauth2/token/revoke')
expect(actual).toBe(expected)
})
it('should ensure base url does not have ending slash', () => {
const expected = 'https://api.dev.zoo.dev'
const actual = withAPIBaseURL('')
expect(actual).toBe(expected)
const expectedEndsWith = expected[expected.length - 1]
const actualEndsWith = actual[actual.length - 1]
expect(actual).toBe(expected)
expect(actualEndsWith).toBe(expectedEndsWith)
})
})
})

View File

@ -1,5 +1,5 @@
import { VITE_KITTYCAD_API_BASE_URL } from '@src/env' import { VITE_KC_API_BASE_URL } from '@src/env'
export function withAPIBaseURL(path: string): string { export default function withBaseUrl(path: string): string {
return VITE_KITTYCAD_API_BASE_URL + path return VITE_KC_API_BASE_URL + path
} }

View File

@ -1,5 +1,10 @@
import type { Models } from '@kittycad/lib' import type { Models } from '@kittycad/lib'
import { VITE_KITTYCAD_API_TOKEN } from '@src/env' import {
DEV,
VITE_KC_API_BASE_URL,
VITE_KC_DEV_TOKEN,
VITE_KC_SKIP_AUTH,
} from '@src/env'
import { assign, fromPromise, setup } from 'xstate' import { assign, fromPromise, setup } from 'xstate'
import { COOKIE_NAME, OAUTH2_DEVICE_CLIENT_ID } from '@src/lib/constants' import { COOKIE_NAME, OAUTH2_DEVICE_CLIENT_ID } from '@src/lib/constants'
@ -10,9 +15,32 @@ import {
} from '@src/lib/desktop' } from '@src/lib/desktop'
import { isDesktop } from '@src/lib/isDesktop' import { isDesktop } from '@src/lib/isDesktop'
import { markOnce } from '@src/lib/performance' import { markOnce } from '@src/lib/performance'
import { withAPIBaseURL } from '@src/lib/withBaseURL' import {
default as withBaseURL,
default as withBaseUrl,
} from '@src/lib/withBaseURL'
import { ACTOR_IDS } from '@src/machines/machineConstants' import { ACTOR_IDS } from '@src/machines/machineConstants'
const SKIP_AUTH = VITE_KC_SKIP_AUTH === 'true' && DEV
const LOCAL_USER: Models['User_type'] = {
id: '8675309',
name: 'Test User',
email: 'kittycad.sidebar.test@example.com',
image: 'https://placekitten.com/200/200',
created_at: 'yesteryear',
updated_at: 'today',
company: 'Test Company',
discord: 'Test User#1234',
github: 'testuser',
phone: '555-555-5555',
first_name: 'Test',
last_name: 'User',
can_train_on_data: false,
is_service_account: false,
deletion_scheduled: false,
}
export interface UserContext { export interface UserContext {
user?: Models['User_type'] user?: Models['User_type']
token: string token: string
@ -28,21 +56,11 @@ export type Events =
} }
export const TOKEN_PERSIST_KEY = 'TOKEN_PERSIST_KEY' export const TOKEN_PERSIST_KEY = 'TOKEN_PERSIST_KEY'
/**
* Determine which token do we have persisted to initialize the auth machine
*/
const persistedCookie = getCookie(COOKIE_NAME)
const persistedLocalStorage = localStorage?.getItem(TOKEN_PERSIST_KEY) || ''
const persistedDevToken = VITE_KITTYCAD_API_TOKEN
export const persistedToken = export const persistedToken =
persistedDevToken || persistedCookie || persistedLocalStorage VITE_KC_DEV_TOKEN ||
console.log('Initial persisted token') getCookie(COOKIE_NAME) ||
console.table([ localStorage?.getItem(TOKEN_PERSIST_KEY) ||
['cookie', !!persistedCookie], ''
['local storage', !!persistedLocalStorage],
['api token', !!persistedDevToken],
])
export const authMachine = setup({ export const authMachine = setup({
types: {} as { types: {} as {
@ -139,7 +157,7 @@ export const authMachine = setup({
async function getUser(input: { token?: string }) { async function getUser(input: { token?: string }) {
const token = await getAndSyncStoredToken(input) const token = await getAndSyncStoredToken(input)
const url = withAPIBaseURL('/user') const url = withBaseURL('/user')
const headers: { [key: string]: string } = { const headers: { [key: string]: string } = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
} }
@ -147,8 +165,21 @@ async function getUser(input: { token?: string }) {
if (!token && isDesktop()) return Promise.reject(new Error('No token found')) if (!token && isDesktop()) return Promise.reject(new Error('No token found'))
if (token) headers['Authorization'] = `Bearer ${token}` if (token) headers['Authorization'] = `Bearer ${token}`
if (SKIP_AUTH) {
// For local tests
if (localStorage.getItem('FORCE_NO_IMAGE')) {
LOCAL_USER.image = ''
}
markOnce('code/didAuth')
return {
user: LOCAL_USER,
token,
}
}
const userPromise = isDesktop() const userPromise = isDesktop()
? getUserDesktop(token) ? getUserDesktop(token, VITE_KC_API_BASE_URL)
: fetch(url, { : fetch(url, {
method: 'GET', method: 'GET',
credentials: 'include', credentials: 'include',
@ -197,28 +228,16 @@ async function getAndSyncStoredToken(input: {
token?: string token?: string
}): Promise<string> { }): Promise<string> {
// dev mode // dev mode
if (VITE_KITTYCAD_API_TOKEN) { if (VITE_KC_DEV_TOKEN) return VITE_KC_DEV_TOKEN
console.log('Token used for authentication')
console.table([['api token', !!VITE_KITTYCAD_API_TOKEN]])
return VITE_KITTYCAD_API_TOKEN
}
const inputToken = input.token && input.token !== '' ? input.token : '' const token =
const cookieToken = getCookie(COOKIE_NAME) input.token && input.token !== ''
const localStorageToken = localStorage?.getItem(TOKEN_PERSIST_KEY) || '' ? input.token
const token = inputToken || cookieToken || localStorageToken : getCookie(COOKIE_NAME) || localStorage?.getItem(TOKEN_PERSIST_KEY) || ''
console.log('Token used for authentication')
console.table([
['persisted token', !!inputToken],
['cookie', !!cookieToken],
['local storage', !!localStorageToken],
['api token', !!VITE_KITTYCAD_API_TOKEN],
])
if (token) { if (token) {
// has just logged in, update storage
localStorage.setItem(TOKEN_PERSIST_KEY, token)
if (isDesktop()) { if (isDesktop()) {
// has just logged in, update storage
localStorage.setItem(TOKEN_PERSIST_KEY, token)
await writeTokenFile(token) await writeTokenFile(token)
} }
return token return token
@ -240,7 +259,7 @@ async function logout() {
if (token) { if (token) {
try { try {
await fetch(withAPIBaseURL('/oauth2/token/revoke'), { await fetch(withBaseUrl('/oauth2/token/revoke'), {
method: 'POST', method: 'POST',
credentials: 'include', credentials: 'include',
headers: { headers: {
@ -263,7 +282,7 @@ async function logout() {
} }
} }
return fetch(withAPIBaseURL('/logout'), { return fetch(withBaseUrl('/logout'), {
method: 'POST', method: 'POST',
credentials: 'include', credentials: 'include',
}) })

View File

@ -15,6 +15,7 @@ export type CommandBarContext = {
currentArgument?: CommandArgument<unknown> & { name: string } currentArgument?: CommandArgument<unknown> & { name: string }
argumentsToSubmit: { [x: string]: unknown } argumentsToSubmit: { [x: string]: unknown }
machineManager: MachineManager machineManager: MachineManager
additionalData: { [x: string]: unknown } // For storing extra parameters like scaleSketch
} }
export type CommandBarMachineEvent = export type CommandBarMachineEvent =
@ -77,6 +78,7 @@ export type CommandBarMachineEvent =
data: { [x: string]: CommandArgumentWithName<unknown> } data: { [x: string]: CommandArgumentWithName<unknown> }
} }
| { type: 'Set machine manager'; data: MachineManager } | { type: 'Set machine manager'; data: MachineManager }
| { type: 'Set additional data'; data: { [x: string]: unknown } }
export const commandBarMachine = setup({ export const commandBarMachine = setup({
types: { types: {
@ -117,6 +119,12 @@ export const commandBarMachine = setup({
resolvedArgs[argName] = resolvedArgs[argName] =
typeof argValue === 'function' ? argValue(context) : argValue typeof argValue === 'function' ? argValue(context) : argValue
} }
// Special handling for "Constrain with named value" command to include scaleSketch
if (selectedCommand.name === 'Constrain with named value') {
resolvedArgs.scaleSketch = context.additionalData.scaleSketch
}
selectedCommand?.onSubmit(resolvedArgs) selectedCommand?.onSubmit(resolvedArgs)
} else { } else {
selectedCommand?.onSubmit({ context, event }) selectedCommand?.onSubmit({ context, event })
@ -224,6 +232,16 @@ export const commandBarMachine = setup({
selectedCommand: undefined, selectedCommand: undefined,
currentArgument: undefined, currentArgument: undefined,
argumentsToSubmit: {}, argumentsToSubmit: {},
additionalData: {},
}),
'Set additional data': assign({
additionalData: ({ context, event }) => {
if (event.type !== 'Set additional data') return context.additionalData
return {
...context.additionalData,
...event.data,
}
},
}), }),
'Set selected command': assign({ 'Set selected command': assign({
selectedCommand: ({ context, event }) => selectedCommand: ({ context, event }) =>
@ -468,6 +486,7 @@ export const commandBarMachine = setup({
codeBasedSelections: [], codeBasedSelections: [],
}, },
argumentsToSubmit: {}, argumentsToSubmit: {},
additionalData: {},
machineManager: { machineManager: {
machines: [], machines: [],
machineApiIp: null, machineApiIp: null,
@ -626,6 +645,11 @@ export const commandBarMachine = setup({
actions: 'Set machine manager', actions: 'Set machine manager',
}, },
'Set additional data': {
reenter: false,
actions: 'Set additional data',
},
Close: { Close: {
target: '.Closed', target: '.Closed',
actions: 'Clear selected command', actions: 'Clear selected command',

View File

@ -11,7 +11,7 @@ import {
engineCommandManager, engineCommandManager,
kclManager, kclManager,
} from '@src/lib/singletons' } from '@src/lib/singletons'
import { VITE_KITTYCAD_API_TOKEN } from '@src/env' import { VITE_KC_DEV_TOKEN } from '@src/env'
import { getConstraintInfoKw } from '@src/lang/std/sketch' import { getConstraintInfoKw } from '@src/lang/std/sketch'
import { getNodeFromPath } from '@src/lang/queryAst' import { getNodeFromPath } from '@src/lang/queryAst'
import type { Node } from '@rust/kcl-lib/bindings/Node' import type { Node } from '@rust/kcl-lib/bindings/Node'
@ -19,6 +19,7 @@ import { err } from '@src/lib/trap'
import { import {
createIdentifier, createIdentifier,
createLiteral, createLiteral,
createLocalName,
createVariableDeclaration, createVariableDeclaration,
} from '@src/lang/create' } from '@src/lang/create'
import { ARG_END_ABSOLUTE, ARG_INTERIOR_ABSOLUTE } from '@src/lang/constants' import { ARG_END_ABSOLUTE, ARG_INTERIOR_ABSOLUTE } from '@src/lang/constants'
@ -29,9 +30,10 @@ import { removeSingleConstraintInfo } from '@src/lang/modifyAst'
beforeAll(async () => { beforeAll(async () => {
await initPromise await initPromise
// THESE TEST WILL FAIL without VITE_KC_DEV_TOKEN set in .env.development.local
await new Promise((resolve) => { await new Promise((resolve) => {
engineCommandManager.start({ engineCommandManager.start({
token: VITE_KITTYCAD_API_TOKEN, token: VITE_KC_DEV_TOKEN,
width: 256, width: 256,
height: 256, height: 256,
setMediaStream: () => {}, setMediaStream: () => {},
@ -1156,7 +1158,7 @@ p3 = [342.51, 216.38],
filter filter
) )
const constraint = constraintInfo[constraintIndex] const constraint = constraintInfo[constraintIndex]
console.log('constraint', constraint)
if (!constraint.argPosition) { if (!constraint.argPosition) {
throw new Error( throw new Error(
`Constraint at index ${constraintIndex} does not have argPosition` `Constraint at index ${constraintIndex} does not have argPosition`
@ -1291,3 +1293,279 @@ p3 = [342.51, 216.38],
) )
}) })
}) })
describe('testing sketch scale on first length constraint', () => {
it('should scale sketch when constrain with named value is used with scale checkbox enabled', async () => {
// Create a sketch with multiple segments using only literal values (no constraints)
const code = `sketch001 = startSketchOn(XZ)
profile001 = startProfile(sketch001, at = [100, 100])
|> line(end = [200, 0])
|> line(end = [0, 200])
|> line(end = [-200, 0])
|> close()
profile002 = circle(sketch001, center = [400, 400], radius = 50)
`
const ast = assertParse(code)
await kclManager.executeAst({ ast })
expect(kclManager.errors).toEqual([])
// Find a segment to constrain (the first line segment)
const indexOfInterest = code.indexOf('line(end = [200, 0])')
const artifact = [...kclManager.artifactGraph].find(
([_, artifact]) =>
artifact?.type === 'segment' &&
artifact.codeRef.range[0] <= indexOfInterest &&
indexOfInterest <= artifact.codeRef.range[1]
)?.[1]
if (!artifact || !('codeRef' in artifact)) {
throw new Error('Artifact not found or invalid artifact structure')
}
// Create modeling machine actor
const modelingActor = createActor(modelingMachine, {
input: modelingMachineDefaultContext,
}).start()
// Enter sketch mode
modelingActor.send({
type: 'Set selection',
data: {
selectionType: 'mirrorCodeMirrorSelections',
selection: {
graphSelections: [
{
artifact: artifact,
codeRef: artifact.codeRef,
},
],
otherSelections: [],
},
},
})
modelingActor.send({ type: 'Enter sketch' })
// Wait for sketch mode
await waitForCondition(() => {
const snapshot = modelingActor.getSnapshot()
return snapshot.value !== 'animating to existing sketch'
}, 5000)
expect(modelingActor.getSnapshot().value).toEqual({
Sketch: { SketchIdle: 'scene drawn' },
})
// Get constraint info for the segment
const callExp = getNodeFromPath<Node<CallExpressionKw>>(
kclManager.ast,
artifact.codeRef.pathToNode,
'CallExpressionKw'
)
if (err(callExp)) {
throw new Error('Failed to get CallExpressionKw node')
}
const constraintInfo = getConstraintInfoKw(
callExp.node,
codeManager.code,
artifact.codeRef.pathToNode
)
const constraint = constraintInfo[0] // First constraint (x value)
// Store original code to compare scaling
const originalCode = codeManager.code
// No need for command bar setup, we're testing the modeling machine directly
// Simulate submitting the command with scaling enabled
// The new value will be 100 (half of original 200), so scale factor should be 0.5
modelingActor.send({
type: 'Constrain with named value',
data: {
currentValue: {
valueText: constraint.value,
pathToNode: constraint.pathToNode,
variableName: 'length_var',
},
namedValue: {
valueText: '100',
variableName: 'length_var',
insertIndex: 0,
valueCalculated: '100',
variableDeclarationAst: createVariableDeclaration(
'length_var',
createLiteral('100')
),
variableIdentifierAst: createLocalName('length_var'),
valueAst: createLiteral('100'),
},
scaleSketch: true, // This should trigger scaling
},
})
// Wait for the constraint to be applied and sketch to be scaled
await waitForCondition(() => {
const snapshot = modelingActor.getSnapshot()
return (
JSON.stringify(snapshot.value) !==
JSON.stringify({ Sketch: 'Converting to named value' })
)
}, 5000)
// Wait for code to be updated
const startTime = Date.now()
while (codeManager.code === originalCode && Date.now() - startTime < 5000) {
await new Promise((resolve) => setTimeout(resolve, 100))
}
await new Promise((resolve) => setTimeout(resolve, 1000))
console.log('code is', codeManager.code)
// Verify the constraint was applied
expect(codeManager.code).toContain('length_var')
expect(codeManager.code).toContain("length_var = '100'")
// Verify scaling occurred - all dimensions should be scaled by 0.5 (100/200)
// Original values: line(end = [200, 0]), line(end = [0, 200]), line(end = [-200, 0])
// Scaled values should be: line(end = [100, 0]), line(end = [0, 100]), line(end = [-100, 0])
expect(codeManager.code).toContain('line(end = [length_var, 0])') // First line uses variable
expect(codeManager.code).toContain('line(end = [0, 100])') // Second line scaled
expect(codeManager.code).toContain('line(end = [-100, 0])') // Third line scaled
// Circle should also be scaled: radius = 50 -> radius = 25
expect(codeManager.code).toContain('radius = 25')
// Start positions should be scaled: at = [100, 100] -> at = [50, 50], at = [400, 400] -> at = [200, 200]
expect(codeManager.code).toContain('at = [50, 50]')
expect(codeManager.code).toContain('center = [200, 200]')
}, 15_000)
it('should not scale sketch when constrain with named value is used with scale checkbox disabled', async () => {
// Create a sketch with multiple segments using only literal values (no constraints)
const code = `sketch001 = startSketchOn(XZ)
profile001 = startProfile(sketch001, at = [100, 100])
|> line(end = [200, 0])
|> line(end = [0, 200])
|> close()`
const ast = assertParse(code)
await kclManager.executeAst({ ast })
expect(kclManager.errors).toEqual([])
// Find a segment to constrain (the first line segment)
const indexOfInterest = code.indexOf('line(end = [200, 0])')
const artifact = [...kclManager.artifactGraph].find(
([_, artifact]) =>
artifact?.type === 'segment' &&
artifact.codeRef.range[0] <= indexOfInterest &&
indexOfInterest <= artifact.codeRef.range[1]
)?.[1]
if (!artifact || !('codeRef' in artifact)) {
throw new Error('Artifact not found or invalid artifact structure')
}
// Create modeling machine actor
const modelingActor = createActor(modelingMachine, {
input: modelingMachineDefaultContext,
}).start()
// Enter sketch mode
modelingActor.send({
type: 'Set selection',
data: {
selectionType: 'mirrorCodeMirrorSelections',
selection: {
graphSelections: [
{
artifact: artifact,
codeRef: artifact.codeRef,
},
],
otherSelections: [],
},
},
})
modelingActor.send({ type: 'Enter sketch' })
// Wait for sketch mode
await waitForCondition(() => {
const snapshot = modelingActor.getSnapshot()
return snapshot.value !== 'animating to existing sketch'
}, 5000)
// Get constraint info for the segment
const callExp = getNodeFromPath<Node<CallExpressionKw>>(
kclManager.ast,
artifact.codeRef.pathToNode,
'CallExpressionKw'
)
if (err(callExp)) {
throw new Error('Failed to get CallExpressionKw node')
}
const constraintInfo = getConstraintInfoKw(
callExp.node,
codeManager.code,
artifact.codeRef.pathToNode
)
const constraint = constraintInfo[0] // First constraint (x value)
// Store original code to compare
const originalCode = codeManager.code
// Submit command with scaling disabled directly to modeling machine
modelingActor.send({
type: 'Constrain with named value',
data: {
currentValue: {
valueText: constraint.value,
pathToNode: constraint.pathToNode,
variableName: 'length_var',
},
namedValue: {
valueText: '100',
variableName: 'length_var',
insertIndex: 0,
valueCalculated: '100',
variableDeclarationAst: createVariableDeclaration(
'length_var',
createLiteral('100')
),
variableIdentifierAst: createLocalName('length_var'),
valueAst: createLiteral('100'),
},
scaleSketch: false, // Scaling disabled
},
})
// Wait for the constraint to be applied
await waitForCondition(() => {
const snapshot = modelingActor.getSnapshot()
return (
JSON.stringify(snapshot.value) !==
JSON.stringify({ Sketch: 'Converting to named value' })
)
}, 5000)
// Wait for code to be updated
const startTime = Date.now()
while (codeManager.code === originalCode && Date.now() - startTime < 5000) {
await new Promise((resolve) => setTimeout(resolve, 100))
}
// Verify the constraint was applied but no scaling occurred
expect(codeManager.code).toContain('length_var')
expect(codeManager.code).toContain("length_var = '100'")
expect(codeManager.code).toContain('line(end = [length_var, 0])')
// Other dimensions should remain unchanged (no scaling)
expect(codeManager.code).toContain('line(end = [0, 200])') // Should remain 200, not scaled to 100
expect(codeManager.code).toContain('at = [100, 100]') // Should remain [100, 100], not scaled
}, 15_000)
// Note: The third test for checking if scale checkbox is disabled when sketch has existing constraints
// would be better tested in the UI layer (CommandBarKclInput.tsx) or as an e2e test
// since it's primarily a UI behavior test
})

View File

@ -9,6 +9,7 @@ import {
orthoScale, orthoScale,
quaternionFromUpNForward, quaternionFromUpNForward,
} from '@src/clientSideScene/helpers' } from '@src/clientSideScene/helpers'
import { scaleProfiles } from '@src/clientSideScene/sceneEntities'
import type { Setting } from '@src/lib/settings/initialSettings' import type { Setting } from '@src/lib/settings/initialSettings'
import type { CameraProjectionType } from '@rust/kcl-lib/bindings/CameraProjectionType' import type { CameraProjectionType } from '@rust/kcl-lib/bindings/CameraProjectionType'
import { DRAFT_DASHED_LINE } from '@src/clientSideScene/sceneConstants' import { DRAFT_DASHED_LINE } from '@src/clientSideScene/sceneConstants'
@ -2248,6 +2249,55 @@ export const modelingMachine = setup({
return Promise.reject(new Error('Unexpected compilation error')) return Promise.reject(new Error('Unexpected compilation error'))
let parsed = pResult.program let parsed = pResult.program
// Apply sketch scaling if enabled
if (data.scaleSketch && sketchDetails.sketchNodePaths) {
const originalValue = parseFloat(data.currentValue.valueText)
const newValue = parseFloat(
typeof data.namedValue === 'object' &&
'valueText' in data.namedValue
? data.namedValue.valueText
: String(data.namedValue)
)
if (
!Number.isNaN(originalValue) &&
!Number.isNaN(newValue) &&
originalValue !== 0
) {
const scaleFactor = newValue / originalValue
try {
const scaleResult = scaleProfiles({
ast: parsed,
pathsToProfile: sketchDetails.sketchNodePaths,
factor: scaleFactor,
variables: kclManager.variables,
})
if (!err(scaleResult)) {
parsed = scaleResult.modifiedAst
// Reparse and recast to get fresh source ranges after scaling
const reparseResult = parse(recast(parsed))
if (!trap(reparseResult) && resultIsOk(reparseResult)) {
parsed = reparseResult.program
}
} else {
// Continue with constraint application even if scaling fails
console.warn(
'Failed to scale sketch, continuing with constraint application'
)
}
} catch (error) {
// Continue with constraint application even if scaling fails
console.warn(
'Error scaling sketch, continuing with constraint application:',
error
)
}
}
}
let result: { let result: {
modifiedAst: Node<Program> modifiedAst: Node<Program>
pathToReplaced: PathToNode | null pathToReplaced: PathToNode | null

View File

@ -1,5 +1,5 @@
import { engineCommandManager, kclManager } from '@src/lib/singletons' import { engineCommandManager, kclManager } from '@src/lib/singletons'
import { VITE_KITTYCAD_API_TOKEN } from '@src/env' import { VITE_KC_DEV_TOKEN } from '@src/env'
import { getModuleIdByFileName, isArray } from '@src/lib/utils' import { getModuleIdByFileName, isArray } from '@src/lib/utils'
import { vi, inject } from 'vitest' import { vi, inject } from 'vitest'
import { assertParse } from '@src/lang/wasm' import { assertParse } from '@src/lang/wasm'
@ -355,9 +355,10 @@ cases.push(
beforeAll(async () => { beforeAll(async () => {
await initPromise await initPromise
// THESE TEST WILL FAIL without VITE_KC_DEV_TOKEN set in .env.development.local
await new Promise((resolve) => { await new Promise((resolve) => {
engineCommandManager.start({ engineCommandManager.start({
token: VITE_KITTYCAD_API_TOKEN, token: VITE_KC_DEV_TOKEN,
width: 256, width: 256,
height: 256, height: 256,
setMediaStream: () => {}, setMediaStream: () => {},

View File

@ -70,10 +70,12 @@ dotenv.config({ path: [`.env.${NODE_ENV}.local`, `.env.${NODE_ENV}`] })
// default vite values based on mode // default vite values based on mode
process.env.NODE_ENV ??= viteEnv.MODE process.env.NODE_ENV ??= viteEnv.MODE
process.env.BASE_URL ??= viteEnv.VITE_KC_API_BASE_URL
process.env.VITE_KC_API_WS_MODELING_URL ??= viteEnv.VITE_KC_API_WS_MODELING_URL process.env.VITE_KC_API_WS_MODELING_URL ??= viteEnv.VITE_KC_API_WS_MODELING_URL
process.env.VITE_KITTYCAD_API_BASE_URL ??= viteEnv.VITE_KITTYCAD_API_BASE_URL process.env.VITE_KC_API_BASE_URL ??= viteEnv.VITE_KC_API_BASE_URL
process.env.VITE_KC_SITE_BASE_URL ??= viteEnv.VITE_KC_SITE_BASE_URL process.env.VITE_KC_SITE_BASE_URL ??= viteEnv.VITE_KC_SITE_BASE_URL
process.env.VITE_KC_SITE_APP_URL ??= viteEnv.VITE_KC_SITE_APP_URL process.env.VITE_KC_SITE_APP_URL ??= viteEnv.VITE_KC_SITE_APP_URL
process.env.VITE_KC_SKIP_AUTH ??= viteEnv.VITE_KC_SKIP_AUTH
process.env.VITE_KC_CONNECTION_TIMEOUT_MS ??= process.env.VITE_KC_CONNECTION_TIMEOUT_MS ??=
viteEnv.VITE_KC_CONNECTION_TIMEOUT_MS viteEnv.VITE_KC_CONNECTION_TIMEOUT_MS

View File

@ -289,11 +289,12 @@ contextBridge.exposeInMainWorld('electron', {
exposeProcessEnvs([ exposeProcessEnvs([
'NODE_ENV', 'NODE_ENV',
'VITE_KC_API_WS_MODELING_URL', 'VITE_KC_API_WS_MODELING_URL',
'VITE_KITTYCAD_API_BASE_URL', 'VITE_KC_API_BASE_URL',
'VITE_KC_SITE_BASE_URL', 'VITE_KC_SITE_BASE_URL',
'VITE_KC_SITE_APP_URL', 'VITE_KC_SITE_APP_URL',
'VITE_KC_SKIP_AUTH',
'VITE_KC_CONNECTION_TIMEOUT_MS', 'VITE_KC_CONNECTION_TIMEOUT_MS',
'VITE_KITTYCAD_API_TOKEN', 'VITE_KC_DEV_TOKEN',
'IS_PLAYWRIGHT', 'IS_PLAYWRIGHT',

View File

@ -6,7 +6,7 @@ import { Link } from 'react-router-dom'
import { ActionButton } from '@src/components/ActionButton' import { ActionButton } from '@src/components/ActionButton'
import { CustomIcon } from '@src/components/CustomIcon' import { CustomIcon } from '@src/components/CustomIcon'
import { Logo } from '@src/components/Logo' import { Logo } from '@src/components/Logo'
import { VITE_KC_SITE_BASE_URL } from '@src/env' 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'
@ -15,7 +15,6 @@ 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, generateSignInUrl } from '@src/routes/utils' import { APP_VERSION, generateSignInUrl } from '@src/routes/utils'
import { withAPIBaseURL } from '@src/lib/withBaseURL'
const subtleBorder = const subtleBorder =
'border border-solid border-chalkboard-30 dark:border-chalkboard-80' 'border border-solid border-chalkboard-30 dark:border-chalkboard-80'
@ -55,7 +54,7 @@ const SignIn = () => {
const signInDesktop = async () => { const signInDesktop = async () => {
// We want to invoke our command to login via device auth. // We want to invoke our command to login via device auth.
const userCodeToDisplay = await window.electron const userCodeToDisplay = await window.electron
.startDeviceFlow(withAPIBaseURL(location.search)) .startDeviceFlow(VITE_KC_API_BASE_URL + location.search)
.catch(reportError) .catch(reportError)
if (!userCodeToDisplay) { if (!userCodeToDisplay) {
console.error('No user code received while trying to log in') console.error('No user code received while trying to log in')