Compare commits

..

13 Commits

51 changed files with 1791 additions and 526 deletions

View File

@ -3,17 +3,18 @@
NODE_ENV=development
DEV=true
# App
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_APP_URL=https://app.dev.zoo.dev
VITE_KC_SKIP_AUTH=false
VITE_KC_CONNECTION_TIMEOUT_MS=5000
#VITE_WASM_URL="optional override of Wasm URL if not on default port 3000"
#VITE_KITTYCAD_API_TOKEN="required for testing, optional to skip auth in the app"
FAIL_ON_CONSOLE_ERRORS=true
#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_KC_DEV_TOKEN="optional token to skip auth in the app"
#token="required token for playwright. TODO: clean up env vars in #3973"
# KCL
RUST_BACKTRACE=1
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
# App
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_APP_URL=https://app.zoo.dev
VITE_KC_SKIP_AUTH=false
VITE_KC_CONNECTION_TIMEOUT_MS=15000

View File

@ -157,7 +157,7 @@ jobs:
timeout_minutes: 5
max_attempts: 5
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_KEY: ${{ secrets.TAB_API_KEY }}
CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
@ -169,7 +169,7 @@ jobs:
if: always()
run: npm run test:snapshots -- --last-failed --update-snapshots
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_KEY: ${{ secrets.TAB_API_KEY }}
CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
@ -284,7 +284,7 @@ jobs:
timeout_minutes: 5
max_attempts: 5
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_KEY: ${{ secrets.TAB_API_KEY }}
CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
@ -410,7 +410,7 @@ jobs:
max_attempts: 9
env:
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_KEY: ${{ secrets.TAB_API_KEY }}
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' }}
run: xvfb-run -a npm run test:unit
env:
VITE_KITTYCAD_API_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
- name: Check for changes
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
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
@ -96,7 +96,7 @@ To package the app for your platform with electron-builder, run `npm run tronb:p
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)
@ -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 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

View File

@ -49,7 +49,7 @@ RUST_SOURCES := $(wildcard rust/**/*.rs)
REACT_SOURCES := $(wildcard src/*.tsx) $(wildcard src/**/*.tsx)
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
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'
// test file is for testing auth functionality
test.describe('Authentication tests', () => {
test(
`The user can sign out and back in`,
@ -12,12 +13,22 @@ test.describe('Authentication tests', () => {
await page.setBodyDimensions({ width: 1000, height: 500 })
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 toolbar.userSidebarButton.click()
await toolbar.signOutButton.click()
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 signInPage.signInButton.click()
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'
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'

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
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(

7
interface.d.ts vendored
View File

@ -72,13 +72,16 @@ export interface IElectronAPI {
}
process: {
env: {
BASE_URL: string
IS_PLAYWRIGHT: string
VITE_KITTYCAD_API_TOKEN: string
VITE_KC_DEV_TOKEN: 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_APP_URL: string
VITE_KC_SKIP_AUTH: string
VITE_KC_CONNECTION_TIMEOUT_MS: string
VITE_KC_DEV_TOKEN: string
NODE_ENV: string
PROD: string
DEV: string

234
package-lock.json generated
View File

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

View File

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

View File

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

View File

@ -10,76 +10,71 @@ DATA;
NAMED_UNIT(*)
SI_UNIT($, .METRE.)
);
#2 = (
NAMED_UNIT(*)
PLANE_ANGLE_UNIT()
SI_UNIT($, .RADIAN.)
);
#3 = UNCERTAINTY_MEASURE_WITH_UNIT(0.00001, #1, 'DISTANCE_ACCURACY_VALUE', $);
#4 = (
#2 = UNCERTAINTY_MEASURE_WITH_UNIT(0.00001, #1, 'DISTANCE_ACCURACY_VALUE', $);
#3 = (
GEOMETRIC_REPRESENTATION_CONTEXT(3)
GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#3))
GLOBAL_UNIT_ASSIGNED_CONTEXT((#1, #2))
GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#2))
GLOBAL_UNIT_ASSIGNED_CONTEXT((#1))
REPRESENTATION_CONTEXT('', '3D')
);
#5 = CARTESIAN_POINT('NONE', (0.015, -0.01, -0.005));
#6 = VERTEX_POINT('NONE', #5);
#7 = CARTESIAN_POINT('NONE', (0.015, 0, -0.005));
#8 = VERTEX_POINT('NONE', #7);
#9 = DIRECTION('NONE', (1, 0, -0));
#10 = DIRECTION('NONE', (0, 1, 0));
#11 = CARTESIAN_POINT('NONE', (0.005, -0.01, -0.005));
#12 = AXIS2_PLACEMENT_3D('NONE', #11, #10, #9);
#13 = CIRCLE('NONE', #12, 0.01);
#14 = DIRECTION('NONE', (0, 1, 0));
#15 = VECTOR('NONE', #14, 1);
#16 = CARTESIAN_POINT('NONE', (0.015, -0.01, -0.005));
#17 = LINE('NONE', #16, #15);
#18 = DIRECTION('NONE', (1, 0, -0));
#19 = DIRECTION('NONE', (0, 1, 0));
#20 = CARTESIAN_POINT('NONE', (0.005, 0, -0.005));
#21 = AXIS2_PLACEMENT_3D('NONE', #20, #19, #18);
#22 = CIRCLE('NONE', #21, 0.01);
#23 = EDGE_CURVE('NONE', #6, #6, #13, .T.);
#24 = EDGE_CURVE('NONE', #6, #8, #17, .T.);
#25 = EDGE_CURVE('NONE', #8, #8, #22, .T.);
#26 = CARTESIAN_POINT('NONE', (0.005, -0.005, -0.005));
#27 = DIRECTION('NONE', (0, 1, 0));
#28 = DIRECTION('NONE', (1, 0, -0));
#29 = AXIS2_PLACEMENT_3D('NONE', #26, #27, #28);
#30 = CYLINDRICAL_SURFACE('NONE', #29, 0.01);
#31 = CARTESIAN_POINT('NONE', (0, -0.01, -0));
#32 = DIRECTION('NONE', (0, 1, 0));
#33 = AXIS2_PLACEMENT_3D('NONE', #31, #32, $);
#34 = PLANE('NONE', #33);
#35 = CARTESIAN_POINT('NONE', (0, 0, -0));
#36 = DIRECTION('NONE', (0, 1, 0));
#37 = AXIS2_PLACEMENT_3D('NONE', #35, #36, $);
#38 = PLANE('NONE', #37);
#39 = ORIENTED_EDGE('NONE', *, *, #23, .T.);
#40 = ORIENTED_EDGE('NONE', *, *, #25, .F.);
#41 = EDGE_LOOP('NONE', (#39));
#42 = FACE_BOUND('NONE', #41, .T.);
#43 = EDGE_LOOP('NONE', (#40));
#44 = FACE_BOUND('NONE', #43, .T.);
#45 = ADVANCED_FACE('NONE', (#42, #44), #30, .T.);
#46 = ORIENTED_EDGE('NONE', *, *, #23, .F.);
#47 = EDGE_LOOP('NONE', (#46));
#48 = FACE_BOUND('NONE', #47, .T.);
#49 = ADVANCED_FACE('NONE', (#48), #34, .F.);
#50 = ORIENTED_EDGE('NONE', *, *, #25, .T.);
#51 = EDGE_LOOP('NONE', (#50));
#52 = FACE_BOUND('NONE', #51, .T.);
#53 = ADVANCED_FACE('NONE', (#52), #38, .T.);
#54 = CLOSED_SHELL('NONE', (#45, #49, #53));
#55 = MANIFOLD_SOLID_BREP('NONE', #54);
#56 = APPLICATION_CONTEXT('configuration controlled 3D design of mechanical parts and assemblies');
#57 = PRODUCT_DEFINITION_CONTEXT('part definition', #56, 'design');
#58 = PRODUCT('UNIDENTIFIED_PRODUCT', 'NONE', $, ());
#59 = PRODUCT_DEFINITION_FORMATION('', $, #58);
#60 = PRODUCT_DEFINITION('design', $, #59, #57);
#61 = PRODUCT_DEFINITION_SHAPE('NONE', $, #60);
#62 = ADVANCED_BREP_SHAPE_REPRESENTATION('NONE', (#55), #4);
#63 = SHAPE_DEFINITION_REPRESENTATION(#61, #62);
#4 = CARTESIAN_POINT('NONE', (0.015, -0.01, -0.005));
#5 = VERTEX_POINT('NONE', #4);
#6 = CARTESIAN_POINT('NONE', (0.015, 0, -0.005));
#7 = VERTEX_POINT('NONE', #6);
#8 = DIRECTION('NONE', (1, 0, -0));
#9 = DIRECTION('NONE', (0, 1, 0));
#10 = CARTESIAN_POINT('NONE', (0.005, -0.01, -0.005));
#11 = AXIS2_PLACEMENT_3D('NONE', #10, #9, #8);
#12 = CIRCLE('NONE', #11, 0.01);
#13 = DIRECTION('NONE', (0, 1, 0));
#14 = VECTOR('NONE', #13, 1);
#15 = CARTESIAN_POINT('NONE', (0.015, -0.01, -0.005));
#16 = LINE('NONE', #15, #14);
#17 = DIRECTION('NONE', (1, 0, -0));
#18 = DIRECTION('NONE', (0, 1, 0));
#19 = CARTESIAN_POINT('NONE', (0.005, 0, -0.005));
#20 = AXIS2_PLACEMENT_3D('NONE', #19, #18, #17);
#21 = CIRCLE('NONE', #20, 0.01);
#22 = EDGE_CURVE('NONE', #5, #5, #12, .T.);
#23 = EDGE_CURVE('NONE', #5, #7, #16, .T.);
#24 = EDGE_CURVE('NONE', #7, #7, #21, .T.);
#25 = CARTESIAN_POINT('NONE', (0.005, -0.005, -0.005));
#26 = DIRECTION('NONE', (0, 1, 0));
#27 = DIRECTION('NONE', (1, 0, -0));
#28 = AXIS2_PLACEMENT_3D('NONE', #25, #26, #27);
#29 = CYLINDRICAL_SURFACE('NONE', #28, 0.01);
#30 = CARTESIAN_POINT('NONE', (0, -0.01, -0));
#31 = DIRECTION('NONE', (0, 1, 0));
#32 = AXIS2_PLACEMENT_3D('NONE', #30, #31, $);
#33 = PLANE('NONE', #32);
#34 = CARTESIAN_POINT('NONE', (0, 0, -0));
#35 = DIRECTION('NONE', (0, 1, 0));
#36 = AXIS2_PLACEMENT_3D('NONE', #34, #35, $);
#37 = PLANE('NONE', #36);
#38 = ORIENTED_EDGE('NONE', *, *, #22, .T.);
#39 = ORIENTED_EDGE('NONE', *, *, #24, .F.);
#40 = EDGE_LOOP('NONE', (#38));
#41 = FACE_BOUND('NONE', #40, .T.);
#42 = EDGE_LOOP('NONE', (#39));
#43 = FACE_BOUND('NONE', #42, .T.);
#44 = ADVANCED_FACE('NONE', (#41, #43), #29, .T.);
#45 = ORIENTED_EDGE('NONE', *, *, #22, .F.);
#46 = EDGE_LOOP('NONE', (#45));
#47 = FACE_BOUND('NONE', #46, .T.);
#48 = ADVANCED_FACE('NONE', (#47), #33, .F.);
#49 = ORIENTED_EDGE('NONE', *, *, #24, .T.);
#50 = EDGE_LOOP('NONE', (#49));
#51 = FACE_BOUND('NONE', #50, .T.);
#52 = ADVANCED_FACE('NONE', (#51), #37, .T.);
#53 = CLOSED_SHELL('NONE', (#44, #48, #52));
#54 = MANIFOLD_SOLID_BREP('NONE', #53);
#55 = APPLICATION_CONTEXT('configuration controlled 3D design of mechanical parts and assemblies');
#56 = PRODUCT_DEFINITION_CONTEXT('part definition', #55, 'design');
#57 = PRODUCT('UNIDENTIFIED_PRODUCT', 'NONE', $, ());
#58 = PRODUCT_DEFINITION_FORMATION('', $, #57);
#59 = PRODUCT_DEFINITION('design', $, #58, #56);
#60 = PRODUCT_DEFINITION_SHAPE('NONE', $, #59);
#61 = ADVANCED_BREP_SHAPE_REPRESENTATION('NONE', (#54), #3);
#62 = SHAPE_DEFINITION_REPRESENTATION(#60, #61);
ENDSEC;
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.
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) => {
if let Some(value) = map.get(&property) {
Ok(value.to_owned())
@ -1046,22 +1013,7 @@ impl Node<MemberExpression> {
vec![self.clone().into()],
)))
}
(KclValue::Object { value: map, .. }, p @ Property::UInt(i), _) => {
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());
}
(KclValue::Object { .. }, p, _) => {
let t = p.type_name();
let article = article_for(t);
Err(KclError::new_semantic(KclErrorDetails::new(
@ -2253,12 +2205,4 @@ y = x[0mm + 1]
"#;
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,
}
}
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 {

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> {
match self {
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
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
404 https://github.com/KittyCAD/engine/issues/3528
404 https://github.com/KittyCAD/modeling-app/commit/${ref}

View File

@ -1026,7 +1026,7 @@ export class SceneEntities {
const sketch = sketchFromPathToNode({
pathToNode: sketchEntryNodePath,
variables: this.kclManager.variables,
kclManager: this.kclManager,
ast: this.kclManager.ast,
})
if (err(sketch)) return Promise.reject(sketch)
if (!sketch) return Promise.reject(new Error('No sketch found'))
@ -1171,12 +1171,64 @@ export class SceneEntities {
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) {
this.sceneInfra.modelingSend({ type: 'Close sketch' })
} 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(
sketchEntryNodePath,
sketchNodePaths,
updatedSketchNodePaths,
planeNodePath,
forward,
up,
@ -2530,7 +2582,7 @@ export class SceneEntities {
const sketch = sketchFromPathToNode({
pathToNode,
variables: this.kclManager.variables,
kclManager: this.kclManager,
ast: this.kclManager.ast,
})
if (trap(sketch)) return
if (!sketch) {
@ -3784,14 +3836,14 @@ function prepareTruncatedAst(
function sketchFromPathToNode({
pathToNode,
variables,
kclManager,
ast,
}: {
pathToNode: PathToNode
variables: VariableMap
kclManager: KclManager
ast: Node<Program>
}): Sketch | null | Error {
const _varDec = getNodeFromPath<VariableDeclarator>(
kclManager.ast,
ast,
pathToNode,
'VariableDeclarator'
)
@ -3808,6 +3860,141 @@ function sketchFromPathToNode({
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) {
const segmentHead = getParentGroup(object, [ARROWHEAD, PROFILE_START])
if (segmentHead) {
@ -3837,7 +4024,7 @@ export function getSketchQuaternion(
const sketch = sketchFromPathToNode({
pathToNode: sketchPathToNode,
variables: kclManager.variables,
kclManager,
ast: kclManager.ast,
})
if (err(sketch)) return sketch
const zAxis =
@ -3895,7 +4082,7 @@ function getSketchesInfo({
const sketch = sketchFromPathToNode({
pathToNode: path,
variables,
kclManager,
ast: kclManager.ast,
})
if (err(sketch)) continue
if (!sketch) continue
@ -3974,3 +4161,44 @@ function findTangentDirection(segmentGroup: Group) {
}
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 { useSettings } 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'
@ -50,6 +52,7 @@ function CommandBarKclInput({
stepBack: () => void
onSubmit: (event: unknown) => void
}) {
// console.log('arg', arg)
const commandBarState = useCommandBarState()
const previouslySetValue = commandBarState.context.argumentsToSubmit[
arg.name
@ -109,6 +112,32 @@ function CommandBarKclInput({
arg.createVariable === 'force' ||
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)
useHotkeys('mod + k, mod + /', () => commandBarActor.send({ type: 'Close' }))
const editorRef = useRef<HTMLDivElement>(null)
@ -225,6 +254,23 @@ function CommandBarKclInput({
)
}, [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>) {
e?.preventDefault()
if (!canSubmit || valueNode === null) {
@ -237,26 +283,26 @@ function CommandBarKclInput({
return
}
onSubmit(
createNewVariable
? ({
valueAst: valueNode,
valueText: value,
valueCalculated: calcResult,
variableName: newVariableName,
insertIndex: newVariableInsertIndex,
variableIdentifierAst: createLocalName(newVariableName),
variableDeclarationAst: createVariableDeclaration(
newVariableName,
valueNode
),
} satisfies KclCommandValue)
: ({
valueAst: valueNode,
valueText: value,
valueCalculated: calcResult,
} satisfies KclCommandValue)
)
const commandValue = createNewVariable
? ({
valueAst: valueNode,
valueText: value,
valueCalculated: calcResult,
variableName: newVariableName,
insertIndex: newVariableInsertIndex,
variableIdentifierAst: createLocalName(newVariableName),
variableDeclarationAst: createVariableDeclaration(
newVariableName,
valueNode
),
} satisfies KclCommandValue)
: ({
valueAst: valueNode,
valueText: value,
valueCalculated: calcResult,
} satisfies KclCommandValue)
onSubmit(commandValue)
}
return (
@ -359,6 +405,34 @@ function CommandBarKclInput({
)}
</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>
)
}

View File

@ -7,7 +7,7 @@ import {
LanguageServerClient,
LspWorkerEventType,
} 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 { useNavigate } from 'react-router-dom'
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 { err } from '@src/lib/trap'
import { useToken } from '@src/lib/singletons'
import { withAPIBaseURL } from '@src/lib/withBaseURL'
function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
return []
@ -86,7 +85,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const initEvent: KclWorkerOptions = {
wasmUrl: wasmUrl(),
token: token,
apiBaseUrl: withAPIBaseURL(''),
apiBaseUrl: VITE_KC_API_BASE_URL,
}
lspWorker.postMessage({
worker: LspWorker.Kcl,
@ -179,7 +178,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const initEvent: CopilotWorkerOptions = {
wasmUrl: wasmUrl(),
token: token,
apiBaseUrl: withAPIBaseURL(''),
apiBaseUrl: VITE_KC_API_BASE_URL,
}
lspWorker.postMessage({
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
| string
| 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_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 =
env.VITE_KC_CONNECTION_TIMEOUT_MS as string | undefined
export const VITE_KITTYCAD_API_TOKEN = env.VITE_KITTYCAD_API_TOKEN as
| string
| undefined
export const VITE_KC_DEV_TOKEN = env.VITE_KC_DEV_TOKEN as string | undefined
export const PROD = env.PROD as string | undefined
export const TEST = env.TEST 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_LEG = 'leg'
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 { topLevelRange } from '@src/lang/util'
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 { enginelessExecutor } from '@src/lib/testHelpers'
import { err } from '@src/lib/trap'
import { deleteFromSelection } from '@src/lang/modifyAst/deleteFromSelection'
import { assertNotErr } from '@src/unitTestUtils'
import { scaleProfiles } from '@src/clientSideScene/sceneEntities'
beforeAll(async () => {
await initPromise
@ -917,3 +918,223 @@ extrude001 = extrude(part001, length = 5)
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 type {
@ -40,9 +40,10 @@ import { isOverlap } from '@src/lib/utils'
beforeAll(async () => {
await initPromise
// THESE TEST WILL FAIL without VITE_KC_DEV_TOKEN set in .env.development.local
await new Promise((resolve) => {
engineCommandManager.start({
token: VITE_KITTYCAD_API_TOKEN,
token: VITE_KC_DEV_TOKEN,
width: 256,
height: 256,
setMediaStream: () => {},

View File

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

View File

@ -10,6 +10,7 @@ import {
createPipeSubstitution,
} from '@src/lang/create'
import {
doesProfileHaveAnyConstrainedDimension,
doesSceneHaveExtrudedSketch,
doesSceneHaveSweepableSketch,
findAllPreviousVariables,
@ -35,6 +36,95 @@ beforeAll(async () => {
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', () => {
it('should find all previous variables', async () => {
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 type { KclCommandValue } from '@src/lib/commandTypes'
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'
/**
@ -1293,3 +1310,146 @@ export const getPathNormalisedForTruncatedAst = (
Number(nodePathWithCorrectedIndexForTruncatedAst[1][0]) - minIndex
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 { 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 { BSON } from 'bson'
@ -400,7 +400,7 @@ class EngineConnection extends EventTarget {
this.send({
type: 'headers',
headers: {
Authorization: `Bearer ${VITE_KITTYCAD_API_TOKEN}`,
Authorization: `Bearer ${VITE_KC_DEV_TOKEN}`,
},
})
}

View File

@ -24,6 +24,7 @@ import {
ARG_TAG,
DETERMINING_ARGS,
ARG_INTERIOR_ABSOLUTE,
ARG_DIAMETER,
} from '@src/lang/constants'
import {
createArrayExpression,
@ -1424,13 +1425,27 @@ export const circle: SketchLineHelperKw = {
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([
createLiteral(roundOff(center[0])),
createLiteral(roundOff(center[1])),
])
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 {
modifiedAst: _node,
pathToNode,
@ -4168,7 +4183,14 @@ const tangentialArcHelpers = {
.map((arg) => arg.label?.name)
.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
const previousEndTangent = input.previousEndTangent
if (previousEndTangent) {
@ -4219,11 +4241,19 @@ const tangentialArcHelpers = {
const radius = distance2d(center, from)
mutateKwArg(
ARG_RADIUS,
callExpression,
createLiteral(roundOff(radius, 2))
)
if (!isDiameter) {
mutateKwArg(
ARG_RADIUS,
callExpression,
createLiteral(roundOff(radius, 2))
)
} else {
mutateKwArg(
ARG_DIAMETER,
callExpression,
createLiteral(roundOff(radius * 2, 2))
)
}
const angleValue = createLiteralMaybeSuffix({
value: roundOff(angle, 2),
suffix: 'Deg',

View File

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

View File

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

View File

@ -26,7 +26,6 @@ import { err } from '@src/lib/trap'
import type { DeepPartial } from '@src/lib/types'
import { getInVariableCase } from '@src/lib/utils'
import { IS_STAGING } from '@src/routes/utils'
import { withAPIBaseURL } from '@src/lib/withBaseURL'
export async function renameProjectDirectory(
projectPath: string,
@ -698,9 +697,7 @@ export const readTokenFile = async () => {
export const writeTokenFile = async (token: string) => {
const tokenFilePath = await getTokenFilePath()
if (err(token)) return Promise.reject(token)
const result = window.electron.writeFile(tokenFilePath, token)
console.log('token written to disk')
return result
return window.electron.writeFile(tokenFilePath, token)
}
export const writeTelemetryFile = async (content: string) => {
@ -725,9 +722,12 @@ export const setState = async (state: Project | undefined): Promise<void> => {
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 {
const user = await fetch(withAPIBaseURL('/users/me'), {
const user = await fetch(`${hostname}/users/me`, {
headers: new Headers({
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 { stringToBase64 } from '@src/lib/base64'
@ -7,7 +7,6 @@ import {
CREATE_FILE_URL_PARAM,
} from '@src/lib/constants'
import { err } from '@src/lib/trap'
import { withAPIBaseURL } from '@src/lib/withBaseURL'
export interface FileLinkParams {
code: string
@ -97,7 +96,7 @@ export async function createShortlink(
if (password) {
body.password = password
}
const response = await fetch(withAPIBaseURL('/user/shortlinks'), {
const response = await fetch(`${VITE_KC_API_BASE_URL}/user/shortlinks`, {
method: 'POST',
headers: {
'Content-type': 'application/json',

View File

@ -1,7 +1,7 @@
import type { SelectionRange } from '@codemirror/state'
import { EditorSelection, Transaction } from '@codemirror/state'
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 toast from 'react-hot-toast'
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 { FileMeta } from '@src/lib/types'
import type { RequestedKCLFile } from '@src/machines/systemIO/utils'
import { withAPIBaseURL } from '@src/lib/withBaseURL'
type KclFileMetaMap = {
[execStateFileNamesIndex: number]: Extract<FileMeta, { type: 'kcl' }>
@ -78,7 +77,7 @@ async function submitTextToCadRequest(
})
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',
headers: {
@ -305,7 +304,7 @@ export async function getPromptToEditResult(
id: string,
token?: string
): 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 =
await crossPlatformFetch(
url,
@ -336,6 +335,14 @@ export async function doPromptEdit({
const toastId = toast.loading('Submitting to Text-to-CAD API...')
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 {
submitResult = await submitPromptToEditToQueue({
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 { KclManager } from '@src/lang/KclSingleton'
@ -171,7 +171,7 @@ const appMachine = setup({
systemId: BILLING,
input: {
...BILLING_CONTEXT_DEFAULTS,
urlUserService: withAPIBaseURL(''),
urlUserService: VITE_KC_API_BASE_URL,
},
}),
],

View File

@ -1,4 +1,5 @@
import type { Models } from '@kittycad/lib'
import { VITE_KC_API_BASE_URL } from '@src/env'
import toast from 'react-hot-toast'
import type { NavigateFunction } from 'react-router-dom'
import {
@ -18,7 +19,6 @@ import { err, reportRejection } from '@src/lib/trap'
import { toSync } from '@src/lib/utils'
import { getAllSubDirectoriesAtProjectRoot } from '@src/machines/systemIO/snapshotContext'
import { joinOSPaths } from '@src/lib/paths'
import { withAPIBaseURL } from '@src/lib/withBaseURL'
export async function submitTextToCadPrompt(
prompt: string,
@ -32,7 +32,7 @@ export async function submitTextToCadPrompt(
kcl_version: kclManager.kclVersion,
}
// 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(
url,
{
@ -58,7 +58,7 @@ export async function getTextToCadResult(
id: string,
token?: string
): 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(
url,
{

View File

@ -1,13 +1,14 @@
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 { withAPIBaseURL } from '@src/lib/withBaseURL'
export async function sendTelemetry(
id: string,
feedback: Models['MlFeedback_type'],
token?: string
): 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(
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 {
return VITE_KITTYCAD_API_BASE_URL + path
export default function withBaseUrl(path: string): string {
return VITE_KC_API_BASE_URL + path
}

View File

@ -1,5 +1,10 @@
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 { COOKIE_NAME, OAUTH2_DEVICE_CLIENT_ID } from '@src/lib/constants'
@ -10,9 +15,32 @@ import {
} from '@src/lib/desktop'
import { isDesktop } from '@src/lib/isDesktop'
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'
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 {
user?: Models['User_type']
token: string
@ -28,21 +56,11 @@ export type Events =
}
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 =
persistedDevToken || persistedCookie || persistedLocalStorage
console.log('Initial persisted token')
console.table([
['cookie', !!persistedCookie],
['local storage', !!persistedLocalStorage],
['api token', !!persistedDevToken],
])
VITE_KC_DEV_TOKEN ||
getCookie(COOKIE_NAME) ||
localStorage?.getItem(TOKEN_PERSIST_KEY) ||
''
export const authMachine = setup({
types: {} as {
@ -139,7 +157,7 @@ export const authMachine = setup({
async function getUser(input: { token?: string }) {
const token = await getAndSyncStoredToken(input)
const url = withAPIBaseURL('/user')
const url = withBaseURL('/user')
const headers: { [key: string]: string } = {
'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) 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()
? getUserDesktop(token)
? getUserDesktop(token, VITE_KC_API_BASE_URL)
: fetch(url, {
method: 'GET',
credentials: 'include',
@ -197,28 +228,16 @@ async function getAndSyncStoredToken(input: {
token?: string
}): Promise<string> {
// dev mode
if (VITE_KITTYCAD_API_TOKEN) {
console.log('Token used for authentication')
console.table([['api token', !!VITE_KITTYCAD_API_TOKEN]])
return VITE_KITTYCAD_API_TOKEN
}
if (VITE_KC_DEV_TOKEN) return VITE_KC_DEV_TOKEN
const inputToken = input.token && input.token !== '' ? input.token : ''
const cookieToken = getCookie(COOKIE_NAME)
const localStorageToken = localStorage?.getItem(TOKEN_PERSIST_KEY) || ''
const token = inputToken || cookieToken || localStorageToken
console.log('Token used for authentication')
console.table([
['persisted token', !!inputToken],
['cookie', !!cookieToken],
['local storage', !!localStorageToken],
['api token', !!VITE_KITTYCAD_API_TOKEN],
])
const token =
input.token && input.token !== ''
? input.token
: getCookie(COOKIE_NAME) || localStorage?.getItem(TOKEN_PERSIST_KEY) || ''
if (token) {
// has just logged in, update storage
localStorage.setItem(TOKEN_PERSIST_KEY, token)
if (isDesktop()) {
// has just logged in, update storage
localStorage.setItem(TOKEN_PERSIST_KEY, token)
await writeTokenFile(token)
}
return token
@ -240,7 +259,7 @@ async function logout() {
if (token) {
try {
await fetch(withAPIBaseURL('/oauth2/token/revoke'), {
await fetch(withBaseUrl('/oauth2/token/revoke'), {
method: 'POST',
credentials: 'include',
headers: {
@ -263,7 +282,7 @@ async function logout() {
}
}
return fetch(withAPIBaseURL('/logout'), {
return fetch(withBaseUrl('/logout'), {
method: 'POST',
credentials: 'include',
})

View File

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

View File

@ -11,7 +11,7 @@ import {
engineCommandManager,
kclManager,
} 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 { getNodeFromPath } from '@src/lang/queryAst'
import type { Node } from '@rust/kcl-lib/bindings/Node'
@ -19,6 +19,7 @@ import { err } from '@src/lib/trap'
import {
createIdentifier,
createLiteral,
createLocalName,
createVariableDeclaration,
} from '@src/lang/create'
import { ARG_END_ABSOLUTE, ARG_INTERIOR_ABSOLUTE } from '@src/lang/constants'
@ -29,9 +30,10 @@ import { removeSingleConstraintInfo } from '@src/lang/modifyAst'
beforeAll(async () => {
await initPromise
// THESE TEST WILL FAIL without VITE_KC_DEV_TOKEN set in .env.development.local
await new Promise((resolve) => {
engineCommandManager.start({
token: VITE_KITTYCAD_API_TOKEN,
token: VITE_KC_DEV_TOKEN,
width: 256,
height: 256,
setMediaStream: () => {},
@ -1156,7 +1158,7 @@ p3 = [342.51, 216.38],
filter
)
const constraint = constraintInfo[constraintIndex]
console.log('constraint', constraint)
if (!constraint.argPosition) {
throw new Error(
`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,
quaternionFromUpNForward,
} from '@src/clientSideScene/helpers'
import { scaleProfiles } from '@src/clientSideScene/sceneEntities'
import type { Setting } from '@src/lib/settings/initialSettings'
import type { CameraProjectionType } from '@rust/kcl-lib/bindings/CameraProjectionType'
import { DRAFT_DASHED_LINE } from '@src/clientSideScene/sceneConstants'
@ -2248,6 +2249,55 @@ export const modelingMachine = setup({
return Promise.reject(new Error('Unexpected compilation error'))
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: {
modifiedAst: Node<Program>
pathToReplaced: PathToNode | null

View File

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

View File

@ -70,10 +70,12 @@ dotenv.config({ path: [`.env.${NODE_ENV}.local`, `.env.${NODE_ENV}`] })
// default vite values based on 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_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_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 ??=
viteEnv.VITE_KC_CONNECTION_TIMEOUT_MS

View File

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

View File

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