Compare commits

..

8 Commits

Author SHA1 Message Date
5474b3409e Merge branch 'main' into kurt-contraint-colours 2025-07-02 12:18:46 +10:00
29ae16fbf0 fix console noise 2025-07-02 11:55:50 +10:00
38ee257996 Merge branch 'main' into kurt-contraint-colours 2025-07-02 06:03:07 +10:00
47c29b2681 Update snapshots 2025-07-01 08:18:39 +00:00
23f51d73ee Update snapshots 2025-07-01 08:04:33 +00:00
f752a496de fix package 2025-07-01 17:51:02 +10:00
6545fb6db0 package 2025-07-01 17:26:15 +10:00
e63eb18d65 constraint colors 2025-07-01 16:46:43 +10:00
56 changed files with 935 additions and 947 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -187,68 +187,6 @@ sketch001 = startProfile(sketch002, at = [12.34, -12.34])
page.getByRole('button', { name: 'Start Sketch' }) page.getByRole('button', { name: 'Start Sketch' })
).toBeVisible() ).toBeVisible()
}) })
test('Can select planes in Feature Tree after Start Sketch', async ({
page,
homePage,
toolbar,
editor,
}) => {
// Load the app with empty code
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`plane001 = offsetPlane(XZ, offset = 5)`
)
})
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await test.step('Click Start Sketch button', async () => {
await page.getByRole('button', { name: 'Start Sketch' }).click()
await expect(
page.getByRole('button', { name: 'Exit Sketch' })
).toBeVisible()
await expect(page.getByText('select a plane or face')).toBeVisible()
})
await test.step('Open feature tree and select Front plane (XZ)', async () => {
await toolbar.openFeatureTreePane()
await page.getByRole('button', { name: 'Front plane' }).click()
await page.waitForTimeout(600)
await expect(toolbar.lineBtn).toBeEnabled()
await editor.expectEditor.toContain('startSketchOn(XZ)')
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeVisible()
})
await test.step('Click Start Sketch button again', async () => {
await page.getByRole('button', { name: 'Start Sketch' }).click()
await expect(
page.getByRole('button', { name: 'Exit Sketch' })
).toBeVisible()
})
await test.step('Select the offset plane', async () => {
await toolbar.openFeatureTreePane()
await page.getByRole('button', { name: 'Offset plane' }).click()
await page.waitForTimeout(600)
await expect(toolbar.lineBtn).toBeEnabled()
await editor.expectEditor.toContain('startSketchOn(plane001)')
})
})
test('Can edit segments by dragging their handles', () => { test('Can edit segments by dragging their handles', () => {
const doEditSegmentsByDraggingHandle = async ( const doEditSegmentsByDraggingHandle = async (
page: Page, page: Page,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

View File

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

7
interface.d.ts vendored
View File

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

252
package-lock.json generated
View File

@ -43,6 +43,7 @@
"bson": "^6.10.3", "bson": "^6.10.3",
"chokidar": "^4.0.3", "chokidar": "^4.0.3",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"culori": "^4.0.2",
"decamelize": "^6.0.0", "decamelize": "^6.0.0",
"diff": "^7.0.0", "diff": "^7.0.0",
"electron-updater": "^6.6.2", "electron-updater": "^6.6.2",
@ -92,6 +93,7 @@
"@playwright/test": "^1.52.0", "@playwright/test": "^1.52.0",
"@testing-library/jest-dom": "^5.17.0", "@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^15.0.7", "@testing-library/react": "^15.0.7",
"@types/culori": "^4.0.0",
"@types/diff": "^7.0.2", "@types/diff": "^7.0.2",
"@types/electron": "^1.6.10", "@types/electron": "^1.6.10",
"@types/hammerjs": "^2.0.46", "@types/hammerjs": "^2.0.46",
@ -149,7 +151,7 @@
"ts-node": "^10.0.0", "ts-node": "^10.0.0",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"typescript-eslint": "^8.30.1", "typescript-eslint": "^8.30.1",
"vite": "^5.4.19", "vite": "^5.4.18",
"vite-plugin-package-version": "^1.1.0", "vite-plugin-package-version": "^1.1.0",
"vite-plugin-top-level-await": "^1.5.0", "vite-plugin-top-level-await": "^1.5.0",
"vite-tsconfig-paths": "^4.3.2", "vite-tsconfig-paths": "^4.3.2",
@ -3652,9 +3654,9 @@
} }
}, },
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz",
"integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", "integrity": "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@ -3668,9 +3670,9 @@
} }
}, },
"node_modules/@esbuild/android-arm": { "node_modules/@esbuild/android-arm": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.3.tgz",
"integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", "integrity": "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -3684,9 +3686,9 @@
} }
}, },
"node_modules/@esbuild/android-arm64": { "node_modules/@esbuild/android-arm64": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.3.tgz",
"integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", "integrity": "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -3700,9 +3702,9 @@
} }
}, },
"node_modules/@esbuild/android-x64": { "node_modules/@esbuild/android-x64": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.3.tgz",
"integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", "integrity": "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -3716,9 +3718,9 @@
} }
}, },
"node_modules/@esbuild/darwin-arm64": { "node_modules/@esbuild/darwin-arm64": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz",
"integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", "integrity": "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -3732,9 +3734,9 @@
} }
}, },
"node_modules/@esbuild/darwin-x64": { "node_modules/@esbuild/darwin-x64": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.3.tgz",
"integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", "integrity": "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -3748,9 +3750,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-arm64": { "node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.3.tgz",
"integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", "integrity": "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -3764,9 +3766,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-x64": { "node_modules/@esbuild/freebsd-x64": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.3.tgz",
"integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", "integrity": "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -3780,9 +3782,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm": { "node_modules/@esbuild/linux-arm": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.3.tgz",
"integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", "integrity": "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -3796,9 +3798,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm64": { "node_modules/@esbuild/linux-arm64": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.3.tgz",
"integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", "integrity": "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -3812,9 +3814,9 @@
} }
}, },
"node_modules/@esbuild/linux-ia32": { "node_modules/@esbuild/linux-ia32": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.3.tgz",
"integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", "integrity": "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -3828,9 +3830,9 @@
} }
}, },
"node_modules/@esbuild/linux-loong64": { "node_modules/@esbuild/linux-loong64": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.3.tgz",
"integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", "integrity": "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@ -3844,9 +3846,9 @@
} }
}, },
"node_modules/@esbuild/linux-mips64el": { "node_modules/@esbuild/linux-mips64el": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.3.tgz",
"integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", "integrity": "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==",
"cpu": [ "cpu": [
"mips64el" "mips64el"
], ],
@ -3860,9 +3862,9 @@
} }
}, },
"node_modules/@esbuild/linux-ppc64": { "node_modules/@esbuild/linux-ppc64": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.3.tgz",
"integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", "integrity": "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@ -3876,9 +3878,9 @@
} }
}, },
"node_modules/@esbuild/linux-riscv64": { "node_modules/@esbuild/linux-riscv64": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.3.tgz",
"integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", "integrity": "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@ -3892,9 +3894,9 @@
} }
}, },
"node_modules/@esbuild/linux-s390x": { "node_modules/@esbuild/linux-s390x": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.3.tgz",
"integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", "integrity": "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@ -3908,9 +3910,9 @@
} }
}, },
"node_modules/@esbuild/linux-x64": { "node_modules/@esbuild/linux-x64": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.3.tgz",
"integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", "integrity": "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -3924,9 +3926,9 @@
} }
}, },
"node_modules/@esbuild/netbsd-arm64": { "node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.3.tgz",
"integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", "integrity": "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -3940,9 +3942,9 @@
} }
}, },
"node_modules/@esbuild/netbsd-x64": { "node_modules/@esbuild/netbsd-x64": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.3.tgz",
"integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", "integrity": "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -3956,9 +3958,9 @@
} }
}, },
"node_modules/@esbuild/openbsd-arm64": { "node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.3.tgz",
"integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", "integrity": "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -3972,9 +3974,9 @@
} }
}, },
"node_modules/@esbuild/openbsd-x64": { "node_modules/@esbuild/openbsd-x64": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.3.tgz",
"integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", "integrity": "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -3988,9 +3990,9 @@
} }
}, },
"node_modules/@esbuild/sunos-x64": { "node_modules/@esbuild/sunos-x64": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.3.tgz",
"integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", "integrity": "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -4004,9 +4006,9 @@
} }
}, },
"node_modules/@esbuild/win32-arm64": { "node_modules/@esbuild/win32-arm64": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.3.tgz",
"integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", "integrity": "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -4020,9 +4022,9 @@
} }
}, },
"node_modules/@esbuild/win32-ia32": { "node_modules/@esbuild/win32-ia32": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.3.tgz",
"integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", "integrity": "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -4036,9 +4038,9 @@
} }
}, },
"node_modules/@esbuild/win32-x64": { "node_modules/@esbuild/win32-x64": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.3.tgz",
"integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", "integrity": "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -7392,6 +7394,13 @@
"integrity": "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==", "integrity": "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/culori": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/culori/-/culori-4.0.0.tgz",
"integrity": "sha512-aFljQwjb++sl6TAyEXeHTiK/fk9epZOQ+nMmadjnAvzZFIvNoQ0x8XQYfcOaRTBwmDUPUlghhZCJ66MTcqQAsg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/debug": { "node_modules/@types/debug": {
"version": "4.1.12", "version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@ -11823,6 +11832,15 @@
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/culori": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/culori/-/culori-4.0.2.tgz",
"integrity": "sha512-1+BhOB8ahCn4O0cep0Sh2l9KCOfOdY+BXJnKMHFFzDEouSr/el18QwXEMRlOj9UY5nCeA8UN3a/82rUWRBeyBw==",
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
"node_modules/damerau-levenshtein": { "node_modules/damerau-levenshtein": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@ -13219,9 +13237,9 @@
"optional": true "optional": true
}, },
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.25.4", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz",
"integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", "integrity": "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"bin": { "bin": {
@ -13231,31 +13249,31 @@
"node": ">=18" "node": ">=18"
}, },
"optionalDependencies": { "optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.4", "@esbuild/aix-ppc64": "0.25.3",
"@esbuild/android-arm": "0.25.4", "@esbuild/android-arm": "0.25.3",
"@esbuild/android-arm64": "0.25.4", "@esbuild/android-arm64": "0.25.3",
"@esbuild/android-x64": "0.25.4", "@esbuild/android-x64": "0.25.3",
"@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-arm64": "0.25.3",
"@esbuild/darwin-x64": "0.25.4", "@esbuild/darwin-x64": "0.25.3",
"@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.3",
"@esbuild/freebsd-x64": "0.25.4", "@esbuild/freebsd-x64": "0.25.3",
"@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm": "0.25.3",
"@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-arm64": "0.25.3",
"@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-ia32": "0.25.3",
"@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-loong64": "0.25.3",
"@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-mips64el": "0.25.3",
"@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-ppc64": "0.25.3",
"@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-riscv64": "0.25.3",
"@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-s390x": "0.25.3",
"@esbuild/linux-x64": "0.25.4", "@esbuild/linux-x64": "0.25.3",
"@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.3",
"@esbuild/netbsd-x64": "0.25.4", "@esbuild/netbsd-x64": "0.25.3",
"@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.3",
"@esbuild/openbsd-x64": "0.25.4", "@esbuild/openbsd-x64": "0.25.3",
"@esbuild/sunos-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.3",
"@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-arm64": "0.25.3",
"@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-ia32": "0.25.3",
"@esbuild/win32-x64": "0.25.4" "@esbuild/win32-x64": "0.25.3"
} }
}, },
"node_modules/escalade": { "node_modules/escalade": {
@ -25132,10 +25150,11 @@
"optional": true "optional": true
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "5.4.19", "version": "5.4.18",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.18.tgz",
"integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", "integrity": "sha512-1oDcnEp3lVyHCuQ2YFelM4Alm2o91xNoMncRm1U7S+JdYfYOvbiGZ3/CxGttrOu2M/KcGz7cRC2DoNUA6urmMA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "^0.21.3", "esbuild": "^0.21.3",
"postcss": "^8.4.43", "postcss": "^8.4.43",
@ -26639,25 +26658,10 @@
"vscode-uri": "^3.1.0" "vscode-uri": "^3.1.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^24.0.7", "@types/node": "^22.14.1",
"ts-node": "^10.9.2" "ts-node": "^10.9.2"
} }
}, },
"packages/codemirror-lsp-client/node_modules/@types/node": {
"version": "24.0.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.10.tgz",
"integrity": "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==",
"dev": true,
"dependencies": {
"undici-types": "~7.8.0"
}
},
"packages/codemirror-lsp-client/node_modules/undici-types": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
"integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
"dev": true
},
"rust/kcl-language-server": { "rust/kcl-language-server": {
"version": "0.0.0", "version": "0.0.0",
"license": "MIT", "license": "MIT",
@ -26675,7 +26679,7 @@
"@vscode/test-electron": "^2.4.1", "@vscode/test-electron": "^2.4.1",
"@vscode/vsce": "^3.3.2", "@vscode/vsce": "^3.3.2",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"esbuild": "^0.25.4", "esbuild": "^0.25.3",
"glob": "^11.0.1", "glob": "^11.0.1",
"mocha": "^11.1.0", "mocha": "^11.1.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"

View File

@ -45,6 +45,7 @@
"bson": "^6.10.3", "bson": "^6.10.3",
"chokidar": "^4.0.3", "chokidar": "^4.0.3",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"culori": "^4.0.2",
"decamelize": "^6.0.0", "decamelize": "^6.0.0",
"diff": "^7.0.0", "diff": "^7.0.0",
"electron-updater": "^6.6.2", "electron-updater": "^6.6.2",
@ -170,6 +171,7 @@
"@playwright/test": "^1.52.0", "@playwright/test": "^1.52.0",
"@testing-library/jest-dom": "^5.17.0", "@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^15.0.7", "@testing-library/react": "^15.0.7",
"@types/culori": "^4.0.0",
"@types/diff": "^7.0.2", "@types/diff": "^7.0.2",
"@types/electron": "^1.6.10", "@types/electron": "^1.6.10",
"@types/hammerjs": "^2.0.46", "@types/hammerjs": "^2.0.46",
@ -227,7 +229,7 @@
"ts-node": "^10.0.0", "ts-node": "^10.0.0",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"typescript-eslint": "^8.30.1", "typescript-eslint": "^8.30.1",
"vite": "^5.4.19", "vite": "^5.4.18",
"vite-plugin-package-version": "^1.1.0", "vite-plugin-package-version": "^1.1.0",
"vite-plugin-top-level-await": "^1.5.0", "vite-plugin-top-level-await": "^1.5.0",
"vite-tsconfig-paths": "^4.3.2", "vite-tsconfig-paths": "^4.3.2",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -324,6 +324,8 @@ export class SceneEntities {
group: segment, group: segment,
scale: factor, scale: factor,
sceneInfra: this.sceneInfra, sceneInfra: this.sceneInfra,
// Note: AST and code not available in onCamChange, so constraints won't be checked here
// This is primarily for scaling changes
}) })
callBack && !err(callBack) && callbacks.push(callBack) callBack && !err(callBack) && callbacks.push(callBack)
if (segment.name === PROFILE_START) { if (segment.name === PROFILE_START) {
@ -729,6 +731,8 @@ export class SceneEntities {
scale, scale,
theme: this.sceneInfra._theme, theme: this.sceneInfra._theme,
isDraft: false, isDraft: false,
ast: maybeModdedAst,
code: this.codeManager.code,
}) })
_profileStart.layers.set(SKETCH_LAYER) _profileStart.layers.set(SKETCH_LAYER)
_profileStart.traverse((child) => { _profileStart.traverse((child) => {
@ -866,6 +870,8 @@ export class SceneEntities {
isSelected, isSelected,
sceneInfra: this.sceneInfra, sceneInfra: this.sceneInfra,
selection, selection,
ast: maybeModdedAst,
code: this.codeManager.code,
}) })
if (err(result)) return if (err(result)) return
const { group: _group, updateOverlaysCallback } = result const { group: _group, updateOverlaysCallback } = result
@ -3252,6 +3258,8 @@ export class SceneEntities {
scale: factor, scale: factor,
prevSegment: sgPaths[index - 1], prevSegment: sgPaths[index - 1],
sceneInfra: this.sceneInfra, sceneInfra: this.sceneInfra,
ast: modifiedAst,
code: this.codeManager.code,
}) })
if (callBack && !err(callBack)) return callBack if (callBack && !err(callBack)) return callBack

View File

@ -76,12 +76,17 @@ import {
} from '@src/clientSideScene/sceneUtils' } from '@src/clientSideScene/sceneUtils'
import { angleLengthInfo } from '@src/components/Toolbar/angleLengthInfo' import { angleLengthInfo } from '@src/components/Toolbar/angleLengthInfo'
import type { Coords2d } from '@src/lang/std/sketch' import type { Coords2d } from '@src/lang/std/sketch'
import { getConstraintInfoKw } from '@src/lang/std/sketch'
import type { SegmentInputs } from '@src/lang/std/stdTypes' import type { SegmentInputs } from '@src/lang/std/stdTypes'
import type { PathToNode } from '@src/lang/wasm' import type { PathToNode, Program } from '@src/lang/wasm'
import { getTangentialArcToInfo } from '@src/lang/wasm' import { getTangentialArcToInfo } from '@src/lang/wasm'
import { getNodeFromPath } from '@src/lang/queryAst'
import type { Selections } from '@src/lib/selections' import type { Selections } from '@src/lib/selections'
import type { Themes } from '@src/lib/theme' import type { Themes } from '@src/lib/theme'
import { getThemeColorForThreeJs } from '@src/lib/theme' import {
getThemeColorForThreeJs,
getPrimaryColorForThreeJs,
} from '@src/lib/theme'
import { err } from '@src/lib/trap' import { err } from '@src/lib/trap'
import { isClockwise, normaliseAngle, roundOff } from '@src/lib/utils' import { isClockwise, normaliseAngle, roundOff } from '@src/lib/utils'
import { getTangentPointFromPreviousArc } from '@src/lib/utils2d' import { getTangentPointFromPreviousArc } from '@src/lib/utils2d'
@ -95,6 +100,7 @@ import toast from 'react-hot-toast'
import { ARG_INTERIOR_ABSOLUTE } from '@src/lang/constants' import { ARG_INTERIOR_ABSOLUTE } from '@src/lang/constants'
const ANGLE_INDICATOR_RADIUS = 30 // in px const ANGLE_INDICATOR_RADIUS = 30 // in px
interface CreateSegmentArgs { interface CreateSegmentArgs {
input: SegmentInputs input: SegmentInputs
prevSegment: Sketch['paths'][number] prevSegment: Sketch['paths'][number]
@ -108,6 +114,9 @@ interface CreateSegmentArgs {
isSelected?: boolean isSelected?: boolean
sceneInfra: SceneInfra sceneInfra: SceneInfra
selection?: Selections selection?: Selections
// Add optional AST and code for constraint checking
ast?: Program
code?: string
} }
interface UpdateSegmentArgs { interface UpdateSegmentArgs {
@ -116,6 +125,9 @@ interface UpdateSegmentArgs {
group: Group group: Group
sceneInfra: SceneInfra sceneInfra: SceneInfra
scale?: number scale?: number
// Add optional AST and code for constraint checking
ast?: Program
code?: string
} }
interface CreateSegmentResult { interface CreateSegmentResult {
@ -144,6 +156,55 @@ export interface SegmentUtils {
) => CreateSegmentResult['updateOverlaysCallback'] | Error ) => CreateSegmentResult['updateOverlaysCallback'] | Error
} }
/**
* Checks if a segment is fully constrained by examining all its constraint info
*/
function isSegmentFullyConstrained(
pathToNode: PathToNode,
ast: Program,
code: string
): boolean {
try {
const nodeMeta = getNodeFromPath<any>(ast, pathToNode)
if (err(nodeMeta) || nodeMeta.node.type !== 'CallExpressionKw') {
return false
}
const constraintInfos = getConstraintInfoKw(nodeMeta.node, code, pathToNode)
// If there are no constraints, consider it not fully constrained
if (constraintInfos.length === 0) {
return false
}
// Check if all constraints are constrained
return constraintInfos.every((info) => info.isConstrained)
} catch (error) {
console.warn('Error checking segment constraints:', error)
return false
}
}
/**
* Gets the appropriate color for a segment based on selection, constraints, and theme
*/
function getSegmentColor({
theme,
isSelected,
callExpName = '',
isFullyConstrained = false,
}: {
theme: Themes
isSelected: boolean
callExpName?: string
isFullyConstrained: boolean
}): number {
if (isSelected) return 0x0000ff // Blue for selected
if (callExpName === 'close') return 0x444444 // Gray for close segments
if (!isFullyConstrained) return getPrimaryColorForThreeJs() // Primary color for unconstrained segments
return getThemeColorForThreeJs(theme) // Default theme color for constrained segments
}
class StraightSegment implements SegmentUtils { class StraightSegment implements SegmentUtils {
init: SegmentUtils['init'] = ({ init: SegmentUtils['init'] = ({
input, input,
@ -158,13 +219,32 @@ class StraightSegment implements SegmentUtils {
sceneInfra, sceneInfra,
prevSegment, prevSegment,
selection, selection,
ast,
code,
}) => { }) => {
if (input.type !== 'straight-segment') if (input.type !== 'straight-segment')
return new Error('Invalid segment type') return new Error('Invalid segment type')
const { from, to } = input const { from, to } = input
const baseColor =
callExpName === 'close' ? 0x444444 : getThemeColorForThreeJs(theme) // Check if segment is fully constrained (only if we have AST and code)
const color = isSelected ? 0x0000ff : baseColor const isFullyConstrained =
callExpName === 'close'
? true
: ast && code
? isSegmentFullyConstrained(pathToNode, ast, code)
: false
const color = getSegmentColor({
theme,
isSelected: !!isSelected,
callExpName,
isFullyConstrained,
})
const baseColor = !isFullyConstrained
? getPrimaryColorForThreeJs()
: callExpName === 'close'
? 0x444444
: getThemeColorForThreeJs(theme)
const meshType = isDraftSegment const meshType = isDraftSegment
? STRAIGHT_SEGMENT_DASH ? STRAIGHT_SEGMENT_DASH
: STRAIGHT_SEGMENT_BODY : STRAIGHT_SEGMENT_BODY
@ -250,12 +330,40 @@ class StraightSegment implements SegmentUtils {
group, group,
scale = 1, scale = 1,
sceneInfra, sceneInfra,
ast,
code,
}) => { }) => {
if (input.type !== 'straight-segment') if (input.type !== 'straight-segment')
return new Error('Invalid segment type') return new Error('Invalid segment type')
const { from, to } = input const { from, to } = input
group.userData.from = from group.userData.from = from
group.userData.to = to group.userData.to = to
// Check if segment is fully constrained and update color if needed
if (ast && code) {
const pathToNode = group.userData.pathToNode
const isFullyConstrained =
group.userData.callExpName === 'close'
? true
: isSegmentFullyConstrained(pathToNode, ast, code)
const color = getSegmentColor({
theme: sceneInfra._theme,
isSelected: group.userData.isSelected,
callExpName: group.userData.callExpName,
isFullyConstrained,
})
// Update the material color
const straightSegmentBody = group.children.find(
(child) => child.userData.type === STRAIGHT_SEGMENT_BODY
) as Mesh
if (
straightSegmentBody &&
straightSegmentBody.material instanceof MeshBasicMaterial
) {
straightSegmentBody.material.color.set(color)
}
}
const shape = createLineShape(scale) const shape = createLineShape(scale)
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
const labelGroup = group.getObjectByName(SEGMENT_LENGTH_LABEL) as Group const labelGroup = group.getObjectByName(SEGMENT_LENGTH_LABEL) as Group
@ -391,6 +499,8 @@ class TangentialArcToSegment implements SegmentUtils {
theme, theme,
isSelected, isSelected,
sceneInfra, sceneInfra,
ast,
code,
}) => { }) => {
if (input.type !== 'straight-segment') if (input.type !== 'straight-segment')
return new Error('Invalid segment type') return new Error('Invalid segment type')
@ -409,8 +519,19 @@ class TangentialArcToSegment implements SegmentUtils {
isDashed: isDraftSegment, isDashed: isDraftSegment,
scale, scale,
}) })
const baseColor = getThemeColorForThreeJs(theme)
const color = isSelected ? 0x0000ff : baseColor // Check if segment is fully constrained (only if we have AST and code)
const isFullyConstrained =
ast && code ? isSegmentFullyConstrained(pathToNode, ast, code) : false
const color = getSegmentColor({
theme,
isSelected: !!isSelected,
isFullyConstrained,
})
const baseColor = !isFullyConstrained
? getPrimaryColorForThreeJs()
: getThemeColorForThreeJs(theme)
const body = new MeshBasicMaterial({ color }) const body = new MeshBasicMaterial({ color })
const mesh = new Mesh(geometry, body) const mesh = new Mesh(geometry, body)
const arrowGroup = createArrowhead(scale, theme, color) const arrowGroup = createArrowhead(scale, theme, color)
@ -453,6 +574,8 @@ class TangentialArcToSegment implements SegmentUtils {
group, group,
scale = 1, scale = 1,
sceneInfra, sceneInfra,
ast,
code,
}) => { }) => {
if (input.type !== 'straight-segment') if (input.type !== 'straight-segment')
return new Error('Invalid segment type') return new Error('Invalid segment type')
@ -460,6 +583,32 @@ class TangentialArcToSegment implements SegmentUtils {
group.userData.from = from group.userData.from = from
group.userData.to = to group.userData.to = to
group.userData.prevSegment = prevSegment group.userData.prevSegment = prevSegment
// Check if segment is fully constrained and update color if needed
if (ast && code) {
const pathToNode = group.userData.pathToNode
const isFullyConstrained = isSegmentFullyConstrained(
pathToNode,
ast,
code
)
const color = getSegmentColor({
theme: sceneInfra._theme,
isSelected: group.userData.isSelected,
isFullyConstrained,
})
// Update the material color
const tangentialArcSegmentBody = group.children.find(
(child) => child.userData.type === TANGENTIAL_ARC_TO_SEGMENT_BODY
) as Mesh
if (
tangentialArcSegmentBody &&
tangentialArcSegmentBody.material instanceof MeshBasicMaterial
) {
tangentialArcSegmentBody.material.color.set(color)
}
}
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
const extraSegmentGroup = group.getObjectByName(EXTRA_SEGMENT_HANDLE) const extraSegmentGroup = group.getObjectByName(EXTRA_SEGMENT_HANDLE)
@ -588,13 +737,26 @@ class CircleSegment implements SegmentUtils {
theme, theme,
isSelected, isSelected,
sceneInfra, sceneInfra,
ast,
code,
}) => { }) => {
if (input.type !== 'arc-segment') { if (input.type !== 'arc-segment') {
return new Error('Invalid segment type') return new Error('Invalid segment type')
} }
const { from, center, radius } = input const { from, center, radius } = input
const baseColor = getThemeColorForThreeJs(theme)
const color = isSelected ? 0x0000ff : baseColor // Check if segment is fully constrained
const isFullyConstrained =
ast && code ? isSegmentFullyConstrained(pathToNode, ast, code) : false
const color = getSegmentColor({
theme,
isSelected: !!isSelected,
isFullyConstrained,
})
const baseColor = !isFullyConstrained
? getPrimaryColorForThreeJs()
: getThemeColorForThreeJs(theme)
const group = new Group() const group = new Group()
const geometry = createArcGeometry({ const geometry = createArcGeometry({
@ -678,6 +840,8 @@ class CircleSegment implements SegmentUtils {
group, group,
scale = 1, scale = 1,
sceneInfra, sceneInfra,
ast,
code,
}) => { }) => {
if (input.type !== 'arc-segment') { if (input.type !== 'arc-segment') {
return new Error('Invalid segment type') return new Error('Invalid segment type')
@ -687,6 +851,32 @@ class CircleSegment implements SegmentUtils {
group.userData.center = center group.userData.center = center
group.userData.radius = radius group.userData.radius = radius
group.userData.prevSegment = prevSegment group.userData.prevSegment = prevSegment
// Check if segment is fully constrained and update color if needed
if (ast && code) {
const pathToNode = group.userData.pathToNode
const isFullyConstrained = isSegmentFullyConstrained(
pathToNode,
ast,
code
)
const color = getSegmentColor({
theme: sceneInfra._theme,
isSelected: group.userData.isSelected,
isFullyConstrained,
})
// Update the material color
const circleSegmentBody = group.children.find(
(child) => child.userData.type === CIRCLE_SEGMENT_BODY
) as Mesh
if (
circleSegmentBody &&
circleSegmentBody.material instanceof MeshBasicMaterial
) {
circleSegmentBody.material.color.set(color)
}
}
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
const radiusLengthIndicator = group.getObjectByName( const radiusLengthIndicator = group.getObjectByName(
SEGMENT_LENGTH_LABEL SEGMENT_LENGTH_LABEL
@ -831,6 +1021,8 @@ class CircleThreePointSegment implements SegmentUtils {
isSelected = false, isSelected = false,
sceneInfra, sceneInfra,
prevSegment, prevSegment,
ast,
code,
}) => { }) => {
if (input.type !== 'circle-three-point-segment') { if (input.type !== 'circle-three-point-segment') {
return new Error('Invalid segment type') return new Error('Invalid segment type')
@ -845,8 +1037,19 @@ class CircleThreePointSegment implements SegmentUtils {
p3[1] p3[1]
) )
const center: [number, number] = [center_x, center_y] const center: [number, number] = [center_x, center_y]
const baseColor = getThemeColorForThreeJs(theme)
const color = isSelected ? 0x0000ff : baseColor // Check if segment is fully constrained
const isFullyConstrained =
ast && code ? isSegmentFullyConstrained(pathToNode, ast, code) : false
const color = getSegmentColor({
theme,
isSelected: !!isSelected,
isFullyConstrained,
})
const baseColor = !isFullyConstrained
? getPrimaryColorForThreeJs()
: getThemeColorForThreeJs(theme)
const group = new Group() const group = new Group()
const geometry = createArcGeometry({ const geometry = createArcGeometry({
@ -919,6 +1122,8 @@ class CircleThreePointSegment implements SegmentUtils {
group, group,
scale = 1, scale = 1,
sceneInfra, sceneInfra,
ast,
code,
}) => { }) => {
if (input.type !== 'circle-three-point-segment') { if (input.type !== 'circle-three-point-segment') {
return new Error('Invalid segment type') return new Error('Invalid segment type')
@ -927,6 +1132,32 @@ class CircleThreePointSegment implements SegmentUtils {
group.userData.p1 = p1 group.userData.p1 = p1
group.userData.p2 = p2 group.userData.p2 = p2
group.userData.p3 = p3 group.userData.p3 = p3
// Check if segment is fully constrained and update color if needed
if (ast && code) {
const pathToNode = group.userData.pathToNode
const isFullyConstrained = isSegmentFullyConstrained(
pathToNode,
ast,
code
)
const color = getSegmentColor({
theme: sceneInfra._theme,
isSelected: group.userData.isSelected,
isFullyConstrained,
})
// Update the material color
const circleSegmentBody = group.children.find(
(child) => child.userData.type === CIRCLE_THREE_POINT_SEGMENT_BODY
) as Mesh
if (
circleSegmentBody &&
circleSegmentBody.material instanceof MeshBasicMaterial
) {
circleSegmentBody.material.color.set(color)
}
}
const { center_x, center_y, radius } = calculate_circle_from_3_points( const { center_x, center_y, radius } = calculate_circle_from_3_points(
p1[0], p1[0],
p1[1], p1[1],
@ -1048,13 +1279,26 @@ class ArcSegment implements SegmentUtils {
theme, theme,
isSelected, isSelected,
sceneInfra, sceneInfra,
ast,
code,
}) => { }) => {
if (input.type !== 'arc-segment') { if (input.type !== 'arc-segment') {
return new Error('Invalid segment type') return new Error('Invalid segment type')
} }
const { from, to, center, radius, ccw } = input const { from, to, center, radius, ccw } = input
const baseColor = getThemeColorForThreeJs(theme)
const color = isSelected ? 0x0000ff : baseColor // Check if segment is fully constrained
const isFullyConstrained =
ast && code ? isSegmentFullyConstrained(pathToNode, ast, code) : false
const color = getSegmentColor({
theme,
isSelected: !!isSelected,
isFullyConstrained,
})
const baseColor = !isFullyConstrained
? getPrimaryColorForThreeJs()
: getThemeColorForThreeJs(theme)
// Calculate start and end angles // Calculate start and end angles
const startAngle = Math.atan2(from[1] - center[1], from[0] - center[0]) const startAngle = Math.atan2(from[1] - center[1], from[0] - center[0])
@ -1195,6 +1439,8 @@ class ArcSegment implements SegmentUtils {
group, group,
scale = 1, scale = 1,
sceneInfra, sceneInfra,
ast,
code,
}) => { }) => {
if (input.type !== 'arc-segment') { if (input.type !== 'arc-segment') {
return new Error('Invalid segment type') return new Error('Invalid segment type')
@ -1207,6 +1453,32 @@ class ArcSegment implements SegmentUtils {
group.userData.ccw = ccw group.userData.ccw = ccw
group.userData.prevSegment = prevSegment group.userData.prevSegment = prevSegment
// Check if segment is fully constrained and update color if needed
if (ast && code) {
const pathToNode = group.userData.pathToNode
const isFullyConstrained = isSegmentFullyConstrained(
pathToNode,
ast,
code
)
const color = getSegmentColor({
theme: sceneInfra._theme,
isSelected: group.userData.isSelected,
isFullyConstrained,
})
// Update the material color
const arcSegmentBody = group.children.find(
(child) => child.userData.type === ARC_SEGMENT_BODY
) as Mesh
if (
arcSegmentBody &&
arcSegmentBody.material instanceof MeshBasicMaterial
) {
arcSegmentBody.material.color.set(color)
}
}
// Calculate start and end angles // Calculate start and end angles
const startAngle = Math.atan2(from[1] - center[1], from[0] - center[0]) const startAngle = Math.atan2(from[1] - center[1], from[0] - center[0])
const endAngle = Math.atan2(to[1] - center[1], to[0] - center[0]) const endAngle = Math.atan2(to[1] - center[1], to[0] - center[0])
@ -1403,6 +1675,8 @@ class ThreePointArcSegment implements SegmentUtils {
isSelected = false, isSelected = false,
sceneInfra, sceneInfra,
prevSegment, prevSegment,
ast,
code,
}) => { }) => {
if (input.type !== 'circle-three-point-segment') { if (input.type !== 'circle-three-point-segment') {
return new Error('Invalid segment type') return new Error('Invalid segment type')
@ -1417,8 +1691,19 @@ class ThreePointArcSegment implements SegmentUtils {
p3[1] p3[1]
) )
const center: [number, number] = [center_x, center_y] const center: [number, number] = [center_x, center_y]
const baseColor = getThemeColorForThreeJs(theme)
const color = isSelected ? 0x0000ff : baseColor // Check if segment is fully constrained
const isFullyConstrained =
ast && code ? isSegmentFullyConstrained(pathToNode, ast, code) : false
const color = getSegmentColor({
theme,
isSelected: !!isSelected,
isFullyConstrained,
})
const baseColor = !isFullyConstrained
? getPrimaryColorForThreeJs()
: getThemeColorForThreeJs(theme)
// Calculate start and end angles // Calculate start and end angles
const startAngle = Math.atan2(p1[1] - center[1], p1[0] - center[0]) const startAngle = Math.atan2(p1[1] - center[1], p1[0] - center[0])
@ -1500,6 +1785,8 @@ class ThreePointArcSegment implements SegmentUtils {
group, group,
scale = 1, scale = 1,
sceneInfra, sceneInfra,
ast,
code,
}) => { }) => {
if (input.type !== 'circle-three-point-segment') { if (input.type !== 'circle-three-point-segment') {
return new Error('Invalid segment type') return new Error('Invalid segment type')
@ -1523,6 +1810,32 @@ class ThreePointArcSegment implements SegmentUtils {
group.userData.radius = radius group.userData.radius = radius
group.userData.prevSegment = prevSegment group.userData.prevSegment = prevSegment
// Check if segment is fully constrained and update color if needed
if (ast && code) {
const pathToNode = group.userData.pathToNode
const isFullyConstrained = isSegmentFullyConstrained(
pathToNode,
ast,
code
)
const color = getSegmentColor({
theme: sceneInfra._theme,
isSelected: group.userData.isSelected,
isFullyConstrained,
})
// Update the material color
const arcSegmentBody = group.children.find(
(child) => child.userData.type === THREE_POINT_ARC_SEGMENT_BODY
) as Mesh
if (
arcSegmentBody &&
arcSegmentBody.material instanceof MeshBasicMaterial
) {
arcSegmentBody.material.color.set(color)
}
}
// Calculate start and end angles // Calculate start and end angles
const startAngle = Math.atan2(p1[1] - center[1], p1[0] - center[0]) const startAngle = Math.atan2(p1[1] - center[1], p1[0] - center[0])
const endAngle = Math.atan2(p3[1] - center[1], p3[0] - center[0]) const endAngle = Math.atan2(p3[1] - center[1], p3[0] - center[0])
@ -1619,6 +1932,8 @@ export function createProfileStartHandle({
theme, theme,
isSelected, isSelected,
size = 12, size = 12,
ast,
code,
...rest ...rest
}: { }: {
from: Coords2d from: Coords2d
@ -1626,15 +1941,30 @@ export function createProfileStartHandle({
theme: Themes theme: Themes
isSelected?: boolean isSelected?: boolean
size?: number size?: number
ast?: Program
code?: string
} & ( } & (
| { isDraft: true } | { isDraft: true }
| { isDraft: false; id: string; pathToNode: PathToNode } | { isDraft: false; id: string; pathToNode: PathToNode }
)) { )) {
const group = new Group() const group = new Group()
// Check if profile start is fully constrained (only if we have AST, code, and it's not a draft)
const isFullyConstrained =
!isDraft && ast && code && 'pathToNode' in rest
? isSegmentFullyConstrained(rest.pathToNode, ast, code)
: false
const geometry = new BoxGeometry(size, size, size) // in pixels scaled later const geometry = new BoxGeometry(size, size, size) // in pixels scaled later
const baseColor = getThemeColorForThreeJs(theme) const color = getSegmentColor({
const color = isSelected ? 0x0000ff : baseColor theme,
isSelected: !!isSelected,
callExpName: 'profileStart',
isFullyConstrained,
})
const baseColor = !isFullyConstrained
? getPrimaryColorForThreeJs()
: getThemeColorForThreeJs(theme)
const body = new MeshBasicMaterial({ color }) const body = new MeshBasicMaterial({ color })
const mesh = new Mesh(geometry, body) const mesh = new Mesh(geometry, body)
@ -1645,6 +1975,7 @@ export function createProfileStartHandle({
from, from,
isSelected, isSelected,
baseColor, baseColor,
isFullyConstrained,
...rest, ...rest,
} }
group.name = isDraft ? DRAFT_POINT : PROFILE_START group.name = isDraft ? DRAFT_POINT : PROFILE_START

View File

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

View File

@ -24,12 +24,7 @@ import {
getOperationVariableName, getOperationVariableName,
stdLibMap, stdLibMap,
} from '@src/lib/operations' } from '@src/lib/operations'
import { import { editorManager, kclManager, rustContext } from '@src/lib/singletons'
editorManager,
kclManager,
rustContext,
sceneInfra,
} from '@src/lib/singletons'
import { import {
featureTreeMachine, featureTreeMachine,
featureTreeMachineDefaultContext, featureTreeMachineDefaultContext,
@ -39,20 +34,11 @@ import {
kclEditorActor, kclEditorActor,
selectionEventSelector, selectionEventSelector,
} from '@src/machines/kclEditorMachine' } from '@src/machines/kclEditorMachine'
import type { Plane } from '@rust/kcl-lib/bindings/Artifact'
import {
selectDefaultSketchPlane,
selectOffsetSketchPlane,
} from '@src/lib/selections'
import type { DefaultPlaneStr } from '@src/lib/planes'
export const FeatureTreePane = () => { export const FeatureTreePane = () => {
const isEditorMounted = useSelector(kclEditorActor, editorIsMountedSelector) const isEditorMounted = useSelector(kclEditorActor, editorIsMountedSelector)
const lastSelectionEvent = useSelector(kclEditorActor, selectionEventSelector) const lastSelectionEvent = useSelector(kclEditorActor, selectionEventSelector)
const { send: modelingSend, state: modelingState } = useModelingContext() const { send: modelingSend, state: modelingState } = useModelingContext()
const sketchNoFace = modelingState.matches('Sketch no face')
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_featureTreeState, featureTreeSend] = useMachine( const [_featureTreeState, featureTreeSend] = useMachine(
featureTreeMachine.provide({ featureTreeMachine.provide({
@ -209,7 +195,6 @@ export const FeatureTreePane = () => {
key={key} key={key}
item={operation} item={operation}
send={featureTreeSend} send={featureTreeSend}
sketchNoFace={sketchNoFace}
/> />
) )
})} })}
@ -266,7 +251,6 @@ const OperationItemWrapper = ({
customSuffix, customSuffix,
className, className,
selectable = true, selectable = true,
greyedOut = false,
...props ...props
}: React.HTMLAttributes<HTMLButtonElement> & { }: React.HTMLAttributes<HTMLButtonElement> & {
icon: CustomIconName icon: CustomIconName
@ -278,19 +262,18 @@ const OperationItemWrapper = ({
menuItems?: ComponentProps<typeof ContextMenu>['items'] menuItems?: ComponentProps<typeof ContextMenu>['items']
errors?: Diagnostic[] errors?: Diagnostic[]
selectable?: boolean selectable?: boolean
greyedOut?: boolean
}) => { }) => {
const menuRef = useRef<HTMLDivElement>(null) const menuRef = useRef<HTMLDivElement>(null)
return ( return (
<div <div
ref={menuRef} ref={menuRef}
className={`flex select-none items-center group/item my-0 py-0.5 px-1 ${selectable ? 'focus-within:bg-primary/10 hover:bg-primary/5' : ''} ${greyedOut ? 'opacity-50 cursor-not-allowed' : ''}`} className={`flex select-none items-center group/item my-0 py-0.5 px-1 ${selectable ? 'focus-within:bg-primary/10 hover:bg-primary/5' : ''}`}
data-testid="feature-tree-operation-item" data-testid="feature-tree-operation-item"
> >
<button <button
{...props} {...props}
className={`reset !py-0.5 !px-1 flex-1 flex items-center gap-2 text-left text-base ${selectable ? 'border-transparent dark:border-transparent' : '!border-transparent cursor-default'} ${className}`} className={`reset !py-0.5 !px-1 flex-1 flex items-center gap-2 text-left text-base ${selectable ? 'border-transparent dark:border-transparent' : 'border-none cursor-default'} ${className}`}
> >
<CustomIcon name={icon} className="w-5 h-5 block" /> <CustomIcon name={icon} className="w-5 h-5 block" />
<div className="flex flex-1 items-baseline align-baseline"> <div className="flex flex-1 items-baseline align-baseline">
@ -328,7 +311,6 @@ const OperationItemWrapper = ({
const OperationItem = (props: { const OperationItem = (props: {
item: Operation item: Operation
send: Prop<Actor<typeof featureTreeMachine>, 'send'> send: Prop<Actor<typeof featureTreeMachine>, 'send'>
sketchNoFace: boolean
}) => { }) => {
const kclContext = useKclContext() const kclContext = useKclContext()
const name = getOperationLabel(props.item) const name = getOperationLabel(props.item)
@ -361,22 +343,15 @@ const OperationItem = (props: {
}, [kclContext.diagnostics.length]) }, [kclContext.diagnostics.length])
function selectOperation() { function selectOperation() {
if (props.sketchNoFace) { if (props.item.type === 'GroupEnd') {
if (isOffsetPlane(props.item)) { return
const artifact = findOperationArtifact(props.item)
void selectOffsetSketchPlane(artifact)
}
} else {
if (props.item.type === 'GroupEnd') {
return
}
props.send({
type: 'selectOperation',
data: {
targetSourceRange: sourceRangeFromRust(props.item.sourceRange),
},
})
} }
props.send({
type: 'selectOperation',
data: {
targetSourceRange: sourceRangeFromRust(props.item.sourceRange),
},
})
} }
/** /**
@ -457,20 +432,6 @@ const OperationItem = (props: {
} }
} }
function startSketchOnOffsetPlane() {
if (isOffsetPlane(props.item)) {
const artifact = findOperationArtifact(props.item)
if (artifact?.id) {
sceneInfra.modelingSend({
type: 'Enter sketch',
data: { forceNewSketch: true },
})
void selectOffsetSketchPlane(artifact)
}
}
}
const menuItems = useMemo( const menuItems = useMemo(
() => [ () => [
<ContextMenuItem <ContextMenuItem
@ -516,13 +477,6 @@ const OperationItem = (props: {
</ContextMenuItem>, </ContextMenuItem>,
] ]
: []), : []),
...(isOffsetPlane(props.item)
? [
<ContextMenuItem onClick={startSketchOnOffsetPlane}>
Start Sketch
</ContextMenuItem>,
]
: []),
...(props.item.type === 'StdLibCall' || ...(props.item.type === 'StdLibCall' ||
props.item.type === 'VariableDeclaration' props.item.type === 'VariableDeclaration'
? [ ? [
@ -596,63 +550,22 @@ const OperationItem = (props: {
[props.item, props.send] [props.item, props.send]
) )
const enabled = !props.sketchNoFace || isOffsetPlane(props.item)
return ( return (
<OperationItemWrapper <OperationItemWrapper
selectable={enabled}
icon={getOperationIcon(props.item)} icon={getOperationIcon(props.item)}
name={name} name={name}
variableName={variableName} variableName={variableName}
valueDetail={valueDetail} valueDetail={valueDetail}
menuItems={menuItems} menuItems={menuItems}
onClick={selectOperation} onClick={selectOperation}
onDoubleClick={props.sketchNoFace ? undefined : enterEditFlow} // no double click in "Sketch no face" mode onDoubleClick={enterEditFlow}
errors={errors} errors={errors}
greyedOut={!enabled}
/> />
) )
} }
const DefaultPlanes = () => { const DefaultPlanes = () => {
const { state: modelingState, send } = useModelingContext() const { state: modelingState, send } = useModelingContext()
const sketchNoFace = modelingState.matches('Sketch no face')
const onClickPlane = useCallback(
(planeId: string) => {
if (sketchNoFace) {
selectDefaultSketchPlane(planeId)
} else {
const foundDefaultPlane =
rustContext.defaultPlanes !== null &&
Object.entries(rustContext.defaultPlanes).find(
([, plane]) => plane === planeId
)
if (foundDefaultPlane) {
send({
type: 'Set selection',
data: {
selectionType: 'defaultPlaneSelection',
selection: {
name: foundDefaultPlane[0] as DefaultPlaneStr,
id: planeId,
},
},
})
}
}
},
[sketchNoFace]
)
const startSketchOnDefaultPlane = useCallback((planeId: string) => {
sceneInfra.modelingSend({
type: 'Enter sketch',
data: { forceNewSketch: true },
})
selectDefaultSketchPlane(planeId)
}, [])
const defaultPlanes = rustContext.defaultPlanes const defaultPlanes = rustContext.defaultPlanes
if (!defaultPlanes) return null if (!defaultPlanes) return null
@ -690,15 +603,7 @@ const DefaultPlanes = () => {
customSuffix={plane.customSuffix} customSuffix={plane.customSuffix}
icon={'plane'} icon={'plane'}
name={plane.name} name={plane.name}
selectable={true} selectable={false}
onClick={() => onClickPlane(plane.id)}
menuItems={[
<ContextMenuItem
onClick={() => startSketchOnDefaultPlane(plane.id)}
>
Start Sketch
</ContextMenuItem>,
]}
visibilityToggle={{ visibilityToggle={{
visible: modelingState.context.defaultPlaneVisibility[plane.key], visible: modelingState.context.defaultPlaneVisibility[plane.key],
onVisibilityChange: () => { onVisibilityChange: () => {
@ -715,17 +620,3 @@ const DefaultPlanes = () => {
</div> </div>
) )
} }
type StdLibCallOp = Extract<Operation, { type: 'StdLibCall' }>
const isOffsetPlane = (item: Operation): item is StdLibCallOp => {
return item.type === 'StdLibCall' && item.name === 'offsetPlane'
}
const findOperationArtifact = (item: StdLibCallOp) => {
const nodePath = JSON.stringify(item.nodePath)
const artifact = [...kclManager.artifactGraph.values()].find(
(a) => JSON.stringify((a as Plane).codeRef?.nodePath) === nodePath
)
return artifact
}

View File

@ -10,22 +10,13 @@ import {
import { useModelingContext } from '@src/hooks/useModelingContext' import { useModelingContext } from '@src/hooks/useModelingContext'
import type { AxisNames } from '@src/lib/constants' import type { AxisNames } from '@src/lib/constants'
import { VIEW_NAMES_SEMANTIC } from '@src/lib/constants' import { VIEW_NAMES_SEMANTIC } from '@src/lib/constants'
import { kclManager, sceneInfra } from '@src/lib/singletons' import { sceneInfra } from '@src/lib/singletons'
import { err, reportRejection } from '@src/lib/trap' import { reportRejection } from '@src/lib/trap'
import { useSettings } from '@src/lib/singletons' import { useSettings } from '@src/lib/singletons'
import { resetCameraPosition } from '@src/lib/resetCameraPosition' import { resetCameraPosition } from '@src/lib/resetCameraPosition'
import type { Selections } from '@src/lib/selections'
import {
selectDefaultSketchPlane,
selectOffsetSketchPlane,
} from '@src/lib/selections'
export function useViewControlMenuItems() { export function useViewControlMenuItems() {
const { state: modelingState, send: modelingSend } = useModelingContext() const { state: modelingState, send: modelingSend } = useModelingContext()
const selectedPlaneId = getCurrentPlaneId(
modelingState.context.selectionRanges
)
const settings = useSettings() const settings = useSettings()
const shouldLockView = const shouldLockView =
modelingState.matches('Sketch') && modelingState.matches('Sketch') &&
@ -65,35 +56,9 @@ export function useViewControlMenuItems() {
Center view on selection Center view on selection
</ContextMenuItem>, </ContextMenuItem>,
<ContextMenuDivider />, <ContextMenuDivider />,
<ContextMenuItem
onClick={() => {
if (selectedPlaneId) {
sceneInfra.modelingSend({
type: 'Enter sketch',
data: { forceNewSketch: true },
})
const defaultSketchPlaneSelected =
selectDefaultSketchPlane(selectedPlaneId)
if (
!err(defaultSketchPlaneSelected) &&
defaultSketchPlaneSelected
) {
return
}
const artifact = kclManager.artifactGraph.get(selectedPlaneId)
void selectOffsetSketchPlane(artifact)
}
}}
disabled={!selectedPlaneId}
>
Start sketch on selection
</ContextMenuItem>,
<ContextMenuDivider />,
<ContextMenuItemRefresh />, <ContextMenuItemRefresh />,
], ],
[VIEW_NAMES_SEMANTIC, shouldLockView, selectedPlaneId] [VIEW_NAMES_SEMANTIC, shouldLockView]
) )
return menuItems return menuItems
} }
@ -112,21 +77,3 @@ export function ViewControlContextMenu({
/> />
) )
} }
function getCurrentPlaneId(selectionRanges: Selections): string | null {
const defaultPlane = selectionRanges.otherSelections.find(
(selection) => typeof selection === 'object' && 'name' in selection
)
if (defaultPlane) {
return defaultPlane.id
}
const planeSelection = selectionRanges.graphSelections.find(
(selection) => selection.artifact?.type === 'plane'
)
if (planeSelection) {
return planeSelection.artifact?.id || null
}
return null
}

View File

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

View File

@ -14,15 +14,13 @@ import {
import { isTopLevelModule } from '@src/lang/util' import { isTopLevelModule } from '@src/lang/util'
import type { CallExpressionKw, PathToNode } from '@src/lang/wasm' import type { CallExpressionKw, PathToNode } from '@src/lang/wasm'
import { defaultSourceRange } from '@src/lang/sourceRange' import { defaultSourceRange } from '@src/lang/sourceRange'
import { import type { DefaultPlaneStr } from '@src/lib/planes'
getEventForSelectWithPoint, import { getEventForSelectWithPoint } from '@src/lib/selections'
selectDefaultSketchPlane,
selectOffsetSketchPlane,
} from '@src/lib/selections'
import { import {
editorManager, editorManager,
engineCommandManager, engineCommandManager,
kclManager, kclManager,
rustContext,
sceneEntitiesManager, sceneEntitiesManager,
sceneInfra, sceneInfra,
} from '@src/lib/singletons' } from '@src/lib/singletons'
@ -98,18 +96,131 @@ export function useEngineConnectionSubscriptions() {
;(async () => { ;(async () => {
let planeOrFaceId = data.entity_id let planeOrFaceId = data.entity_id
if (!planeOrFaceId) return if (!planeOrFaceId) return
const defaultSketchPlaneSelected =
selectDefaultSketchPlane(planeOrFaceId)
if ( if (
!err(defaultSketchPlaneSelected) && rustContext.defaultPlanes?.xy === planeOrFaceId ||
defaultSketchPlaneSelected rustContext.defaultPlanes?.xz === planeOrFaceId ||
rustContext.defaultPlanes?.yz === planeOrFaceId ||
rustContext.defaultPlanes?.negXy === planeOrFaceId ||
rustContext.defaultPlanes?.negXz === planeOrFaceId ||
rustContext.defaultPlanes?.negYz === planeOrFaceId
) { ) {
let planeId = planeOrFaceId
const defaultPlaneStrMap: Record<string, DefaultPlaneStr> = {
[rustContext.defaultPlanes.xy]: 'XY',
[rustContext.defaultPlanes.xz]: 'XZ',
[rustContext.defaultPlanes.yz]: 'YZ',
[rustContext.defaultPlanes.negXy]: '-XY',
[rustContext.defaultPlanes.negXz]: '-XZ',
[rustContext.defaultPlanes.negYz]: '-YZ',
}
// TODO can we get this information from rust land when it creates the default planes?
// maybe returned from make_default_planes (src/wasm-lib/src/wasm.rs)
let zAxis: [number, number, number] = [0, 0, 1]
let yAxis: [number, number, number] = [0, 1, 0]
// get unit vector from camera position to target
const camVector = sceneInfra.camControls.camera.position
.clone()
.sub(sceneInfra.camControls.target)
if (rustContext.defaultPlanes?.xy === planeId) {
zAxis = [0, 0, 1]
yAxis = [0, 1, 0]
if (camVector.z < 0) {
zAxis = [0, 0, -1]
planeId = rustContext.defaultPlanes?.negXy || ''
}
} else if (rustContext.defaultPlanes?.yz === planeId) {
zAxis = [1, 0, 0]
yAxis = [0, 0, 1]
if (camVector.x < 0) {
zAxis = [-1, 0, 0]
planeId = rustContext.defaultPlanes?.negYz || ''
}
} else if (rustContext.defaultPlanes?.xz === planeId) {
zAxis = [0, 1, 0]
yAxis = [0, 0, 1]
planeId = rustContext.defaultPlanes?.negXz || ''
if (camVector.y < 0) {
zAxis = [0, -1, 0]
planeId = rustContext.defaultPlanes?.xz || ''
}
}
sceneInfra.modelingSend({
type: 'Select sketch plane',
data: {
type: 'defaultPlane',
planeId: planeId,
plane: defaultPlaneStrMap[planeId],
zAxis,
yAxis,
},
})
return return
} }
const artifact = kclManager.artifactGraph.get(planeOrFaceId) const artifact = kclManager.artifactGraph.get(planeOrFaceId)
if (await selectOffsetSketchPlane(artifact)) {
if (artifact?.type === 'plane') {
const planeInfo =
await sceneEntitiesManager.getFaceDetails(planeOrFaceId)
// Apply camera-based orientation logic similar to default planes
let zAxis: [number, number, number] = [
planeInfo.z_axis.x,
planeInfo.z_axis.y,
planeInfo.z_axis.z,
]
let yAxis: [number, number, number] = [
planeInfo.y_axis.x,
planeInfo.y_axis.y,
planeInfo.y_axis.z,
]
// Get camera vector to determine which side of the plane we're viewing from
const camVector = sceneInfra.camControls.camera.position
.clone()
.sub(sceneInfra.camControls.target)
// Determine the canonical (absolute) plane orientation
const absZAxis: [number, number, number] = [
Math.abs(zAxis[0]),
Math.abs(zAxis[1]),
Math.abs(zAxis[2]),
]
// Find the dominant axis (like default planes do)
const maxComponent = Math.max(...absZAxis)
const dominantAxisIndex = absZAxis.indexOf(maxComponent)
// Check camera position against canonical orientation (like default planes)
const cameraComponents = [camVector.x, camVector.y, camVector.z]
let negated = cameraComponents[dominantAxisIndex] < 0
if (dominantAxisIndex === 1) {
// offset of the XZ is being weird, not sure if this is a camera bug
negated = !negated
}
sceneInfra.modelingSend({
type: 'Select sketch plane',
data: {
type: 'offsetPlane',
zAxis,
yAxis,
position: [
planeInfo.origin.x,
planeInfo.origin.y,
planeInfo.origin.z,
].map((num) => num / sceneInfra._baseUnitMultiplier) as [
number,
number,
number,
],
planeId: planeOrFaceId,
pathToNode: artifact.codeRef.pathToNode,
negated,
},
})
return return
} }

View File

@ -483,13 +483,13 @@ export function sketchOnExtrudedFace(
*/ */
export function addOffsetPlane({ export function addOffsetPlane({
node, node,
plane, defaultPlane,
insertIndex, insertIndex,
offset, offset,
planeName, planeName,
}: { }: {
node: Node<Program> node: Node<Program>
plane: Node<Literal> | Node<Name> // Can be DefaultPlaneStr or string for offsetPlanes defaultPlane: DefaultPlaneStr
insertIndex?: number insertIndex?: number
offset: Expr offset: Expr
planeName?: string planeName?: string
@ -500,9 +500,11 @@ export function addOffsetPlane({
const newPlane = createVariableDeclaration( const newPlane = createVariableDeclaration(
newPlaneName, newPlaneName,
createCallExpressionStdLibKw('offsetPlane', plane, [ createCallExpressionStdLibKw(
createLabeledArg('offset', offset), 'offsetPlane',
]) createLiteral(defaultPlane.toUpperCase()),
[createLabeledArg('offset', offset)]
)
) )
const insertAt = const insertAt =

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -34,7 +34,6 @@ import {
kclManager, kclManager,
rustContext, rustContext,
sceneEntitiesManager, sceneEntitiesManager,
sceneInfra,
} from '@src/lib/singletons' } from '@src/lib/singletons'
import { err } from '@src/lib/trap' import { err } from '@src/lib/trap'
import { import {
@ -804,156 +803,3 @@ export function getSemanticSelectionType(selectionType: Artifact['type'][]) {
return Array.from(semanticSelectionType) return Array.from(semanticSelectionType)
} }
export function selectDefaultSketchPlane(
defaultPlaneId: string
): Error | boolean {
const defaultPlanes = rustContext.defaultPlanes
if (!defaultPlanes) {
return new Error('No default planes defined in rustContext')
}
if (
![
defaultPlanes.xy,
defaultPlanes.xz,
defaultPlanes.yz,
defaultPlanes.negXy,
defaultPlanes.negXz,
defaultPlanes.negYz,
].includes(defaultPlaneId)
) {
// Supplied defaultPlaneId is not a valid default plane id
return false
}
const camVector = sceneInfra.camControls.camera.position
.clone()
.sub(sceneInfra.camControls.target)
// TODO can we get this information from rust land when it creates the default planes?
// maybe returned from make_default_planes (src/wasm-lib/src/wasm.rs)
let zAxis: [number, number, number] = [0, 0, 1]
let yAxis: [number, number, number] = [0, 1, 0]
if (defaultPlanes?.xy === defaultPlaneId) {
zAxis = [0, 0, 1]
yAxis = [0, 1, 0]
if (camVector.z < 0) {
zAxis = [0, 0, -1]
defaultPlaneId = defaultPlanes?.negXy || ''
}
} else if (defaultPlanes?.yz === defaultPlaneId) {
zAxis = [1, 0, 0]
yAxis = [0, 0, 1]
if (camVector.x < 0) {
zAxis = [-1, 0, 0]
defaultPlaneId = defaultPlanes?.negYz || ''
}
} else if (defaultPlanes?.xz === defaultPlaneId) {
zAxis = [0, 1, 0]
yAxis = [0, 0, 1]
defaultPlaneId = defaultPlanes?.negXz || ''
if (camVector.y < 0) {
zAxis = [0, -1, 0]
defaultPlaneId = defaultPlanes?.xz || ''
}
}
const defaultPlaneStrMap: Record<string, DefaultPlaneStr> = {
[defaultPlanes.xy]: 'XY',
[defaultPlanes.xz]: 'XZ',
[defaultPlanes.yz]: 'YZ',
[defaultPlanes.negXy]: '-XY',
[defaultPlanes.negXz]: '-XZ',
[defaultPlanes.negYz]: '-YZ',
}
sceneInfra.modelingSend({
type: 'Select sketch plane',
data: {
type: 'defaultPlane',
planeId: defaultPlaneId,
plane: defaultPlaneStrMap[defaultPlaneId],
zAxis,
yAxis,
},
})
return true
}
export async function selectOffsetSketchPlane(artifact: Artifact | undefined) {
return new Promise((resolve) => {
if (artifact?.type === 'plane') {
const planeId = artifact.id
void sceneEntitiesManager
.getFaceDetails(planeId)
.then((planeInfo) => {
// Apply camera-based orientation logic similar to default planes
let zAxis: [number, number, number] = [
planeInfo.z_axis.x,
planeInfo.z_axis.y,
planeInfo.z_axis.z,
]
let yAxis: [number, number, number] = [
planeInfo.y_axis.x,
planeInfo.y_axis.y,
planeInfo.y_axis.z,
]
// Get camera vector to determine which side of the plane we're viewing from
const camVector = sceneInfra.camControls.camera.position
.clone()
.sub(sceneInfra.camControls.target)
// Determine the canonical (absolute) plane orientation
const absZAxis: [number, number, number] = [
Math.abs(zAxis[0]),
Math.abs(zAxis[1]),
Math.abs(zAxis[2]),
]
// Find the dominant axis (like default planes do)
const maxComponent = Math.max(...absZAxis)
const dominantAxisIndex = absZAxis.indexOf(maxComponent)
// Check camera position against canonical orientation (like default planes)
const cameraComponents = [camVector.x, camVector.y, camVector.z]
let negated = cameraComponents[dominantAxisIndex] < 0
if (dominantAxisIndex === 1) {
// offset of the XZ is being weird, not sure if this is a camera bug
negated = !negated
}
sceneInfra.modelingSend({
type: 'Select sketch plane',
data: {
type: 'offsetPlane',
zAxis,
yAxis,
position: [
planeInfo.origin.x,
planeInfo.origin.y,
planeInfo.origin.z,
].map((num) => num / sceneInfra._baseUnitMultiplier) as [
number,
number,
number,
],
planeId,
pathToNode: artifact.codeRef.pathToNode,
negated,
},
})
resolve(true)
})
.catch((error) => {
console.error('Error getting face details:', error)
resolve(false)
})
} else {
// selectOffsetSketchPlane called with an invalid artifact type',
resolve(false)
}
})
}

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
import type { AppTheme } from '@rust/kcl-lib/bindings/AppTheme' import type { AppTheme } from '@rust/kcl-lib/bindings/AppTheme'
import { converter } from 'culori'
/** A media query matcher for dark mode */ /** A media query matcher for dark mode */
export const darkModeMatcher = export const darkModeMatcher =
@ -58,6 +59,84 @@ export function getOppositeTheme(theme: Themes) {
return resolvedTheme === Themes.Dark ? Themes.Light : Themes.Dark return resolvedTheme === Themes.Dark ? Themes.Light : Themes.Dark
} }
/**
* Converts OKLCH values to RGB using Culori library
* @param l - Lightness (0-1)
* @param c - Chroma (0-1)
* @param h - Hue (0-360 degrees)
* @returns RGB values as [r, g, b] where each component is 0-255
*/
function oklchToRgb(l: number, c: number, h: number): [number, number, number] {
// Create a converter from OKLCH to RGB using Culori
const toRgb = converter('rgb')
// Convert OKLCH to RGB using Culori
const rgb = toRgb({ mode: 'oklch', l, c, h })
if (!rgb) {
// Fallback if conversion fails
return [255, 255, 255]
}
// Clamp values. When OKLCH values represent colors outside the sRGB gamut, the RGB values can be negative or greater than 1.
const clampedR = Math.max(0, Math.min(1, rgb.r))
const clampedG = Math.max(0, Math.min(1, rgb.g))
const clampedB = Math.max(0, Math.min(1, rgb.b))
// Convert from 0-1 range to 0-255 range
return [
Math.round(clampedR * 255),
Math.round(clampedG * 255),
Math.round(clampedB * 255),
]
}
/**
* Gets the primary color from CSS custom properties and converts it to Three.js hex format
* @returns Primary color as a hex number for Three.js, or fallback purple if unable to get CSS value
*/
export function getPrimaryColorForThreeJs(): number {
if (typeof globalThis.window === 'undefined' || !globalThis.document) {
// Fallback for SSR or when DOM is not available
return 0x7c3aed // Default purple
}
try {
const computedStyle = getComputedStyle(document.documentElement)
// Get the individual primary color components
const hue = parseFloat(
computedStyle.getPropertyValue('--primary-hue').trim()
)
const chroma = parseFloat(
computedStyle.getPropertyValue('--primary-chroma').trim()
)
const lightness =
parseFloat(
computedStyle
.getPropertyValue('--primary-lightness')
.replace('%', '')
.trim()
) / 100
if (Number.isNaN(hue) || Number.isNaN(chroma) || Number.isNaN(lightness)) {
console.warn(
'Unable to parse primary color components from CSS, using fallback'
)
return 0x7c3aed // Default purple
}
// Convert OKLCH to RGB
const [r, g, b] = oklchToRgb(lightness, chroma, hue)
// Convert RGB to hex
return (r << 16) | (g << 8) | b
} catch (error) {
console.warn('Error getting primary color from CSS:', error)
return 0x7c3aed // Default purple fallback
}
}
/** /**
* The engine takes RGBA values from 0-1 * The engine takes RGBA values from 0-1
* So we convert from the conventional 0-255 found in Figma * So we convert from the conventional 0-255 found in Figma

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@ import {
engineCommandManager, engineCommandManager,
kclManager, kclManager,
} from '@src/lib/singletons' } from '@src/lib/singletons'
import { VITE_KITTYCAD_API_TOKEN } from '@src/env' import { VITE_KC_DEV_TOKEN } from '@src/env'
import { getConstraintInfoKw } from '@src/lang/std/sketch' import { getConstraintInfoKw } from '@src/lang/std/sketch'
import { getNodeFromPath } from '@src/lang/queryAst' import { getNodeFromPath } from '@src/lang/queryAst'
import type { Node } from '@rust/kcl-lib/bindings/Node' import type { Node } from '@rust/kcl-lib/bindings/Node'
@ -29,9 +29,10 @@ import { removeSingleConstraintInfo } from '@src/lang/modifyAst'
beforeAll(async () => { beforeAll(async () => {
await initPromise await initPromise
// THESE TEST WILL FAIL without VITE_KC_DEV_TOKEN set in .env.development.local
await new Promise((resolve) => { await new Promise((resolve) => {
engineCommandManager.start({ engineCommandManager.start({
token: VITE_KITTYCAD_API_TOKEN, token: VITE_KC_DEV_TOKEN,
width: 256, width: 256,
height: 256, height: 256,
setMediaStream: () => {}, setMediaStream: () => {},

View File

@ -2609,15 +2609,15 @@ export const modelingMachine = setup({
insertIndex = nodeToEdit[1][0] insertIndex = nodeToEdit[1][0]
} }
const selectedPlane = getSelectedPlane(selection) // Extract the default plane from selection
if (!selectedPlane) { const plane = selection.otherSelections[0]
if (!(plane && plane instanceof Object && 'name' in plane))
return trap('No plane selected') return trap('No plane selected')
}
// Get the default plane name from the selection // Get the default plane name from the selection
const offsetPlaneResult = addOffsetPlane({ const offsetPlaneResult = addOffsetPlane({
node: ast, node: ast,
plane: selectedPlane, defaultPlane: plane.name,
offset: offset:
'variableName' in distance 'variableName' in distance
? distance.variableIdentifierAst ? distance.variableIdentifierAst
@ -5520,33 +5520,6 @@ export function isEditingExistingSketch({
return (hasStartProfileAt && maybePipeExpression.body.length > 1) || hasCircle return (hasStartProfileAt && maybePipeExpression.body.length > 1) || hasCircle
} }
const getSelectedPlane = (
selection: Selections
): Node<Name> | Node<Literal> | undefined => {
const defaultPlane = selection.otherSelections[0]
if (
defaultPlane &&
defaultPlane instanceof Object &&
'name' in defaultPlane
) {
return createLiteral(defaultPlane.name.toUpperCase())
}
const offsetPlane = selection.graphSelections[0]
if (offsetPlane.artifact?.type === 'plane') {
const artifactId = offsetPlane.artifact?.id
const variableName = Object.entries(kclManager.variables).find(
([_, value]) => {
return value?.type === 'Plane' && value.value?.artifactId === artifactId
}
)
const offsetPlaneName = variableName?.[0]
return offsetPlaneName ? createLocalName(offsetPlaneName) : undefined
}
return undefined
}
export function pipeHasCircle({ export function pipeHasCircle({
sketchDetails, sketchDetails,
}: { }: {

View File

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

View File

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

View File

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

View File

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