Compare commits

..

1 Commits

Author SHA1 Message Date
0c2f63b399 Fix yarn lint to use all files recursively 2025-01-12 15:47:33 -05:00
793 changed files with 72198 additions and 155872 deletions

View File

@ -1,3 +1,3 @@
[codespell]
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,atleast,ue,afterall,ser
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,atleast,ue,afterall
skip: **/target,node_modules,build,dist,./out,**/Cargo.lock,./docs/kcl/*.md,.yarn.lock,**/yarn.lock,./openapi/*.json,./packages/codemirror-lang-kcl/test/all.test.ts,tsconfig.tsbuildinfo

View File

@ -6,24 +6,16 @@
"plugins": [
"css-modules",
"jest",
"jsx-a11y",
"react",
"react-hooks",
"suggest-no-throw",
"testing-library",
"@typescript-eslint"
],
"extends": [
"plugin:css-modules/recommended",
"plugin:jsx-a11y/recommended",
"plugin:react-hooks/recommended"
"plugin:css-modules/recommended"
],
"rules": {
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error",
"jsx-a11y/click-events-have-key-events": "off",
"jsx-a11y/no-autofocus": "off",
"jsx-a11y/no-noninteractive-element-interactions": "off",
"no-restricted-globals": [
"error",
{
@ -41,9 +33,6 @@
"overrides": [
{
"files": ["e2e/**/*.ts"], // Update the pattern based on your file structure
"extends": [
"plugin:testing-library/react"
],
"rules": {
"suggest-no-throw/suggest-no-throw": "off",
"testing-library/prefer-screen-queries": "off",
@ -52,9 +41,6 @@
},
{
"files": ["src/**/*.test.ts"],
"extends": [
"plugin:testing-library/react"
],
"rules": {
"suggest-no-throw/suggest-no-throw": "off",
}

View File

@ -6,29 +6,23 @@
version: 2
updates:
- package-ecosystem: 'npm' # See documentation for possible values
directories:
- '/'
- '/packages/codemirror-lang-kcl/'
- '/packages/codemirror-lsp-client/'
directory: '/' # Location of package manifests
schedule:
interval: weekly
day: monday
interval: 'weekly'
reviewers:
- franknoirot
- irev-dev
- package-ecosystem: 'github-actions' # See documentation for possible values
directory: '/' # Location of package manifests
schedule:
interval: weekly
day: monday
interval: 'weekly'
reviewers:
- adamchalmers
- jessfraz
- package-ecosystem: 'cargo' # See documentation for possible values
directory: '/src/wasm-lib/' # Location of package manifests
schedule:
interval: weekly
day: monday
interval: 'weekly'
reviewers:
- adamchalmers
- jessfraz
@ -36,6 +30,3 @@ updates:
serde-dependencies:
patterns:
- "serde*"
wasm-bindgen-deps:
patterns:
- "wasm-bindgen*"

View File

@ -27,7 +27,7 @@ jobs:
# Upload the WASM bundle as an artifact
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v3
with:
name: wasm-bundle
path: src/wasm-lib/pkg

View File

@ -126,13 +126,7 @@ jobs:
node-version-file: '.nvmrc'
cache: 'yarn' # Set this to npm, yarn or pnpm.
- name: yarn install
# Windows is picky sometimes and fails on fetch. Step takes about ~30s
uses: nick-fields/retry@v3.0.0
with:
timeout_minutes: 2
max_attempts: 3
command: yarn install
- run: yarn install
- run: yarn tronb:vite

44
.github/workflows/cargo-bench.yml vendored Normal file
View File

@ -0,0 +1,44 @@
on:
push:
branches:
- main
paths:
- '**.rs'
- '**/Cargo.toml'
- '**/Cargo.lock'
- '**/rust-toolchain.toml'
- .github/workflows/cargo-bench.yml
pull_request:
paths:
- '**.rs'
- '**/Cargo.toml'
- '**/Cargo.lock'
- '**/rust-toolchain.toml'
- .github/workflows/cargo-bench.yml
workflow_dispatch:
permissions: read-all
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
name: cargo bench
jobs:
cargo-bench:
name: Benchmark with iai
runs-on: ubuntu-latest-8-cores
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Install dependencies
run: |
cargo install cargo-criterion
sudo apt update
sudo apt install -y valgrind
- name: Rust Cache
uses: Swatinem/rust-cache@v2.6.1
- name: Benchmark kcl library
shell: bash
run: |-
cd src/wasm-lib/kcl; cargo bench --all-features -- iai
env:
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}

View File

@ -1,32 +0,0 @@
name: CodeMirror Lang KCL
on:
pull_request:
push:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
yarn-unit-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
working-directory: packages/codemirror-lang-kcl
- run: yarn tsc
working-directory: packages/codemirror-lang-kcl
- name: run unit tests
run: yarn test
working-directory: packages/codemirror-lang-kcl

View File

@ -126,20 +126,20 @@ jobs:
- name: build electron
shell: bash
run: yarn tron:package
# - name: Run ubuntu/chrome snapshots
# if: ${{ matrix.os == 'namespace-profile-ubuntu-8-cores' && matrix.shardIndex == 1 }}
# shell: bash
# # TODO: break this in its own job, for now it's not slowing down the overall execution as ubuntu is the quickest,
# # but we could do better. This forces a large 1/1 shard of all 20 snapshot tests that runs in about 3 minutes.
# run: |
# PLATFORM=web yarn playwright test --config=playwright.config.ts --retries="3" --update-snapshots --grep=@snapshot --shard=1/1
# env:
# CI: true
# NODE_ENV: development
# VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
# VITE_KC_SKIP_AUTH: true
# token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
# snapshottoken: ${{ secrets.KITTYCAD_API_TOKEN }}
- name: Run ubuntu/chrome snapshots
if: ${{ matrix.os == 'namespace-profile-ubuntu-8-cores' && matrix.shardIndex == 1 }}
shell: bash
# TODO: break this in its own job, for now it's not slowing down the overall execution as ubuntu is the quickest,
# but we could do better. This forces a large 1/1 shard of all 20 snapshot tests that runs in about 3 minutes.
run: |
PLATFORM=web yarn playwright test --config=playwright.config.ts --retries="3" --update-snapshots --grep=@snapshot --shard=1/1
env:
CI: true
NODE_ENV: development
VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
VITE_KC_SKIP_AUTH: true
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
snapshottoken: ${{ secrets.KITTYCAD_API_TOKEN }}
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() && (success() || failure()) }}
with:
@ -162,20 +162,20 @@ jobs:
then echo "modified=true" >> $GITHUB_OUTPUT
else echo "modified=false" >> $GITHUB_OUTPUT
fi
# - name: Commit changes, if any
# if: steps.git-check.outputs.modified == 'true'
# shell: bash
# run: |
# git add .
# git config --local user.email "github-actions[bot]@users.noreply.github.com"
# git config --local user.name "github-actions[bot]"
# git remote set-url origin https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
# git fetch origin
# echo ${{ github.head_ref }}
# git checkout ${{ github.head_ref }}
# git commit -am "A snapshot a day keeps the bugs away! 📷🐛 (OS: ${{matrix.os}})" || true
# git push
# git push origin ${{ github.head_ref }}
- name: Commit changes, if any
if: steps.git-check.outputs.modified == 'true'
shell: bash
run: |
git add .
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git remote set-url origin https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
git fetch origin
echo ${{ github.head_ref }}
git checkout ${{ github.head_ref }}
git commit -am "A snapshot a day keeps the bugs away! 📷🐛 (OS: ${{matrix.os}})" || true
git push
git push origin ${{ github.head_ref }}
# only upload artifacts if there's actually changes
- uses: actions/upload-artifact@v4
if: steps.git-check.outputs.modified == 'true'

2
.gitignore vendored
View File

@ -44,7 +44,7 @@ e2e/playwright/temp3.png
e2e/playwright/export-snapshots/*
!e2e/playwright/export-snapshots/*.png
/kcl-samples
/test-results/
/playwright-report/
/blob-report/

View File

@ -24,3 +24,5 @@ once fixed in engine will just start working here with no language changes.
chamfer cases work currently.
- **Appearance**: Changing the appearance on a loft does not work.
- **Helix**: Currently sweeping a helix does not work.

File diff suppressed because one or more lines are too long

View File

@ -4,16 +4,14 @@ excerpt: "Import a CAD file."
layout: manual
---
**WARNING:** This function is deprecated.
Import a CAD file.
**DEPRECATED** Prefer to use import statements.
For formats lacking unit data (such as STL, OBJ, or PLY files), the default unit of measurement is millimeters. Alternatively you may specify the unit by passing your desired measurement unit in the options parameter. When importing a GLTF file, the bin file will be imported as well. Import paths are relative to the current project directory.
Note: The import command currently only works when using the native Modeling App.
For importing KCL functions using the `import` statement, see the docs on [KCL modules](/docs/kcl/modules).
```js
import(file_path: String, options?: ImportFormat) -> ImportedGeometry
```

View File

@ -51,7 +51,9 @@ layout: manual
* [`helixRevolutions`](kcl/helixRevolutions)
* [`hole`](kcl/hole)
* [`hollow`](kcl/hollow)
* [`import`](kcl/import)
* [`inch`](kcl/inch)
* [`int`](kcl/int)
* [`lastSegX`](kcl/lastSegX)
* [`lastSegY`](kcl/lastSegY)
* [`legAngX`](kcl/legAngX)

View File

@ -4,8 +4,6 @@ excerpt: "Convert a number to an integer."
layout: manual
---
**WARNING:** This function is deprecated.
Convert a number to an integer.
DEPRECATED use floor(), ceil(), or round().

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,28 +0,0 @@
---
title: "Face"
excerpt: "A face."
layout: manual
---
A face.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `id` |`string`| The id of the face. | No |
| `value` |`string`| The tag of the face. | No |
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the faces X axis be? | No |
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the faces Y axis be? | No |
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
| `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A face. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |

View File

@ -20,7 +20,6 @@ A helix.
| `revolutions` |`number`| Number of revolutions. | No |
| `angleStart` |`number`| Start angle (in degrees). | No |
| `ccw` |`boolean`| Is the helix rotation counter clockwise? | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A helix. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |

View File

@ -19,7 +19,7 @@ Data for a helix.
| `revolutions` |`number`| Number of revolutions. | No |
| `angleStart` |`number`| Start angle (in degrees). | No |
| `ccw` |`boolean`| Is the helix rotation counter clockwise? The default is `false`. | No |
| `length` |`number`| Length of the helix. This is not necessary if the helix is created around an edge. If not given the length of the edge is used. | No |
| `length` |`number`| Length of the helix. | No |
| `radius` |`number`| Radius of the helix. | No |
| `axis` |[`Axis3dOrEdgeReference`](/docs/kcl/types/Axis3dOrEdgeReference)| Axis to use as mirror. | No |

View File

@ -20,7 +20,6 @@ A helix.
| `revolutions` |`number`| Number of revolutions. | No |
| `angleStart` |`number`| Start angle (in degrees). | No |
| `ccw` |`boolean`| Is the helix rotation counter clockwise? | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A helix. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |

View File

@ -168,6 +168,7 @@ Any KCL value.
----
A plane.
**Type:** `object`
@ -180,10 +181,17 @@ Any KCL value.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Plane`](/docs/kcl/types/Plane)| | No |
| `value` |[`Plane`](/docs/kcl/types/Plane)| Any KCL value. | No |
| `id` |`string`| The id of the plane. | No |
| `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| Any KCL value. | No |
| `origin` |[`Point3d`](/docs/kcl/types/Point3d)| Origin of the plane. | No |
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the planes X axis be? | No |
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the planes Y axis be? | No |
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
A face.
**Type:** `object`
@ -195,8 +203,14 @@ Any KCL value.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Face`](/docs/kcl/types/Face)| | No |
| `value` |[`Face`](/docs/kcl/types/Face)| Any KCL value. | No |
| `type` |enum: `Face`| | No |
| `id` |`string`| The id of the face. | No |
| `value` |`string`| The tag of the face. | No |
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the faces X axis be? | No |
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the faces Y axis be? | No |
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
| `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
@ -232,6 +246,7 @@ Any KCL value.
----
An solid is a collection of extrude surfaces.
**Type:** `object`
@ -244,7 +259,14 @@ Any KCL value.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Solid`](/docs/kcl/types/Solid)| | No |
| `value` |[`Solid`](/docs/kcl/types/Solid)| Any KCL value. | No |
| `id` |`string`| The id of the solid. | No |
| `value` |`[` [`ExtrudeSurface`](/docs/kcl/types/ExtrudeSurface) `]`| The extrude surfaces. | No |
| `sketch` |[`Sketch`](/docs/kcl/types/Sketch)| The sketch. | No |
| `height` |`number`| The height of the solid. | No |
| `startCapId` |`string`| The id of the extrusion start cap | No |
| `endCapId` |`string`| The id of the extrusion end cap | No |
| `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |
----
@ -264,6 +286,7 @@ Any KCL value.
----
A helix.
**Type:** `object`
@ -276,7 +299,11 @@ Any KCL value.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Helix`](/docs/kcl/types/Helix)| | No |
| `value` |[`Helix`](/docs/kcl/types/Helix)| Any KCL value. | No |
| `value` |`string`| The id of the helix. | No |
| `revolutions` |`number`| Number of revolutions. | No |
| `angleStart` |`number`| Start angle (in degrees). | No |
| `ccw` |`boolean`| Is the helix rotation counter clockwise? | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----

View File

@ -22,7 +22,6 @@ A plane.
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the planes X axis be? | No |
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the planes Y axis be? | No |
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A plane. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |

View File

@ -21,7 +21,6 @@ A sketch is a collection of paths.
| `on` |[`SketchSurface`](/docs/kcl/types/SketchSurface)| What the sketch is on (can be a plane or a face). | No |
| `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No |
| `tags` |`object`| Tag identifiers that have been declared in this sketch. | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch is a collection of paths. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |

View File

@ -30,7 +30,6 @@ A sketch is a collection of paths.
| `on` |[`SketchSurface`](/docs/kcl/types/SketchSurface)| What the sketch is on (can be a plane or a face). | No |
| `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No |
| `tags` |`object`| Tag identifiers that have been declared in this sketch. | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch or a group of sketches. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |

View File

@ -31,7 +31,6 @@ A plane.
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the planes X axis be? | No |
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the planes Y axis be? | No |
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch type. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
@ -55,7 +54,6 @@ A face.
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the faces Y axis be? | No |
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
| `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch type. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |

View File

@ -23,7 +23,6 @@ An solid is a collection of extrude surfaces.
| `startCapId` |`string`| The id of the extrusion start cap | No |
| `endCapId` |`string`| The id of the extrusion end cap | No |
| `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| An solid is a collection of extrude surfaces. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |

View File

@ -32,7 +32,6 @@ An solid is a collection of extrude surfaces.
| `startCapId` |`string`| The id of the extrusion start cap | No |
| `endCapId` |`string`| The id of the extrusion end cap | No |
| `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A solid or a group of solids. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |

View File

@ -1,107 +0,0 @@
---
title: "UnitLen"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Mm`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Cm`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `M`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Inches`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Feet`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Yards`| | No |
----

View File

@ -280,7 +280,7 @@ test(
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
await expect(page.getByText('router-template-slate')).toBeVisible()
await expect(page.getByText('Create project')).toBeVisible()
await expect(page.getByText('New Project')).toBeVisible()
})
await test.step('Opening the router-template project should load', async () => {

View File

@ -1,8 +1,7 @@
import { test, expect } from './zoo-test'
import * as fsp from 'fs/promises'
import { executorInputPath, getUtils } from './test-utils'
import { getUtils } from './test-utils'
import { KCL_DEFAULT_LENGTH } from 'lib/constants'
import path from 'path'
test.describe('Command bar tests', () => {
test('Extrude from command bar selects extrude line after', async ({
@ -46,6 +45,46 @@ test.describe('Command bar tests', () => {
)
})
// TODO: fix this test after the electron migration
test.fixme('Fillet from command bar', async ({ page, homePage }) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`sketch001 = startSketchOn('XY')
|> startProfileAt([-5, -5], %)
|> line([0, 10], %)
|> line([10, 0], %)
|> line([0, -10], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(-10, sketch001)`
)
})
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
const selectSegment = () => page.getByText(`line([0, -10], %)`).click()
await selectSegment()
await page.waitForTimeout(100)
await page.getByRole('button', { name: 'Fillet' }).click()
await page.waitForTimeout(100)
await page.keyboard.press('Enter') // skip selection
await page.waitForTimeout(100)
await page.keyboard.press('Enter') // accept default radius
await page.waitForTimeout(100)
await page.keyboard.press('Enter') // submit
await page.waitForTimeout(100)
await expect(page.locator('.cm-activeLine')).toContainText(
`fillet({ radius = ${KCL_DEFAULT_LENGTH}, tags = [seg01] }, %)`
)
})
test('Command bar can change a setting, and switch back and forth between arguments', async ({
page,
homePage,
@ -306,132 +345,4 @@ test.describe('Command bar tests', () => {
await arcToolCommand.click()
await expect(arcToolButton).toHaveAttribute('aria-pressed', 'true')
})
test(`Reacts to query param to open "import from URL" command`, async ({
page,
cmdBar,
editor,
homePage,
}) => {
await test.step(`Prepare and navigate to home page with query params`, async () => {
const targetURL = `?create-file&name=test&units=mm&code=ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg%3D%3D&ask-open-desktop`
await homePage.expectState({
projectCards: [],
sortBy: 'last-modified-desc',
})
await page.goto(page.url() + targetURL)
expect(page.url()).toContain(targetURL)
})
await test.step(`Submit the command`, async () => {
await cmdBar.expectState({
stage: 'arguments',
commandName: 'Import file from URL',
currentArgKey: 'method',
currentArgValue: '',
headerArguments: {
Method: '',
Name: 'test',
Code: '1 line',
},
highlightedHeaderArg: 'method',
})
await cmdBar.selectOption({ name: 'New Project' }).click()
await cmdBar.expectState({
stage: 'review',
commandName: 'Import file from URL',
headerArguments: {
Method: 'New project',
Name: 'test',
Code: '1 line',
},
})
await cmdBar.progressCmdBar()
})
await test.step(`Ensure we created the project and are in the modeling scene`, async () => {
await editor.expectEditor.toContain('extrusionDistance = 12')
})
})
test(`"import from URL" can add to existing project`, async ({
page,
cmdBar,
editor,
homePage,
toolbar,
context,
}) => {
await context.folderSetupFn(async (dir) => {
const testProjectDir = path.join(dir, 'testProjectDir')
await Promise.all([fsp.mkdir(testProjectDir, { recursive: true })])
await Promise.all([
fsp.copyFile(
executorInputPath('cylinder.kcl'),
path.join(testProjectDir, 'main.kcl')
),
])
})
await test.step(`Prepare and navigate to home page with query params`, async () => {
const targetURL = `?create-file&name=test&units=mm&code=ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg%3D%3D&ask-open-desktop`
await homePage.expectState({
projectCards: [
{
fileCount: 1,
title: 'testProjectDir',
},
],
sortBy: 'last-modified-desc',
})
await page.goto(page.url() + targetURL)
expect(page.url()).toContain(targetURL)
})
await test.step(`Submit the command`, async () => {
await cmdBar.expectState({
stage: 'arguments',
commandName: 'Import file from URL',
currentArgKey: 'method',
currentArgValue: '',
headerArguments: {
Method: '',
Name: 'test',
Code: '1 line',
},
highlightedHeaderArg: 'method',
})
await cmdBar.selectOption({ name: 'Existing Project' }).click()
await cmdBar.expectState({
stage: 'arguments',
commandName: 'Import file from URL',
currentArgKey: 'projectName',
currentArgValue: '',
headerArguments: {
Method: 'Existing project',
Name: 'test',
ProjectName: '',
Code: '1 line',
},
highlightedHeaderArg: 'projectName',
})
await cmdBar.selectOption({ name: 'testProjectDir' }).click()
await cmdBar.expectState({
stage: 'review',
commandName: 'Import file from URL',
headerArguments: {
Method: 'Existing project',
ProjectName: 'testProjectDir',
Name: 'test',
Code: '1 line',
},
})
await cmdBar.progressCmdBar()
})
await test.step(`Ensure we created the project and are in the modeling scene`, async () => {
await editor.expectEditor.toContain('extrusionDistance = 12')
await toolbar.openPane('files')
await toolbar.expectFileTreeState(['main.kcl', 'test.kcl'])
})
})
})

View File

@ -38,14 +38,14 @@ test.describe('Debug pane', () => {
// Set the code in the code editor.
await u.codeLocator.click()
await page.keyboard.type(code, { delay: 0 })
// Scroll to the artifact graph.
// Scroll to the feature tree.
await tree.scrollIntoViewIfNeeded()
// Expand the artifact graph.
await tree.getByText('Artifact Graph').click()
// Expand the feature tree.
await tree.getByText('Feature Tree').click()
// Just expanded the details, making the element taller, so scroll again.
await tree.getByText('Plane').first().scrollIntoViewIfNeeded()
})
// Extract the artifact IDs from the debug artifact graph.
// Extract the artifact IDs from the debug feature tree.
const initialSegmentIds = await segment.innerText({ timeout: 5_000 })
// The artifact ID should include a UUID.
expect(initialSegmentIds).toMatch(

View File

@ -4,6 +4,7 @@ import { expect } from '@playwright/test'
type CmdBarSerialised =
| {
stage: 'commandBarClosed'
// TODO no more properties needed but needs to be implemented in _serialiseCmdBar
}
| {
stage: 'pickCommand'
@ -36,9 +37,6 @@ export class CmdBarFixture {
}
private _serialiseCmdBar = async (): Promise<CmdBarSerialised> => {
if (!(await this.page.getByTestId('command-bar-wrapper').isVisible())) {
return { stage: 'commandBarClosed' }
}
const reviewForm = this.page.locator('#review-form')
const getHeaderArgs = async () => {
const inputs = await this.page.getByTestId('cmd-bar-input-tab').all()
@ -137,27 +135,4 @@ export class CmdBarFixture {
await promptEditCommand.first().click()
}
}
get cmdSearchInput() {
return this.page.getByTestId('cmd-bar-search')
}
get argumentInput() {
return this.page.getByTestId('cmd-bar-arg-value')
}
get cmdOptions() {
return this.page.getByTestId('cmd-bar-option')
}
chooseCommand = async (commandName: string) => {
await this.cmdOptions.getByText(commandName).click()
}
/**
* Select an option from the command bar
*/
selectOption = (options: Parameters<typeof this.page.getByRole>[1]) => {
return this.page.getByRole('option', options)
}
}

View File

@ -121,23 +121,18 @@ export class AuthenticatedTronApp {
export const fixtures = {
cmdBar: async ({ page }: { page: Page }, use: any) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
await use(new CmdBarFixture(page))
},
editor: async ({ page }: { page: Page }, use: any) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
await use(new EditorFixture(page))
},
toolbar: async ({ page }: { page: Page }, use: any) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
await use(new ToolbarFixture(page))
},
scene: async ({ page }: { page: Page }, use: any) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
await use(new SceneFixture(page))
},
homePage: async ({ page }: { page: Page }, use: any) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
await use(new HomePageFixture(page))
},
}

View File

@ -103,7 +103,7 @@ export class HomePageFixture {
.toEqual(expectedState)
}
createAndGoToProject = async (projectTitle = 'project-$nnn') => {
createAndGoToProject = async (projectTitle: string) => {
await expect(this.projectSection).not.toHaveText('Loading your Projects...')
await this.projectButtonNew.click()
await this.projectTextName.click()

View File

@ -15,8 +15,6 @@ export class ToolbarFixture {
extrudeButton!: Locator
loftButton!: Locator
sweepButton!: Locator
filletButton!: Locator
chamferButton!: Locator
shellButton!: Locator
offsetPlaneButton!: Locator
startSketchBtn!: Locator
@ -44,8 +42,6 @@ export class ToolbarFixture {
this.extrudeButton = page.getByTestId('extrude')
this.loftButton = page.getByTestId('loft')
this.sweepButton = page.getByTestId('sweep')
this.filletButton = page.getByTestId('fillet3d')
this.chamferButton = page.getByTestId('chamfer3d')
this.shellButton = page.getByTestId('shell')
this.offsetPlaneButton = page.getByTestId('plane-offset')
this.startSketchBtn = page.getByTestId('sketch')
@ -63,10 +59,6 @@ export class ToolbarFixture {
this.exeIndicator = page.getByTestId('model-state-indicator-execution-done')
}
get logoLink() {
return this.page.getByTestId('app-logo')
}
startSketchPlaneSelection = async () =>
doAndWaitForImageDiff(this.page, () => this.startSketchBtn.click(), 500)

View File

@ -829,6 +829,12 @@ loftPointAndClickCases.forEach(({ shouldPreselect }) => {
})
await selectSketches()
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: { Selection: '2 faces' },
commandName: 'Loft',
})
await cmdBar.progressCmdBar()
})
} else {
await test.step(`Preselect the two sketches`, async () => {
@ -838,6 +844,12 @@ loftPointAndClickCases.forEach(({ shouldPreselect }) => {
await test.step(`Go through the command bar flow with preselected sketches`, async () => {
await toolbar.loftButton.click()
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: { Selection: '2 faces' },
commandName: 'Loft',
})
await cmdBar.progressCmdBar()
})
}
@ -963,31 +975,37 @@ sketch002 = startSketchOn('XZ')
await toolbar.sweepButton.click()
await cmdBar.expectState({
commandName: 'Sweep',
currentArgKey: 'target',
currentArgKey: 'profile',
currentArgValue: '',
headerArguments: {
Target: '',
Trajectory: '',
Path: '',
Profile: '',
},
highlightedHeaderArg: 'target',
highlightedHeaderArg: 'profile',
stage: 'arguments',
})
await clickOnSketch1()
await cmdBar.expectState({
commandName: 'Sweep',
currentArgKey: 'trajectory',
currentArgKey: 'path',
currentArgValue: '',
headerArguments: {
Target: '1 face',
Trajectory: '',
Path: '',
Profile: '1 face',
},
highlightedHeaderArg: 'trajectory',
highlightedHeaderArg: 'path',
stage: 'arguments',
})
await clickOnSketch2()
await page.waitForTimeout(500)
await cmdBar.expectState({
commandName: 'Sweep',
headerArguments: {
Path: '1 face',
Profile: '1 face',
},
stage: 'review',
})
await cmdBar.progressCmdBar()
await page.waitForTimeout(500)
})
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
@ -1014,505 +1032,6 @@ sketch002 = startSketchOn('XZ')
})
})
test(`Sweep point-and-click failing validation`, async ({
context,
page,
homePage,
scene,
toolbar,
cmdBar,
}) => {
const initialCode = `sketch001 = startSketchOn('YZ')
|> circle({
center = [0, 0],
radius = 500
}, %)
sketch002 = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> xLine(-500, %)
|> lineTo([-2000, 500], %)
`
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
// One dumb hardcoded screen pixel value
const testPoint = { x: 700, y: 250 }
const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const [clickOnSketch2] = scene.makeMouseHelpers(testPoint.x - 50, testPoint.y)
await test.step(`Look for sketch001`, async () => {
await toolbar.closePane('code')
await scene.expectPixelColor([53, 53, 53], testPoint, 15)
})
await test.step(`Go through the command bar flow and fail validation with a toast`, async () => {
await toolbar.sweepButton.click()
await cmdBar.expectState({
commandName: 'Sweep',
currentArgKey: 'target',
currentArgValue: '',
headerArguments: {
Target: '',
Trajectory: '',
},
highlightedHeaderArg: 'target',
stage: 'arguments',
})
await clickOnSketch1()
await cmdBar.expectState({
commandName: 'Sweep',
currentArgKey: 'trajectory',
currentArgValue: '',
headerArguments: {
Target: '1 face',
Trajectory: '',
},
highlightedHeaderArg: 'trajectory',
stage: 'arguments',
})
await clickOnSketch2()
await page.waitForTimeout(500)
await cmdBar.progressCmdBar()
await expect(
page.getByText('Unable to sweep with the current selection. Reason:')
).toBeVisible()
})
})
test(`Fillet point-and-click`, async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
// Code samples
const initialCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-12, -6], %)
|> line([0, 12], %)
|> line([24, 0], %)
|> line([0, -12], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(-12, sketch001)
`
const firstFilletDeclaration = 'fillet({ radius = 5, tags = [seg01] }, %)'
const secondFilletDeclaration =
'fillet({ radius = 5, tags = [getOppositeEdge(seg01)] }, %)'
// Locators
const firstEdgeLocation = { x: 600, y: 193 }
const secondEdgeLocation = { x: 600, y: 383 }
const bodyLocation = { x: 630, y: 290 }
const [clickOnFirstEdge] = scene.makeMouseHelpers(
firstEdgeLocation.x,
firstEdgeLocation.y
)
const [clickOnSecondEdge] = scene.makeMouseHelpers(
secondEdgeLocation.x,
secondEdgeLocation.y
)
// Colors
const edgeColorWhite: [number, number, number] = [248, 248, 248]
const edgeColorYellow: [number, number, number] = [251, 251, 40] // Mac:B=67 Ubuntu:B=12
const bodyColor: [number, number, number] = [155, 155, 155]
const filletColor: [number, number, number] = [127, 127, 127]
const backgroundColor: [number, number, number] = [30, 30, 30]
const lowTolerance = 20
const highTolerance = 40
// Setup
await test.step(`Initial test setup`, async () => {
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
// verify modeling scene is loaded
await scene.expectPixelColor(
backgroundColor,
secondEdgeLocation,
lowTolerance
)
// wait for stream to load
await scene.expectPixelColor(bodyColor, bodyLocation, highTolerance)
})
// Test 1: Command bar flow with preselected edges
await test.step(`Select first edge`, async () => {
await scene.expectPixelColor(
edgeColorWhite,
firstEdgeLocation,
lowTolerance
)
await clickOnFirstEdge()
await scene.expectPixelColor(
edgeColorYellow,
firstEdgeLocation,
highTolerance // Ubuntu color mismatch can require high tolerance
)
})
await test.step(`Apply fillet to the preselected edge`, async () => {
await page.waitForTimeout(100)
await toolbar.filletButton.click()
await cmdBar.expectState({
commandName: 'Fillet',
highlightedHeaderArg: 'selection',
currentArgKey: 'selection',
currentArgValue: '',
headerArguments: {
Selection: '',
Radius: '',
},
stage: 'arguments',
})
await cmdBar.progressCmdBar()
await cmdBar.expectState({
commandName: 'Fillet',
highlightedHeaderArg: 'radius',
currentArgKey: 'radius',
currentArgValue: '5',
headerArguments: {
Selection: '1 face',
Radius: '',
},
stage: 'arguments',
})
await cmdBar.progressCmdBar()
await cmdBar.expectState({
commandName: 'Fillet',
headerArguments: {
Selection: '1 face',
Radius: '5',
},
stage: 'review',
})
await cmdBar.progressCmdBar()
})
await test.step(`Confirm code is added to the editor`, async () => {
await editor.expectEditor.toContain(firstFilletDeclaration)
await editor.expectState({
diagnostics: [],
activeLines: ['|>fillet({radius=5,tags=[seg01]},%)'],
highlightedCode: '',
})
})
await test.step(`Confirm scene has changed`, async () => {
await scene.expectPixelColor(filletColor, firstEdgeLocation, lowTolerance)
})
// Test 2: Command bar flow without preselected edges
await test.step(`Open fillet UI without selecting edges`, async () => {
await page.waitForTimeout(100)
await toolbar.filletButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'selection',
currentArgValue: '',
headerArguments: {
Selection: '',
Radius: '',
},
highlightedHeaderArg: 'selection',
commandName: 'Fillet',
})
})
await test.step(`Select second edge`, async () => {
await scene.expectPixelColor(
edgeColorWhite,
secondEdgeLocation,
lowTolerance
)
await clickOnSecondEdge()
await scene.expectPixelColor(
edgeColorYellow,
secondEdgeLocation,
highTolerance // Ubuntu color mismatch can require high tolerance
)
})
await test.step(`Apply fillet to the second edge`, async () => {
await cmdBar.expectState({
commandName: 'Fillet',
highlightedHeaderArg: 'selection',
currentArgKey: 'selection',
currentArgValue: '',
headerArguments: {
Selection: '',
Radius: '',
},
stage: 'arguments',
})
await cmdBar.progressCmdBar()
await cmdBar.expectState({
commandName: 'Fillet',
highlightedHeaderArg: 'radius',
currentArgKey: 'radius',
currentArgValue: '5',
headerArguments: {
Selection: '1 sweepEdge',
Radius: '',
},
stage: 'arguments',
})
await cmdBar.progressCmdBar()
await cmdBar.expectState({
commandName: 'Fillet',
headerArguments: {
Selection: '1 sweepEdge',
Radius: '5',
},
stage: 'review',
})
await cmdBar.progressCmdBar()
})
await test.step(`Confirm code is added to the editor`, async () => {
await editor.expectEditor.toContain(secondFilletDeclaration)
await editor.expectState({
diagnostics: [],
activeLines: ['radius=5,'],
highlightedCode: '',
})
})
await test.step(`Confirm scene has changed`, async () => {
await scene.expectPixelColor(
backgroundColor,
secondEdgeLocation,
lowTolerance
)
})
})
test(`Chamfer point-and-click`, async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
// Code samples
const initialCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-12, -6], %)
|> line([0, 12], %)
|> line([24, 0], %)
|> line([0, -12], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(-12, sketch001)
`
const firstChamferDeclaration = 'chamfer({ length = 5, tags = [seg01] }, %)'
const secondChamferDeclaration =
'chamfer({ length = 5, tags = [getOppositeEdge(seg01)] }, %)'
// Locators
const firstEdgeLocation = { x: 600, y: 193 }
const secondEdgeLocation = { x: 600, y: 383 }
const bodyLocation = { x: 630, y: 290 }
const [clickOnFirstEdge] = scene.makeMouseHelpers(
firstEdgeLocation.x,
firstEdgeLocation.y
)
const [clickOnSecondEdge] = scene.makeMouseHelpers(
secondEdgeLocation.x,
secondEdgeLocation.y
)
// Colors
const edgeColorWhite: [number, number, number] = [248, 248, 248]
const edgeColorYellow: [number, number, number] = [251, 251, 40] // Mac:B=67 Ubuntu:B=12
const bodyColor: [number, number, number] = [155, 155, 155]
const chamferColor: [number, number, number] = [168, 168, 168]
const backgroundColor: [number, number, number] = [30, 30, 30]
const lowTolerance = 20
const highTolerance = 40
// Setup
await test.step(`Initial test setup`, async () => {
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
// verify modeling scene is loaded
await scene.expectPixelColor(
backgroundColor,
secondEdgeLocation,
lowTolerance
)
// wait for stream to load
await scene.expectPixelColor(bodyColor, bodyLocation, highTolerance)
})
// Test 1: Command bar flow with preselected edges
await test.step(`Select first edge`, async () => {
await scene.expectPixelColor(
edgeColorWhite,
firstEdgeLocation,
lowTolerance
)
await clickOnFirstEdge()
await scene.expectPixelColor(
edgeColorYellow,
firstEdgeLocation,
highTolerance // Ubuntu color mismatch can require high tolerance
)
})
await test.step(`Apply chamfer to the preselected edge`, async () => {
await page.waitForTimeout(100)
await toolbar.chamferButton.click()
await cmdBar.expectState({
commandName: 'Chamfer',
highlightedHeaderArg: 'selection',
currentArgKey: 'selection',
currentArgValue: '',
headerArguments: {
Selection: '',
Length: '',
},
stage: 'arguments',
})
await cmdBar.progressCmdBar()
await cmdBar.expectState({
commandName: 'Chamfer',
highlightedHeaderArg: 'length',
currentArgKey: 'length',
currentArgValue: '5',
headerArguments: {
Selection: '1 face',
Length: '',
},
stage: 'arguments',
})
await cmdBar.progressCmdBar()
await cmdBar.expectState({
commandName: 'Chamfer',
headerArguments: {
Selection: '1 face',
Length: '5',
},
stage: 'review',
})
await cmdBar.progressCmdBar()
})
await test.step(`Confirm code is added to the editor`, async () => {
await editor.expectEditor.toContain(firstChamferDeclaration)
await editor.expectState({
diagnostics: [],
activeLines: ['|>chamfer({length=5,tags=[seg01]},%)'],
highlightedCode: '',
})
})
await test.step(`Confirm scene has changed`, async () => {
await scene.expectPixelColor(chamferColor, firstEdgeLocation, lowTolerance)
})
// Test 2: Command bar flow without preselected edges
await test.step(`Open chamfer UI without selecting edges`, async () => {
await page.waitForTimeout(100)
await toolbar.chamferButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'selection',
currentArgValue: '',
headerArguments: {
Selection: '',
Length: '',
},
highlightedHeaderArg: 'selection',
commandName: 'Chamfer',
})
})
await test.step(`Select second edge`, async () => {
await scene.expectPixelColor(
edgeColorWhite,
secondEdgeLocation,
lowTolerance
)
await clickOnSecondEdge()
await scene.expectPixelColor(
edgeColorYellow,
secondEdgeLocation,
highTolerance // Ubuntu color mismatch can require high tolerance
)
})
await test.step(`Apply chamfer to the second edge`, async () => {
await cmdBar.expectState({
commandName: 'Chamfer',
highlightedHeaderArg: 'selection',
currentArgKey: 'selection',
currentArgValue: '',
headerArguments: {
Selection: '',
Length: '',
},
stage: 'arguments',
})
await cmdBar.progressCmdBar()
await cmdBar.expectState({
commandName: 'Chamfer',
highlightedHeaderArg: 'length',
currentArgKey: 'length',
currentArgValue: '5',
headerArguments: {
Selection: '1 sweepEdge',
Length: '',
},
stage: 'arguments',
})
await cmdBar.progressCmdBar()
await cmdBar.expectState({
commandName: 'Chamfer',
headerArguments: {
Selection: '1 sweepEdge',
Length: '5',
},
stage: 'review',
})
await cmdBar.progressCmdBar()
})
await test.step(`Confirm code is added to the editor`, async () => {
await editor.expectEditor.toContain(secondChamferDeclaration)
await editor.expectState({
diagnostics: [],
activeLines: ['length=5,'],
highlightedCode: '',
})
})
await test.step(`Confirm scene has changed`, async () => {
await scene.expectPixelColor(
backgroundColor,
secondEdgeLocation,
lowTolerance
)
})
})
const shellPointAndClickCapCases = [
{ shouldPreselect: true },
{ shouldPreselect: false },
@ -1566,7 +1085,6 @@ shellPointAndClickCapCases.forEach(({ shouldPreselect }) => {
await clickOnCap()
await page.waitForTimeout(500)
await cmdBar.progressCmdBar()
await page.waitForTimeout(500)
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
@ -1587,7 +1105,6 @@ shellPointAndClickCapCases.forEach(({ shouldPreselect }) => {
await test.step(`Go through the command bar flow with a preselected face (cap)`, async () => {
await toolbar.shellButton.click()
await cmdBar.progressCmdBar()
await page.waitForTimeout(500)
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
@ -1669,7 +1186,6 @@ extrude001 = extrude(40, sketch001)
await page.waitForTimeout(500)
await page.keyboard.up('Shift')
await cmdBar.progressCmdBar()
await page.waitForTimeout(500)
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
@ -1793,61 +1309,3 @@ shellSketchOnFacesCases.forEach((initialCode, index) => {
})
})
})
test(`Shell dry-run validation rejects sweeps`, async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
const initialCode = `sketch001 = startSketchOn('YZ')
|> circle({
center = [0, 0],
radius = 500
}, %)
sketch002 = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> xLine(-2000, %)
sweep001 = sweep({ path = sketch002 }, sketch001)
`
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
// One dumb hardcoded screen pixel value
const testPoint = { x: 500, y: 250 }
const [clickOnSweep] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
await test.step(`Confirm sweep exists`, async () => {
await toolbar.closePane('code')
await scene.expectPixelColor([231, 231, 231], testPoint, 15)
})
await test.step(`Go through the Shell flow and fail validation with a toast`, async () => {
await toolbar.shellButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'selection',
currentArgValue: '',
headerArguments: {
Selection: '',
Thickness: '',
},
highlightedHeaderArg: 'selection',
commandName: 'Shell',
})
await clickOnSweep()
await page.waitForTimeout(500)
await cmdBar.progressCmdBar()
await expect(
page.getByText('Unable to shell with the current selection. Reason:')
).toBeVisible()
await page.waitForTimeout(1000)
})
})

View File

@ -172,7 +172,7 @@ test(
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
await expect(page.getByText('broken-code')).toBeVisible()
await expect(page.getByText('bracket')).toBeVisible()
await expect(page.getByText('Create project')).toBeVisible()
await expect(page.getByText('New Project')).toBeVisible()
})
await test.step('opening broken code project should clear the scene and show the error', async () => {
// Go back home.
@ -253,7 +253,7 @@ test(
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
await expect(page.getByText('empty')).toBeVisible()
await expect(page.getByText('bracket')).toBeVisible()
await expect(page.getByText('Create project')).toBeVisible()
await expect(page.getByText('New Project')).toBeVisible()
})
await test.step('opening empty code project should clear the scene', async () => {
// Go back home.
@ -985,126 +985,6 @@ test.describe(`Project management commands`, () => {
})
}
)
test(`Create a new project with a colliding name`, async ({
context,
homePage,
toolbar,
cmdBar,
}) => {
const projectName = 'test-project'
await test.step(`Setup`, async () => {
await context.folderSetupFn(async (dir) => {
const projectDir = path.join(dir, projectName)
await Promise.all([fsp.mkdir(projectDir, { recursive: true })])
await Promise.all([
fsp.copyFile(
executorInputPath('router-template-slate.kcl'),
path.join(projectDir, 'main.kcl')
),
])
})
await homePage.expectState({
projectCards: [
{
title: projectName,
fileCount: 1,
},
],
sortBy: 'last-modified-desc',
})
})
await test.step('Create a new project with the same name', async () => {
await cmdBar.openCmdBar()
await cmdBar.chooseCommand('create project')
await cmdBar.expectState({
stage: 'arguments',
commandName: 'Create project',
currentArgKey: 'name',
currentArgValue: '',
headerArguments: {
Name: '',
},
highlightedHeaderArg: 'name',
})
await cmdBar.argumentInput.fill(projectName)
await cmdBar.progressCmdBar()
})
await test.step(`Check the project was created with a non-colliding name`, async () => {
await toolbar.logoLink.click()
await homePage.expectState({
projectCards: [
{
title: projectName + '-1',
fileCount: 1,
},
{
title: projectName,
fileCount: 1,
},
],
sortBy: 'last-modified-desc',
})
})
await test.step('Create another project with the same name', async () => {
await cmdBar.openCmdBar()
await cmdBar.chooseCommand('create project')
await cmdBar.expectState({
stage: 'arguments',
commandName: 'Create project',
currentArgKey: 'name',
currentArgValue: '',
headerArguments: {
Name: '',
},
highlightedHeaderArg: 'name',
})
await cmdBar.argumentInput.fill(projectName)
await cmdBar.progressCmdBar()
})
await test.step(`Check the second project was created with a non-colliding name`, async () => {
await toolbar.logoLink.click()
await homePage.expectState({
projectCards: [
{
title: projectName + '-2',
fileCount: 1,
},
{
title: projectName + '-1',
fileCount: 1,
},
{
title: projectName,
fileCount: 1,
},
],
sortBy: 'last-modified-desc',
})
})
})
})
test(`Create a few projects using the default project name`, async ({
homePage,
toolbar,
}) => {
for (let i = 0; i < 12; i++) {
await test.step(`Create project ${i}`, async () => {
await homePage.expectState({
projectCards: Array.from({ length: i }, (_, i) => ({
title: `project-${i.toString().padStart(3, '0')}`,
fileCount: 1,
})).toReversed(),
sortBy: 'last-modified-desc',
})
await homePage.createAndGoToProject()
await toolbar.logoLink.click()
})
}
})
test(
@ -1511,7 +1391,7 @@ extrude001 = extrude(200, sketch001)`)
await page.getByTestId('app-logo').click()
await expect(
page.getByRole('button', { name: 'Create project' })
page.getByRole('button', { name: 'New project' })
).toBeVisible()
for (let i = 1; i <= 10; i++) {
@ -1525,7 +1405,7 @@ extrude001 = extrude(200, sketch001)`)
test(
'Opening a project should successfully load the stream, (regression test that this also works when switching between projects)',
{ tag: '@electron' },
async ({ context, page, cmdBar, homePage }, testInfo) => {
async ({ context, page }, testInfo) => {
await context.folderSetupFn(async (dir) => {
await Promise.all([
fsp.mkdir(path.join(dir, 'router-template-slate'), { recursive: true }),
@ -1563,38 +1443,11 @@ test(
const pointOnModel = { x: 630, y: 280 }
await test.step('Opening the bracket project via command palette should load the stream', async () => {
await homePage.expectState({
projectCards: [
{
title: 'bracket',
fileCount: 1,
},
{
title: 'router-template-slate',
fileCount: 1,
},
],
sortBy: 'last-modified-desc',
})
await test.step('Opening the bracket project should load the stream', async () => {
// expect to see the text bracket
await expect(page.getByText('bracket')).toBeVisible()
await cmdBar.openCmdBar()
await cmdBar.chooseCommand('open project')
await cmdBar.expectState({
stage: 'arguments',
commandName: 'Open project',
currentArgKey: 'name',
currentArgValue: '',
headerArguments: {
Name: '',
},
highlightedHeaderArg: 'name',
})
await cmdBar.argumentInput.fill('brac')
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'commandBarClosed',
})
await page.getByText('bracket').click()
await u.waitForPageLoad()
@ -1612,10 +1465,10 @@ test(
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
await expect(page.getByText('router-template-slate')).toBeVisible()
await expect(page.getByText('Create project')).toBeVisible()
await expect(page.getByText('New Project')).toBeVisible()
})
await test.step('Opening the router-template project via link should load the stream', async () => {
await test.step('Opening the router-template project should load the stream', async () => {
// expect to see the text bracket
await expect(page.getByText('router-template-slate')).toBeVisible()
@ -1632,26 +1485,16 @@ test(
.toBeLessThan(15)
})
await test.step('The projects on the home page should still be normal', async () => {
await test.step('Opening the router-template project should load the stream', async () => {
await page.getByTestId('project-sidebar-toggle').click()
await expect(
page.getByRole('button', { name: 'Go to Home' })
).toBeVisible()
await page.getByRole('button', { name: 'Go to Home' }).click()
await homePage.expectState({
projectCards: [
{
title: 'bracket',
fileCount: 1,
},
{
title: 'router-template-slate',
fileCount: 1,
},
],
sortBy: 'last-modified-desc',
})
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
await expect(page.getByText('router-template-slate')).toBeVisible()
await expect(page.getByText('New Project')).toBeVisible()
})
}
)

View File

@ -34,7 +34,7 @@ test.describe('Sketch tests', () => {
screwRadius = 3
wireRadius = 2
wireOffset = 0.5
screwHole = startSketchOn('XY')
${startProfileAt1}
|> arc({
@ -42,7 +42,7 @@ test.describe('Sketch tests', () => {
angleStart = 0,
angleEnd = 360
}, %)
part001 = startSketchOn('XY')
${startProfileAt2}
|> xLine(width * .5, %)
@ -51,7 +51,7 @@ test.describe('Sketch tests', () => {
|> close(%)
|> hole(screwHole, %)
|> extrude(thickness, %)
part002 = startSketchOn('-XZ')
${startProfileAt3}
|> xLine(width / 4, %)
@ -99,7 +99,6 @@ test.describe('Sketch tests', () => {
test('Can delete most of a sketch and the line tool will still work', async ({
page,
homePage,
scene,
}) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
@ -113,13 +112,12 @@ test.describe('Sketch tests', () => {
})
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
await expect(async () => {
await page.getByText('tangentialArcTo([24.95, -5.38], %)').click()
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).toBeEnabled({ timeout: 2000 })
).toBeEnabled({ timeout: 1000 })
await page.getByRole('button', { name: 'Edit Sketch' }).click()
}).toPass({ timeout: 40_000, intervals: [1_000] })
@ -1065,7 +1063,7 @@ test.describe('Sketch tests', () => {
`lugHeadLength = 0.25
lugDiameter = 0.5
lugLength = 2
fn lug = (origin, length, diameter, plane) => {
lugSketch = startSketchOn(plane)
|> startProfileAt([origin[0] + lugDiameter / 2, origin[1]], %)
@ -1074,10 +1072,10 @@ test.describe('Sketch tests', () => {
|> yLineTo(0, %)
|> close(%)
|> revolve({ axis = "Y" }, %)
return lugSketch
}
lug([0, 0], 10, .5, "XY")`
)
})
@ -1129,14 +1127,14 @@ test.describe('Sketch tests', () => {
`fn in2mm = (inches) => {
return inches * 25.4
}
const railTop = in2mm(.748)
const railSide = in2mm(.024)
const railBaseWidth = in2mm(.612)
const railWideWidth = in2mm(.835)
const railBaseLength = in2mm(.200)
const railClampable = in2mm(.200)
const rail = startSketchOn('XZ')
|> startProfileAt([
-railTop / 2,
@ -1407,46 +1405,3 @@ test.describe(`Click based selection don't brick the app when clicked out of ran
})
})
})
// Regression test for https://github.com/KittyCAD/modeling-app/issues/4372
test.describe('Redirecting to home page and back to the original file should clear sketch DOM elements', () => {
test('Can redirect to home page and back to original file and have a cleared DOM', async ({
context,
page,
scene,
toolbar,
editor,
homePage,
}) => {
// We seed the scene with a single offset plane
await context.addInitScript(() => {
localStorage.setItem(
'persistCode',
` sketch001 = startSketchOn('XZ')
|> startProfileAt([256.85, 14.41], %)
|> lineTo([0, 211.07], %)
`
)
})
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
const [objClick] = scene.makeMouseHelpers(634, 274)
await objClick()
// Enter sketch mode
await toolbar.editSketch()
await expect(page.getByText('323.49')).toBeVisible()
// Open navigation side bar
await page.getByTestId('project-sidebar-toggle').click()
const goToHome = page.getByRole('button', {
name: 'Go to Home',
})
await goToHome.click()
await homePage.openProject('testDefault')
await expect(page.getByText('323.49')).not.toBeVisible()
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 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: 60 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 128 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: 54 KiB

After

Width:  |  Height:  |  Size: 53 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: 53 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -1078,7 +1078,7 @@ export async function createProject({
returnHome?: boolean
}) {
await test.step(`Create project and navigate to it`, async () => {
await page.getByRole('button', { name: 'Create project' }).click()
await page.getByRole('button', { name: 'New project' }).click()
await page.getByRole('textbox', { name: 'Name' }).fill(name)
await page.getByRole('button', { name: 'Continue' }).click()

View File

@ -8,8 +8,8 @@ import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
test.describe('Testing in-app sample loading', () => {
/**
* Note this test implicitly depends on the KCL sample "a-parametric-bearing-pillow-block",
* its title, and its units settings. https://github.com/KittyCAD/kcl-samples/blob/main/a-parametric-bearing-pillow-block/main.kcl
* Note this test implicitly depends on the KCL sample "car-wheel.kcl",
* its title, and its units settings. https://github.com/KittyCAD/kcl-samples/blob/main/car-wheel/car-wheel.kcl
*/
test('Web: should overwrite current code, cannot create new file', async ({
editor,
@ -29,8 +29,8 @@ test.describe('Testing in-app sample loading', () => {
// Locators and constants
const newSample = {
file: 'a-parametric-bearing-pillow-block' + FILE_EXT,
title: 'A Parametric Bearing Pillow Block',
file: 'car-wheel' + FILE_EXT,
title: 'Car Wheel',
}
const commandBarButton = page.getByRole('button', { name: 'Commands' })
const samplesCommandOption = page.getByRole('option', {
@ -69,13 +69,14 @@ test.describe('Testing in-app sample loading', () => {
await confirmButton.click()
await editor.expectEditor.toContain('// ' + newSample.title)
await expect(unitsToast('in')).toBeVisible()
})
})
/**
* Note this test implicitly depends on the KCL samples:
* "a-parametric-bearing-pillow-block": https://github.com/KittyCAD/kcl-samples/blob/main/a-parametric-bearing-pillow-block/main.kcl
* "gear-rack": https://github.com/KittyCAD/kcl-samples/blob/main/gear-rack/main.kcl
* "car-wheel.kcl": https://github.com/KittyCAD/kcl-samples/blob/main/car-wheel/car-wheel.kcl
* "gear-rack.kcl": https://github.com/KittyCAD/kcl-samples/blob/main/gear-rack/gear-rack.kcl
*/
test(
'Desktop: should create new file by default, optionally overwrite',
@ -92,8 +93,8 @@ test.describe('Testing in-app sample loading', () => {
// Locators and constants
const sampleOne = {
file: 'a-parametric-bearing-pillow-block' + FILE_EXT,
title: 'A Parametric Bearing Pillow Block',
file: 'car-wheel' + FILE_EXT,
title: 'Car Wheel',
}
const sampleTwo = {
file: 'gear-rack' + FILE_EXT,
@ -157,6 +158,7 @@ test.describe('Testing in-app sample loading', () => {
await editor.expectEditor.toContain('// ' + sampleOne.title)
await expect(newlyCreatedFile(sampleOne.file)).toBeVisible()
await expect(projectMenuButton).toContainText(sampleOne.file)
await expect(unitsToast('in')).toBeVisible()
})
await test.step(`Now overwrite the current file`, async () => {
@ -186,6 +188,7 @@ test.describe('Testing in-app sample loading', () => {
await expect(newlyCreatedFile(sampleOne.file)).toBeVisible()
await expect(newlyCreatedFile(sampleTwo.file)).not.toBeVisible()
await expect(projectMenuButton).toContainText(sampleOne.file)
await expect(unitsToast('mm')).toBeVisible()
})
}
)

View File

@ -906,6 +906,53 @@ test.describe('Testing selections', () => {
).not.toBeDisabled()
})
test('Fillet button states test', async ({ page, homePage }) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`sketch001 = startSketchOn('XZ')
|> startProfileAt([-5, -5], %)
|> line([0, 10], %)
|> line([10, 0], %)
|> line([0, -10], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`
)
})
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
const selectSegment = () => page.getByText(`line([10, 0], %)`).click()
const selectClose = () => page.getByText(`close(%)`).click()
const clickEmpty = () => page.mouse.click(950, 100)
// Now that we don't disable toolbar buttons based on selection,
// but rather based on a "selection" step in the command palette,
// the fillet button should always be enabled with a good network connection.
// I'm not sure if this test is actually useful anymore.
await selectSegment()
await expect(page.getByRole('button', { name: 'Fillet' })).toBeEnabled()
await clickEmpty()
await expect(page.getByRole('button', { name: 'Fillet' })).toBeEnabled()
// test fillet button with the body in the scene
const codeToAdd = `${await u.codeLocator.allInnerTexts()}
extrude001 = extrude(10, sketch001)`
await u.codeLocator.clear()
await u.codeLocator.fill(codeToAdd)
await selectSegment()
await expect(page.getByRole('button', { name: 'Fillet' })).toBeEnabled()
await selectClose()
await expect(page.getByRole('button', { name: 'Fillet' })).toBeEnabled()
await clickEmpty()
await expect(page.getByRole('button', { name: 'Fillet' })).toBeEnabled()
})
const removeAfterFirstParenthesis = (inputString: string) => {
const index = inputString.indexOf('(')
if (index !== -1) {

View File

@ -65,7 +65,7 @@
"vscode-languageserver-protocol": "^3.17.5",
"vscode-uri": "^3.0.8",
"web-vitals": "^3.5.2",
"xstate": "^5.19.2",
"xstate": "^5.17.4",
"yargs": "^17.7.2"
},
"scripts": {
@ -113,9 +113,9 @@
"test:unit": "vitest run --mode development --exclude **/kclSamples.test.ts",
"test:unit:kcl-samples": "vitest run --mode development ./src/lang/kclSamples.test.ts",
"test:playwright:electron": "playwright test --config=playwright.electron.config.ts --grep-invert='@snapshot'",
"test:playwright:electron:windows": "playwright test --config=playwright.electron.config.ts --grep-invert=\"@skipWin|@snapshot\" --quiet",
"test:playwright:electron:macos": "playwright test --config=playwright.electron.config.ts --grep-invert='@skipMacos|@snapshot' --quiet",
"test:playwright:electron:ubuntu": "playwright test --config=playwright.electron.config.ts --grep-invert='@skipLinux|@snapshot' --quiet",
"test:playwright:electron:windows": "playwright test --config=playwright.electron.config.ts --grep-invert=\"@skipWin|@snapshot\"",
"test:playwright:electron:macos": "playwright test --config=playwright.electron.config.ts --grep-invert='@skipMacos|@snapshot'",
"test:playwright:electron:ubuntu": "playwright test --config=playwright.electron.config.ts --grep-invert='@skipLinux|@snapshot'",
"test:playwright:electron:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@snapshot'",
"test:playwright:electron:windows:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert=\"@skipWin|@snapshot\"",
"test:playwright:electron:macos:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@skipMacos|@snapshot'",
@ -149,11 +149,12 @@
"@electron-forge/plugin-vite": "7.4.0",
"@electron/fuses": "1.8.0",
"@iarna/toml": "^2.2.5",
"@lezer/generator": "^1.7.2",
"@lezer/generator": "^1.7.1",
"@nabla/vite-plugin-eslint": "^2.0.5",
"@playwright/test": "^1.49.0",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^15.0.2",
"@types/d3-force": "^3.0.10",
"@types/diff": "^6.0.0",
"@types/electron": "^1.6.10",
"@types/isomorphic-fetch": "^0.0.39",
@ -174,6 +175,7 @@
"@vitest/web-worker": "^1.5.0",
"@xstate/cli": "^0.5.17",
"autoprefixer": "^10.4.19",
"d3-force": "^3.0.0",
"electron": "32.1.2",
"electron-builder": "24.13.3",
"electron-notarize": "1.2.2",
@ -181,11 +183,8 @@
"eslint-plugin-css-modules": "^2.12.0",
"eslint-plugin-import": "^2.30.0",
"eslint-plugin-jest": "^28.10.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.3",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-suggest-no-throw": "^1.0.0",
"eslint-plugin-testing-library": "^7.1.1",
"happy-dom": "^16.3.0",
"http-server": "^14.1.1",
"husky": "^9.1.5",
@ -199,9 +198,9 @@
"setimmediate": "^1.0.5",
"tailwindcss": "^3.4.1",
"ts-node": "^10.0.0",
"typescript": "^5.7.3",
"typescript": "^5.7.2",
"typescript-eslint": "^8.19.1",
"vite": "^5.4.12",
"vite": "^5.4.6",
"vite-plugin-package-version": "^1.1.0",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.6.0",

View File

@ -4,5 +4,4 @@ dist
tsconfig.tsbuildinfo
*.d.ts
*.js
!postcss.config.js
!rollup.config.js

View File

@ -28,7 +28,6 @@
"@rollup/plugin-typescript": "^12.1.2",
"rollup": "^4.29.1",
"rollup-plugin-dts": "^6.1.1",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^2.1.8"
},
"files": [

View File

@ -1 +0,0 @@
// This is here to prevent using the one in the root of the project.

View File

@ -398,7 +398,7 @@ check-error@^2.1.1:
resolved "https://registry.yarnpkg.com/check-error/-/check-error-2.1.1.tgz#87eb876ae71ee388fa0471fe423f494be1d96ccc"
integrity sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==
debug@^4.1.1, debug@^4.3.7:
debug@^4.3.7:
version "4.4.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"
integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==
@ -471,11 +471,6 @@ function-bind@^1.1.2:
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
globrex@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098"
integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==
hasown@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
@ -652,11 +647,6 @@ tinyspy@^3.0.2:
resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-3.0.2.tgz#86dd3cf3d737b15adcf17d7887c84a75201df20a"
integrity sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==
tsconfck@^3.0.3:
version "3.1.4"
resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-3.1.4.tgz#de01a15334962e2feb526824339b51be26712229"
integrity sha512-kdqWFGVJqe+KGYvlSO9NIaWn9jT1Ny4oKVzAJsKii5eoE9snzTJzL4+MMVOMn+fikWGFmKEylcXL710V/kIPJQ==
typescript@^5.7.2:
version "5.7.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6"
@ -673,19 +663,10 @@ vite-node@2.1.8:
pathe "^1.1.2"
vite "^5.0.0"
vite-tsconfig-paths@^4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz#321f02e4b736a90ff62f9086467faf4e2da857a9"
integrity sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==
dependencies:
debug "^4.1.1"
globrex "^0.1.2"
tsconfck "^3.0.3"
vite@^5.0.0:
version "5.4.14"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.14.tgz#ff8255edb02134df180dcfca1916c37a6abe8408"
integrity sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==
version "5.4.11"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.11.tgz#3b415cd4aed781a356c1de5a9ebafb837715f6e5"
integrity sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==
dependencies:
esbuild "^0.21.3"
postcss "^8.4.43"

View File

@ -29,7 +29,7 @@
"vscode-uri": "^3.0.8"
},
"devDependencies": {
"@types/node": "^22.10.6",
"@types/node": "^20.14.9",
"ts-node": "^10.9.2"
}
}

View File

@ -109,12 +109,12 @@
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9"
integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==
"@types/node@^22.10.6":
version "22.10.6"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.6.tgz#5c6795e71635876039f853cbccd59f523d9e4239"
integrity sha512-qNiuwC4ZDAUNcY47xgaSuS92cjf8JbSUoaKS77bmLG1rU7MlATVSiw/IlrjtIyyskXBZ8KkNfjK/P5na7rgXbQ==
"@types/node@^20.14.9":
version "20.14.9"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.9.tgz#12e8e765ab27f8c421a1820c99f5f313a933b420"
integrity sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg==
dependencies:
undici-types "~6.20.0"
undici-types "~5.26.4"
acorn-walk@^8.1.1:
version "8.3.3"
@ -187,10 +187,10 @@ typescript@^5.7.2:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6"
integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==
undici-types@~6.20.0:
version "6.20.0"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433"
integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==
undici-types@~5.26.4:
version "5.26.5"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
v8-compile-cache-lib@^3.0.1:
version "3.0.1"

View File

@ -1,212 +1,172 @@
[
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "80-20-rail/main.kcl",
"multipleFiles": false,
"file": "80-20-rail.kcl",
"title": "80/20 Rail",
"description": "An 80/20 extruded aluminum linear rail. T-slot profile adjustable by profile height, rail length, and origin position"
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "a-parametric-bearing-pillow-block/main.kcl",
"multipleFiles": false,
"file": "a-parametric-bearing-pillow-block.kcl",
"title": "A Parametric Bearing Pillow Block",
"description": "A bearing pillow block, also known as a plummer block or pillow block bearing, is a pedestal used to provide support for a rotating shaft with the help of compatible bearings and various accessories. Housing a bearing, the pillow block provides a secure and stable foundation that allows the shaft to rotate smoothly within its machinery setup. These components are essential in a wide range of mechanical systems and machinery, playing a key role in reducing friction and supporting radial and axial loads."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "ball-bearing/main.kcl",
"multipleFiles": false,
"file": "ball-bearing.kcl",
"title": "Ball Bearing",
"description": "A ball bearing is a type of rolling-element bearing that uses balls to maintain the separation between the bearing races. The primary purpose of a ball bearing is to reduce rotational friction and support radial and axial loads."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "bracket/main.kcl",
"multipleFiles": false,
"file": "bracket.kcl",
"title": "Shelf Bracket",
"description": "This is a bracket that holds a shelf. It is made of aluminum and is designed to hold a force of 300 lbs. The bracket is 6 inches wide and the force is applied at the end of the shelf, 12 inches from the wall. The bracket has a factor of safety of 1.2. The legs of the bracket are 5 inches and 2 inches long. The thickness of the bracket is calculated from the constraints provided."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "car-wheel-assembly/main.kcl",
"multipleFiles": true,
"file": "brake-caliper.kcl",
"title": "Brake Caliper",
"description": "Brake calipers are used to squeeze the brake pads against the rotor, causing larger and larger amounts of friction depending on how hard the brakes are pressed."
},
{
"file": "car-wheel.kcl",
"title": "Car Wheel",
"description": "A sports car wheel with a circular lug pattern and spokes."
},
{
"file": "car-wheel-assembly.kcl",
"title": "Car Wheel Assembly",
"description": "A car wheel assembly with a rotor, tire, and lug nuts."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "dodecahedron/main.kcl",
"multipleFiles": false,
"file": "dodecahedron.kcl",
"title": "Hollow Dodecahedron",
"description": "A regular dodecahedron or pentagonal dodecahedron is a dodecahedron composed of regular pentagonal faces, three meeting at each vertex. This example shows constructing the individual faces of the dodecahedron and extruding inwards."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "enclosure/main.kcl",
"multipleFiles": false,
"file": "enclosure.kcl",
"title": "Enclosure",
"description": "An enclosure body and sealing lid for storing items"
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "flange-with-patterns/main.kcl",
"multipleFiles": false,
"file": "flange-with-patterns.kcl",
"title": "Flange",
"description": "A flange is a flat rim, collar, or rib, typically forged or cast, that is used to strengthen an object, guide it, or attach it to another object. Flanges are known for their use in various applications, including piping, plumbing, and mechanical engineering, among others."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "flange-xy/main.kcl",
"multipleFiles": false,
"file": "flange-xy.kcl",
"title": "Flange with XY coordinates",
"description": "A flange is a flat rim, collar, or rib, typically forged or cast, that is used to strengthen an object, guide it, or attach it to another object. Flanges are known for their use in various applications, including piping, plumbing, and mechanical engineering, among others."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "focusrite-scarlett-mounting-bracket/main.kcl",
"multipleFiles": false,
"file": "focusrite-scarlett-mounting-bracket.kcl",
"title": "A mounting bracket for the Focusrite Scarlett Solo audio interface",
"description": "This is a bracket that holds an audio device underneath a desk or shelf. The audio device has dimensions of 144mm wide, 80mm length and 45mm depth with fillets of 6mm. This mounting bracket is designed to be 3D printed with PLA material"
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "food-service-spatula/main.kcl",
"multipleFiles": false,
"file": "food-service-spatula.kcl",
"title": "Food Service Spatula",
"description": "Use these spatulas for mixing, flipping, and scraping."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "french-press/main.kcl",
"multipleFiles": false,
"file": "french-press.kcl",
"title": "French Press",
"description": "A french press immersion coffee maker"
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "gear/main.kcl",
"multipleFiles": false,
"file": "gear.kcl",
"title": "Spur Gear",
"description": "A rotating machine part having cut teeth or, in the case of a cogwheel, inserted teeth (called cogs), which mesh with another toothed part to transmit torque. Geared devices can change the speed, torque, and direction of a power source. The two elements that define a gear are its circular shape and the teeth that are integrated into its outer edge, which are designed to fit into the teeth of another gear."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "gear-rack/main.kcl",
"multipleFiles": false,
"file": "gear-rack.kcl",
"title": "100mm Gear Rack",
"description": "A flat bar or rail that is engraved with teeth along its length. These teeth are designed to mesh with the teeth of a gear, known as a pinion. When the pinion, a small cylindrical gear, rotates, its teeth engage with the teeth on the rack, causing the rack to move linearly. Conversely, linear motion applied to the rack will cause the pinion to rotate."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "hex-nut/main.kcl",
"multipleFiles": false,
"file": "hex-nut.kcl",
"title": "Hex nut",
"description": "A hex nut is a type of fastener with a threaded hole and a hexagonal outer shape, used in a wide variety of applications to secure parts together. The hexagonal shape allows for a greater torque to be applied with wrenches or tools, making it one of the most common nut types in hardware."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "i-beam/main.kcl",
"multipleFiles": false,
"file": "i-beam.kcl",
"title": "I-beam",
"description": "A structural metal beam with an I shaped cross section. Often used in construction"
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "kitt/main.kcl",
"multipleFiles": false,
"file": "kitt.kcl",
"title": "Kitt",
"description": "The beloved KittyCAD mascot in a voxelized style."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "lego/main.kcl",
"multipleFiles": false,
"file": "lego.kcl",
"title": "Lego Brick",
"description": "A standard Lego brick. This is a small, plastic construction block toy that can be interlocked with other blocks to build various structures, models, and figures. There are a lot of hacks used in this code."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "mounting-plate/main.kcl",
"multipleFiles": false,
"file": "lug-nut.kcl",
"title": "Lug Nut",
"description": "lug Nuts are essential components used to create secure connections, whether for electrical purposes, like terminating wires or grounding, or for mechanical purposes, such as providing mounting points or reinforcing structural joints."
},
{
"file": "mounting-plate.kcl",
"title": "Mounting Plate",
"description": "A flat piece of material, often metal or plastic, that serves as a support or base for attaching, securing, or mounting various types of equipment, devices, or components."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "multi-axis-robot/main.kcl",
"multipleFiles": true,
"file": "multi-axis-robot.kcl",
"title": "Robot Arm",
"description": "A 4 axis robotic arm for industrial use. These machines can be used for assembly, packaging, organization of goods, and quality inspection processes"
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "pipe/main.kcl",
"multipleFiles": false,
"file": "pipe.kcl",
"title": "Pipe",
"description": "A tubular section or hollow cylinder, usually but not necessarily of circular cross-section, used mainly to convey substances that can flow."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "pipe-flange-assembly/main.kcl",
"multipleFiles": false,
"file": "pipe-flange-assembly.kcl",
"title": "Pipe and Flange Assembly",
"description": "A crucial component in various piping systems, designed to facilitate the connection, disconnection, and access to piping for inspection, cleaning, and modifications. This assembly combines pipes (long cylindrical conduits) with flanges (plate-like fittings) to create a secure yet detachable joint."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "pipe-with-bend/main.kcl",
"multipleFiles": false,
"file": "pipe-with-bend.kcl",
"title": "Pipe with bend",
"description": "A tubular section or hollow cylinder, usually but not necessarily of circular cross-section, used mainly to convey substances that can flow."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "poopy-shoe/main.kcl",
"multipleFiles": false,
"file": "poopy-shoe.kcl",
"title": "Poopy Shoe",
"description": "poop shute for bambu labs printer - optimized for printing."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "router-template-cross-bar/main.kcl",
"multipleFiles": false,
"file": "router-template-cross-bar.kcl",
"title": "Router template for a cross bar",
"description": "A guide for routing a notch into a cross bar."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "router-template-slate/main.kcl",
"multipleFiles": false,
"file": "router-template-slate.kcl",
"title": "Router template for a slate",
"description": "A guide for routing a slate for a cross bar."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "sheet-metal-bracket/main.kcl",
"multipleFiles": false,
"file": "sheet-metal-bracket.kcl",
"title": "Sheet Metal Bracket",
"description": "A component typically made from flat sheet metal through various manufacturing processes such as bending, punching, cutting, and forming. These brackets are used to support, attach, or mount other hardware components, often providing a structural or functional base for assembly."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "socket-head-cap-screw/main.kcl",
"multipleFiles": false,
"file": "socket-head-cap-screw.kcl",
"title": "Socket Head Cap Screw",
"description": "This is for a #10-24 screw that is 1.00 inches long. A socket head cap screw is a type of fastener that is widely used in a variety of applications requiring a high strength fastening solution. It is characterized by its cylindrical head and internal hexagonal drive, which allows for tightening with an Allen wrench or hex key."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "walkie-talkie/main.kcl",
"multipleFiles": true,
"title": "Walkie Talkie",
"description": "A portable, handheld two-way radio device that allows users to communicate wirelessly over short to medium distances. It operates on specific radio frequencies and features a push-to-talk button for transmitting messages, making it ideal for quick and reliable communication in outdoor, work, or emergency settings."
"file": "tire.kcl",
"title": "Tire",
"description": "A tire is a critical component of a vehicle that provides the necessary traction and grip between the car and the road. It supports the vehicle's weight and absorbs shocks from road irregularities."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "washer/main.kcl",
"multipleFiles": false,
"file": "washer.kcl",
"title": "Washer",
"description": "A small, typically disk-shaped component with a hole in the middle, used in a wide range of applications, primarily in conjunction with fasteners like bolts and screws. Washers distribute the load of a fastener across a broader area. This is especially important when the fastening surface is soft or uneven, as it helps to prevent damage to the surface and ensures the load is evenly distributed, reducing the risk of the fastener becoming loose over time."
},
{
"file": "wheel-rotor.kcl",
"title": "Wheel rotor",
"description": "A component of a disc brake system. It provides a surface for brake pads to press against, generating the friction needed to slow or stop the vehicle."
}
]

View File

@ -22,29 +22,13 @@ import Gizmo from 'components/Gizmo'
import { CoreDumpManager } from 'lib/coredump'
import { UnitsMenu } from 'components/UnitsMenu'
import { CameraProjectionToggle } from 'components/CameraProjectionToggle'
import { useCreateFileLinkQuery } from 'hooks/useCreateFileLinkQueryWatcher'
import { maybeWriteToDisk } from 'lib/telemetry'
import { commandBarActor } from 'machines/commandBarMachine'
import { useToken } from 'machines/appMachine'
maybeWriteToDisk()
.then(() => {})
.catch(() => {})
export function App() {
const { project, file } = useLoaderData() as IndexLoaderData
// Keep a lookout for a URL query string that invokes the 'import file from URL' command
useCreateFileLinkQuery((argDefaultValues) => {
commandBarActor.send({
type: 'Find and select command',
data: {
groupId: 'projects',
name: 'Import file from URL',
argDefaultValues,
},
})
})
useRefreshSettings(PATHS.FILE + 'SETTINGS')
const navigate = useNavigate()
const filePath = useAbsoluteFilePath()
@ -61,8 +45,8 @@ export function App() {
useHotKeyListener()
const { settings } = useSettingsAuthContext()
const token = useToken()
const { auth, settings } = useSettingsAuthContext()
const token = auth?.context?.token
const coreDumpManager = useMemo(
() => new CoreDumpManager(engineCommandManager, codeManager, token),

View File

@ -1,10 +1,10 @@
import { useAuthState } from 'machines/appMachine'
import Loading from './components/Loading'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
// Wrapper around protected routes, used in src/Router.tsx
export const Auth = ({ children }: React.PropsWithChildren) => {
const authState = useAuthState()
const isLoggingIn = authState.matches('checkIfLoggedIn')
const { auth } = useSettingsAuthContext()
const isLoggingIn = auth?.state.matches('checkIfLoggedIn')
return isLoggingIn ? (
<Loading>

View File

@ -31,12 +31,14 @@ import {
settingsLoader,
telemetryLoader,
} from 'lib/routeLoaders'
import { CommandBarProvider } from 'components/CommandBar/CommandBarProvider'
import SettingsAuthProvider from 'components/SettingsAuthProvider'
import LspProvider from 'components/LspProvider'
import { KclContextProvider } from 'lang/KclProvider'
import { ASK_TO_OPEN_QUERY_PARAM, BROWSER_PROJECT_NAME } from 'lib/constants'
import { BROWSER_PROJECT_NAME } from 'lib/constants'
import { CoreDumpManager } from 'lib/coredump'
import { codeManager, engineCommandManager } from 'lib/singletons'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import useHotkeyWrapper from 'lib/hotkeyWrapper'
import toast from 'react-hot-toast'
import { coreDump } from 'lang/wasm'
@ -45,8 +47,6 @@ import { AppStateProvider } from 'AppState'
import { reportRejection } from 'lib/trap'
import { RouteProvider } from 'components/RouteProvider'
import { ProjectsContextProvider } from 'components/ProjectsContextProvider'
import { OpenInDesktopAppHandler } from 'components/OpenInDesktopAppHandler'
import { useToken } from 'machines/appMachine'
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
@ -58,7 +58,7 @@ const router = createRouter([
/* Make sure auth is the outermost provider or else we will have
* inefficient re-renders, use the react profiler to see. */
element: (
<OpenInDesktopAppHandler>
<CommandBarProvider>
<RouteProvider>
<SettingsAuthProvider>
<LspProvider>
@ -74,26 +74,17 @@ const router = createRouter([
</LspProvider>
</SettingsAuthProvider>
</RouteProvider>
</OpenInDesktopAppHandler>
</CommandBarProvider>
),
errorElement: <ErrorPage />,
children: [
{
path: PATHS.INDEX,
loader: async ({ request }) => {
loader: async () => {
const onDesktop = isDesktop()
const url = new URL(request.url)
if (onDesktop) {
return redirect(PATHS.HOME + (url.search || ''))
} else {
const searchParams = new URLSearchParams(url.search)
if (!searchParams.has(ASK_TO_OPEN_QUERY_PARAM)) {
return redirect(
PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME + (url.search || '')
)
}
}
return null
return onDesktop
? redirect(PATHS.HOME)
: redirect(PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME)
},
},
{
@ -203,7 +194,8 @@ export const Router = () => {
}
function CoreDump() {
const token = useToken()
const { auth } = useSettingsAuthContext()
const token = auth?.context?.token
const coreDumpManager = useMemo(
() => new CoreDumpManager(engineCommandManager, codeManager, token),
[]

View File

@ -1,7 +1,8 @@
import { useRef, useMemo, memo, useCallback, useState } from 'react'
import { useRef, useMemo, memo } from 'react'
import { isCursorInSketchCommandRange } from 'lang/util'
import { engineCommandManager, kclManager } from 'lib/singletons'
import { useModelingContext } from 'hooks/useModelingContext'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { useNetworkContext } from 'hooks/useNetworkContext'
import { NetworkHealthState } from 'hooks/useNetworkStatus'
import { ActionButton } from 'components/ActionButton'
@ -21,19 +22,20 @@ import {
} from 'lib/toolbar'
import { isDesktop } from 'lib/isDesktop'
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { commandBarActor } from 'machines/commandBarMachine'
export function Toolbar({
className = '',
...props
}: React.HTMLAttributes<HTMLElement>) {
const { state, send, context } = useModelingContext()
const { commandBarSend } = useCommandsContext()
const iconClassName =
'group-disabled:text-chalkboard-50 !text-inherit dark:group-enabled:group-hover:!text-inherit'
const bgClassName = '!bg-transparent'
const buttonBgClassName =
'bg-chalkboard-transparent dark:bg-transparent disabled:bg-transparent dark:disabled:bg-transparent enabled:hover:bg-chalkboard-10 dark:enabled:hover:bg-chalkboard-100 pressed:!bg-primary pressed:enabled:hover:!text-chalkboard-10'
const buttonBorderClassName = '!border-transparent'
const buttonBorderClassName =
'!border-transparent hover:!border-chalkboard-20 dark:enabled:hover:!border-primary pressed:!border-primary ui-open:!border-primary'
const sketchPathId = useMemo(() => {
if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast))
@ -48,7 +50,6 @@ export function Toolbar({
const { overallState } = useNetworkContext()
const { isExecuting } = useKclContext()
const { isStreamReady } = useAppState()
const [showRichContent, setShowRichContent] = useState(false)
const disableAllButtons =
(overallState !== NetworkHealthState.Ok &&
@ -70,45 +71,12 @@ export function Toolbar({
() => ({
modelingState: state,
modelingSend: send,
commandBarSend,
sketchPathId,
}),
[state, send, commandBarActor.send, sketchPathId]
[state, send, commandBarSend, sketchPathId]
)
const tooltipContentClassName = !showRichContent
? ''
: '!text-left text-wrap !text-xs !p-0 !pb-2 flex gap-2 !max-w-none !w-72 flex-col items-stretch'
const richContentTimeout = useRef<number | null>(null)
const richContentClearTimeout = useRef<number | null>(null)
// On mouse enter, show rich content after a 1s delay
const handleMouseEnter = useCallback(() => {
// Cancel the clear timeout if it's already set
if (richContentClearTimeout.current) {
clearTimeout(richContentClearTimeout.current)
}
// Start our own timeout to show the rich content
richContentTimeout.current = window.setTimeout(() => {
setShowRichContent(true)
if (richContentClearTimeout.current) {
clearTimeout(richContentClearTimeout.current)
}
}, 1000)
}, [setShowRichContent])
// On mouse leave, clear the timeout and hide rich content
const handleMouseLeave = useCallback(() => {
// Clear the timeout to show rich content
if (richContentTimeout.current) {
clearTimeout(richContentTimeout.current)
}
// Start a timeout to hide the rich content
richContentClearTimeout.current = window.setTimeout(() => {
setShowRichContent(false)
if (richContentClearTimeout.current) {
clearTimeout(richContentClearTimeout.current)
}
}, 500)
}, [setShowRichContent])
/**
* Resolve all the callbacks and values for the current mode,
* so we don't need to worry about the other modes
@ -206,64 +174,43 @@ export function Toolbar({
status: itemConfig.status,
}))}
>
<div
className="contents"
// Mouse events do not fire on disabled buttons
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
<ActionButton
Element="button"
id={maybeIconConfig[0].id}
data-testid={maybeIconConfig[0].id}
iconStart={{
icon: maybeIconConfig[0].icon,
className: iconClassName,
bgClassName: bgClassName,
}}
className={
'!border-transparent !px-0 pressed:!text-chalkboard-10 pressed:enabled:hovered:!text-chalkboard-10 ' +
buttonBgClassName
}
aria-pressed={maybeIconConfig[0].isActive}
disabled={
disableAllButtons ||
maybeIconConfig[0].status !== 'available' ||
maybeIconConfig[0].disabled
}
name={maybeIconConfig[0].title}
// aria-description is still in ARIA 1.3 draft.
// eslint-disable-next-line jsx-a11y/aria-props
aria-description={maybeIconConfig[0].description}
onClick={() =>
maybeIconConfig[0].onClick(configCallbackProps)
}
>
<ActionButton
Element="button"
id={maybeIconConfig[0].id}
data-testid={maybeIconConfig[0].id}
iconStart={{
icon: maybeIconConfig[0].icon,
className: iconClassName,
bgClassName: bgClassName,
}}
className={
'!border-transparent !px-0 pressed:!text-chalkboard-10 pressed:enabled:hovered:!text-chalkboard-10 ' +
buttonBgClassName
}
aria-pressed={maybeIconConfig[0].isActive}
disabled={
disableAllButtons ||
maybeIconConfig[0].status !== 'available' ||
maybeIconConfig[0].disabled
}
name={maybeIconConfig[0].title}
// aria-description is still in ARIA 1.3 draft.
// eslint-disable-next-line jsx-a11y/aria-props
aria-description={maybeIconConfig[0].description}
onClick={() =>
maybeIconConfig[0].onClick(configCallbackProps)
}
<span
className={!maybeIconConfig[0].showTitle ? 'sr-only' : ''}
>
<span
className={!maybeIconConfig[0].showTitle ? 'sr-only' : ''}
>
{maybeIconConfig[0].title}
</span>
<ToolbarItemTooltip
itemConfig={maybeIconConfig[0]}
configCallbackProps={configCallbackProps}
wrapperClassName="ui-open:!hidden"
contentClassName={tooltipContentClassName}
>
{showRichContent ? (
<ToolbarItemTooltipRichContent
itemConfig={maybeIconConfig[0]}
/>
) : (
<ToolbarItemTooltipShortContent
status={maybeIconConfig[0].status}
title={maybeIconConfig[0].title}
hotkey={maybeIconConfig[0].hotkey}
/>
)}
</ToolbarItemTooltip>
</ActionButton>
</div>
{maybeIconConfig[0].title}
</span>
</ActionButton>
<ToolbarItemTooltip
itemConfig={maybeIconConfig[0]}
configCallbackProps={configCallbackProps}
/>
</ActionButtonDropdown>
)
}
@ -271,13 +218,7 @@ export function Toolbar({
// A single button
return (
<div
className="relative"
key={itemConfig.id}
// Mouse events do not fire on disabled buttons
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<div className="relative" key={itemConfig.id}>
<ActionButton
Element="button"
key={itemConfig.id}
@ -314,18 +255,7 @@ export function Toolbar({
<ToolbarItemTooltip
itemConfig={itemConfig}
configCallbackProps={configCallbackProps}
contentClassName={tooltipContentClassName}
>
{showRichContent ? (
<ToolbarItemTooltipRichContent itemConfig={itemConfig} />
) : (
<ToolbarItemTooltipShortContent
status={itemConfig.status}
title={itemConfig.title}
hotkey={itemConfig.hotkey}
/>
)}
</ToolbarItemTooltip>
/>
</div>
)
})}
@ -339,12 +269,6 @@ export function Toolbar({
)
}
interface ToolbarItemContentsProps extends React.PropsWithChildren {
itemConfig: ToolbarItemResolved
configCallbackProps: ToolbarItemCallbackProps
wrapperClassName?: string
contentClassName?: string
}
/**
* The single button and dropdown button share content, so we extract it here
* It contains a tooltip with the title, description, and links
@ -353,10 +277,12 @@ interface ToolbarItemContentsProps extends React.PropsWithChildren {
const ToolbarItemTooltip = memo(function ToolbarItemContents({
itemConfig,
configCallbackProps,
wrapperClassName = '',
contentClassName = '',
children,
}: ToolbarItemContentsProps) {
}: {
itemConfig: ToolbarItemResolved
configCallbackProps: ToolbarItemCallbackProps
}) {
const { state } = useModelingContext()
useHotkeys(
itemConfig.hotkey || '',
() => {
@ -379,50 +305,11 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
? ({ '-webkit-app-region': 'no-drag' } as React.CSSProperties)
: {}
}
hoverOnly
position="bottom"
wrapperClassName={'!p-4 !pointer-events-auto ' + wrapperClassName}
contentClassName={contentClassName}
delay={0}
wrapperClassName="!p-4 !pointer-events-auto"
contentClassName="!text-left text-wrap !text-xs !p-0 !pb-2 flex gap-2 !max-w-none !w-72 flex-col items-stretch"
>
{children}
</Tooltip>
)
})
const ToolbarItemTooltipShortContent = ({
status,
title,
hotkey,
}: {
status: string
title: string
hotkey?: string | string[]
}) => (
<span
className={`text-sm ${
status !== 'available' ? 'text-chalkboard-70 dark:text-chalkboard-40' : ''
}`}
>
{title}
{hotkey && (
<kbd className="inline-block ml-2 flex-none hotkey">{hotkey}</kbd>
)}
</span>
)
const ToolbarItemTooltipRichContent = ({
itemConfig,
}: {
itemConfig: ToolbarItemResolved
}) => {
const { state } = useModelingContext()
return (
<>
<div className="rounded-top flex items-center gap-2 pt-3 pb-2 px-2 bg-chalkboard-20/50 dark:bg-chalkboard-80/50">
{itemConfig.icon && (
<CustomIcon className="w-5 h-5" name={itemConfig.icon} />
)}
<span
className={`text-sm flex-1 ${
itemConfig.status !== 'available'
@ -491,6 +378,6 @@ const ToolbarItemTooltipRichContent = ({
</ul>
</>
)}
</>
</Tooltip>
)
}
})

View File

@ -108,8 +108,6 @@ export class CameraControls {
interactionGuards: MouseGuard = cameraMouseDragGuards.Zoo
isFovAnimationInProgress = false
perspectiveFovBeforeOrtho = 45
// NOTE: Duplicated state across Provider and singleton. Mapped from settingsMachine
_setting_allowOrbitInSketchMode = false
get isPerspective() {
return this.camera instanceof PerspectiveCamera
}

View File

@ -25,13 +25,13 @@ import {
CallExpression,
PathToNode,
Program,
SourceRange,
Expr,
parse,
recast,
defaultSourceRange,
resultIsOk,
ProgramMemory,
topLevelRange,
} from 'lang/wasm'
import { CustomIcon, CustomIconName } from 'components/CustomIcon'
import { ConstrainInfo } from 'lang/std/stdTypes'
@ -46,8 +46,8 @@ import {
} from 'lang/modifyAst'
import { ActionButton } from 'components/ActionButton'
import { err, reportRejection, trap } from 'lib/trap'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { Node } from 'wasm-lib/kcl/bindings/Node'
import { commandBarActor } from 'machines/commandBarMachine'
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
const [isCamMoving, setIsCamMoving] = useState(false)
@ -124,14 +124,6 @@ export const ClientSideScene = ({
'mouseup',
toSync(sceneInfra.onMouseUp, reportRejection)
)
sceneEntitiesManager
.tearDownSketch()
.then(() => {
// no op
})
.catch((e) => {
console.error(e)
})
}
}, [])
@ -518,6 +510,7 @@ const ConstraintSymbol = ({
constrainInfo: ConstrainInfo
verticalPosition: 'top' | 'bottom'
}) => {
const { commandBarSend } = useCommandsContext()
const { context } = useModelingContext()
const varNameMap: {
[key in ConstrainInfo['type']]: {
@ -607,8 +600,8 @@ const ConstraintSymbol = ({
if (err(_node)) return
const node = _node.node
const range = node
? topLevelRange(node.start, node.end)
const range: SourceRange = node
? [node.start, node.end, true]
: defaultSourceRange()
if (_type === 'intersectionTag') return null
@ -637,7 +630,7 @@ const ConstraintSymbol = ({
// disabled={implicitDesc} TODO why does this change styles that are hard to override?
onClick={toSync(async () => {
if (!isConstrained) {
commandBarActor.send({
commandBarSend({
type: 'Find and select command',
data: {
name: 'Constrain with named value',
@ -763,6 +756,7 @@ export const CamDebugSettings = () => {
sceneInfra.camControls.reactCameraProperties
)
const [fov, setFov] = useState(12)
const { commandBarSend } = useCommandsContext()
useEffect(() => {
sceneInfra.camControls.setReactCameraPropertiesCallback(setCamSettings)
@ -781,7 +775,7 @@ export const CamDebugSettings = () => {
type="checkbox"
checked={camSettings.type === 'perspective'}
onChange={() =>
commandBarActor.send({
commandBarSend({
type: 'Find and select command',
data: {
groupId: 'settings',

View File

@ -1,6 +1,5 @@
import {
BoxGeometry,
Color,
DoubleSide,
Group,
Intersection,
@ -59,9 +58,7 @@ import {
sourceRangeFromRust,
resultIsOk,
SourceRange,
topLevelRange,
} from 'lang/wasm'
import { calculate_circle_from_3_points } from '../wasm-lib/pkg/wasm_lib'
import {
engineCommandManager,
kclManager,
@ -69,12 +66,11 @@ import {
codeManager,
editorManager,
} from 'lib/singletons'
import { getNodeFromPath } from 'lang/queryAst'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
import { executeAst, ToolTip } from 'lang/langHelpers'
import {
createProfileStartHandle,
createCircleGeometry,
createArcGeometry,
SegmentUtils,
segmentUtils,
} from './segments'
@ -113,8 +109,6 @@ import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
import { Point3d } from 'wasm-lib/kcl/bindings/Point3d'
import { SegmentInputs } from 'lang/std/stdTypes'
import { Node } from 'wasm-lib/kcl/bindings/Node'
import { LabeledArg } from 'wasm-lib/kcl/bindings/LabeledArg'
import { Literal } from 'wasm-lib/kcl/bindings/Literal'
import { radToDeg } from 'three/src/math/MathUtils'
import { getArtifactFromRange, codeRefFromRange } from 'lang/std/artifactGraph'
@ -630,7 +624,7 @@ export class SceneEntities {
const startRange = _node1.node.start
const endRange = _node1.node.end
const sourceRange = topLevelRange(startRange, endRange)
const sourceRange: SourceRange = [startRange, endRange, true]
const selection: Selections = computeSelectionFromSourceRangeAndAST(
sourceRange,
maybeModdedAst
@ -1267,98 +1261,110 @@ export class SceneEntities {
const groupOfDrafts = new Group()
groupOfDrafts.name = 'circle-3-point-group'
groupOfDrafts.position.copy(sketchOrigin)
// lee: I'm keeping this here as a developer gotchya:
// If you use 3D points, do not rotate anything.
// If you use 2D points (easier to deal with, generally do this!), then
// rotate the group just like this! Remember to rotate other groups too!
groupOfDrafts.setRotationFromQuaternion(orientation)
// Do not reorient your surfaces to the intersection plane. Your points are
// already in 3D space, not 2D. If you intersect say XZ, you want the points
// to continue to live at the 3D intersection point, not be rotated to end
// up elsewhere!
// groupOfDrafts.setRotationFromQuaternion(orientation)
this.scene.add(groupOfDrafts)
// How large the points on the circle will render as
const DRAFT_POINT_RADIUS = 10 // px
const DRAFT_POINT_RADIUS = 6
// The target of our dragging
let target: Object3D | undefined = undefined
// The KCL this will generate.
const kclCircle3Point = parse(`circleThreePoint(
p1 = [0.0, 0.0],
p2 = [0.0, 0.0],
p3 = [0.0, 0.0],
)`)
const createPoint = (
center: Vector3,
opts?: { noInteraction?: boolean }
): Mesh => {
const createPoint = (center: Vector3): number => {
const geometry = new SphereGeometry(DRAFT_POINT_RADIUS)
const color = getThemeColorForThreeJs(sceneInfra._theme)
const material = new MeshBasicMaterial({
color: opts?.noInteraction
? sceneInfra._theme === 'light'
? new Color(color).multiplyScalar(0.15)
: new Color(0x010101).multiplyScalar(2000)
: color,
})
const material = new MeshBasicMaterial({ color })
const mesh = new Mesh(geometry, material)
mesh.userData = {
type: opts?.noInteraction ? 'ghost' : CIRCLE_3_POINT_DRAFT_POINT,
}
mesh.renderOrder = 1000
mesh.userData = { type: CIRCLE_3_POINT_DRAFT_POINT }
mesh.layers.set(SKETCH_LAYER)
mesh.position.copy(center)
mesh.scale.set(scale, scale, scale)
mesh.renderOrder = 100
return mesh
groupOfDrafts.add(mesh)
return mesh.id
}
const createCircle3PointGraphic = async (
points: Vector2[],
center: Vector2,
radius: number
) => {
if (
Number.isNaN(radius) ||
Number.isNaN(center.x) ||
Number.isNaN(center.y)
)
return
const circle3Point = (
points: Vector2[]
): undefined | { center: Vector3; radius: number } => {
// A 3-point circle is undefined if it doesn't have 3 points :)
if (points.length !== 3) return undefined
// y = (i/j)(x-h) + b
// i and j variables for the slopes
const i = [points[1].x - points[0].x, points[2].x - points[1].x]
const j = [points[1].y - points[0].y, points[2].y - points[1].y]
// Our / threejs coordinate system affects this a lot. If you take this
// code into a different code base, you may have to adjust a/b to being
// -1/a/b, b/a, etc! In this case, a/-b did the trick.
const m = [i[0] / -j[0], i[1] / -j[1]]
const h = [
(points[0].x + points[1].x) / 2,
(points[1].x + points[2].x) / 2,
]
const b = [
(points[0].y + points[1].y) / 2,
(points[1].y + points[2].y) / 2,
]
// Algebraically derived
const x = (-m[0] * h[0] + b[0] - b[1] + m[1] * h[1]) / (m[1] - m[0])
const y = m[0] * (x - h[0]) + b[0]
const center = new Vector3(x, y, 0)
const radius = Math.sqrt((points[1].x - x) ** 2 + (points[1].y - y) ** 2)
return {
center,
radius,
}
}
// TO BE SHORT LIVED: unused function to draw the circle and lines.
// @ts-ignore
// eslint-disable-next-line
const createCircle3Point = (points: Vector2[]) => {
const circleParams = circle3Point(points)
// A circle cannot be created for these points.
if (!circleParams) return
const color = getThemeColorForThreeJs(sceneInfra._theme)
const lineCircle = createCircleGeometry({
center: [center.x, center.y],
radius,
color,
isDashed: false,
scale: 1,
const geometryCircle = createArcGeometry({
center: [circleParams.center.x, circleParams.center.y],
radius: circleParams.radius,
startAngle: 0,
endAngle: Math.PI * 2,
ccw: true,
isDashed: true,
scale,
})
lineCircle.userData = { type: CIRCLE_3_POINT_DRAFT_CIRCLE }
lineCircle.layers.set(SKETCH_LAYER)
// devnote: it's a mistake to use these with EllipseCurve :)
// lineCircle.position.set(center.x, center.y, 0)
// lineCircle.scale.set(scale, scale, scale)
const materialCircle = new MeshBasicMaterial({ color })
if (groupCircle) groupOfDrafts.remove(groupCircle)
groupCircle = new Group()
groupCircle.renderOrder = 1
groupCircle.add(lineCircle)
const pointMesh = createPoint(new Vector3(center.x, center.y, 0), {
noInteraction: true,
})
groupCircle.add(pointMesh)
const meshCircle = new Mesh(geometryCircle, materialCircle)
meshCircle.userData = { type: CIRCLE_3_POINT_DRAFT_CIRCLE }
meshCircle.layers.set(SKETCH_LAYER)
meshCircle.position.set(circleParams.center.x, circleParams.center.y, 0)
meshCircle.scale.set(scale, scale, scale)
groupCircle.add(meshCircle)
const geometryPolyLine = new BufferGeometry().setFromPoints([
...points.map((p) => new Vector3(p.x, p.y, 0)),
new Vector3(points[0].x, points[0].y, 0),
...points,
points[0],
])
const materialPolyLine = new LineDashedMaterial({
color,
scale: 1 / scale,
scale,
dashSize: 6,
gapSize: 6,
})
@ -1369,146 +1375,13 @@ export class SceneEntities {
groupOfDrafts.add(groupCircle)
}
const insertCircle3PointKclIntoAstSnapshot = (
points: Vector2[]
): Program => {
if (err(kclCircle3Point) || kclCircle3Point.program === null)
return kclManager.ast
if (kclCircle3Point.program.body[0].type !== 'ExpressionStatement')
return kclManager.ast
if (
kclCircle3Point.program.body[0].expression.type !== 'CallExpressionKw'
)
return kclManager.ast
const arg = (x: LabeledArg): Literal[] | undefined => {
if (
'arg' in x &&
'elements' in x.arg &&
x.arg.type === 'ArrayExpression'
) {
if (x.arg.elements.every((x) => x.type === 'Literal')) {
return x.arg.elements
}
}
return undefined
}
const kclCircle3PointArgs =
kclCircle3Point.program.body[0].expression.arguments
const arg0 = arg(kclCircle3PointArgs[0])
if (!arg0) return kclManager.ast
arg0[0].value = { value: points[0].x, suffix: 'None' }
arg0[0].raw = points[0].x.toString()
arg0[1].value = { value: points[0].y, suffix: 'None' }
arg0[1].raw = points[0].y.toString()
const arg1 = arg(kclCircle3PointArgs[1])
if (!arg1) return kclManager.ast
arg1[0].value = { value: points[1].x, suffix: 'None' }
arg1[0].raw = points[1].x.toString()
arg1[1].value = { value: points[1].y, suffix: 'None' }
arg1[1].raw = points[1].y.toString()
const arg2 = arg(kclCircle3PointArgs[2])
if (!arg2) return kclManager.ast
arg2[0].value = { value: points[2].x, suffix: 'None' }
arg2[0].raw = points[2].x.toString()
arg2[1].value = { value: points[2].y, suffix: 'None' }
arg2[1].raw = points[2].y.toString()
const astSnapshot = structuredClone(kclManager.ast)
const startSketchOnASTNode = getNodeFromPath<VariableDeclaration>(
astSnapshot,
startSketchOnASTNodePath,
'VariableDeclaration'
)
if (err(startSketchOnASTNode)) return astSnapshot
// It's possible we're already dealing with a PipeExpression.
// Modify the current one.
if (
startSketchOnASTNode.node.declaration.init.type === 'PipeExpression' &&
startSketchOnASTNode.node.declaration.init.body[1].type ===
'CallExpressionKw' &&
startSketchOnASTNode.node.declaration.init.body.length >= 2
) {
startSketchOnASTNode.node.declaration.init.body[1].arguments =
kclCircle3Point.program.body[0].expression.arguments
} else {
// Clone a new node based on the old, and replace the old with the new.
const clonedStartSketchOnASTNode = structuredClone(startSketchOnASTNode)
startSketchOnASTNode.node.declaration.init = createPipeExpression([
clonedStartSketchOnASTNode.node.declaration.init,
kclCircle3Point.program.body[0].expression,
])
}
// Return the `Program`
return astSnapshot
}
const updateCircle3Point = async (opts?: { execute?: true }) => {
const points_ = Array.from(points.values())
const circleParams = calculate_circle_from_3_points(
points_[0].x,
points_[0].y,
points_[1].x,
points_[1].y,
points_[2].x,
points_[2].y
)
if (Number.isNaN(circleParams.radius)) return
await createCircle3PointGraphic(
points_,
new Vector2(circleParams.center_x, circleParams.center_y),
circleParams.radius
)
const astWithNewCode = insertCircle3PointKclIntoAstSnapshot(points_)
const codeAsString = recast(astWithNewCode)
if (err(codeAsString)) return
codeManager.updateCodeStateEditor(codeAsString)
}
// The target of our dragging
let target: Object3D | undefined = undefined
const cleanupFn = () => {
this.scene.remove(groupOfDrafts)
}
// The AST node we extracted earlier may already have a circleThreePoint!
// Use the points in the AST as starting points.
const astSnapshot = structuredClone(kclManager.ast)
const maybeVariableDeclaration = getNodeFromPath<VariableDeclaration>(
astSnapshot,
startSketchOnASTNodePath,
'VariableDeclaration'
)
if (err(maybeVariableDeclaration))
return () => {
done()
}
const maybeCallExpressionKw = maybeVariableDeclaration.node.declaration.init
if (
maybeCallExpressionKw.type === 'PipeExpression' &&
maybeCallExpressionKw.body[1].type === 'CallExpressionKw' &&
maybeCallExpressionKw.body[1]?.callee.name === 'circleThreePoint'
) {
maybeCallExpressionKw?.body[1].arguments
.map(
({ arg }: any) =>
new Vector2(arg.elements[0].value, arg.elements[1].value)
)
.forEach((point: Vector2) => {
const pointMesh = createPoint(new Vector3(point.x, point.y, 0))
groupOfDrafts.add(pointMesh)
points.set(pointMesh.id, point)
})
void updateCircle3Point()
}
sceneInfra.setCallbacks({
async onDrag(args) {
const draftPointsIntersected = args.intersects.filter(
@ -1524,18 +1397,8 @@ export class SceneEntities {
// The user was off their mark! Missed the object to select.
if (!target) return
target.position.copy(
new Vector3(
args.intersectionPoint.twoD.x,
args.intersectionPoint.twoD.y,
0
)
)
target.position.copy(args.intersectionPoint.threeD)
points.set(target.id, args.intersectionPoint.twoD)
if (points.size <= 2) return
await updateCircle3Point()
},
async onDragEnd(_args) {
target = undefined
@ -1544,19 +1407,45 @@ export class SceneEntities {
if (points.size >= 3) return
if (!args.intersectionPoint) return
const pointMesh = createPoint(
new Vector3(
args.intersectionPoint.twoD.x,
args.intersectionPoint.twoD.y,
0
)
const id = createPoint(args.intersectionPoint.threeD)
points.set(id, args.intersectionPoint.twoD)
if (points.size < 2) return
// We've now got 3 points, let's create our circle!
const astSnapshot = structuredClone(kclManager.ast)
let nodeQueryResult
nodeQueryResult = getNodeFromPath<VariableDeclaration>(
astSnapshot,
startSketchOnASTNodePath,
'VariableDeclaration'
)
groupOfDrafts.add(pointMesh)
points.set(pointMesh.id, args.intersectionPoint.twoD)
if (err(nodeQueryResult)) return Promise.reject(nodeQueryResult)
const startSketchOnASTNode = nodeQueryResult
if (points.size <= 2) return
const circleParams = circle3Point(Array.from(points.values()))
await updateCircle3Point()
if (!circleParams) return
const kclCircle3Point = parse(`circle({
center = [${circleParams.center.x}, ${circleParams.center.y}],
radius = ${circleParams.radius},
}, %)`)
if (err(kclCircle3Point) || kclCircle3Point.program === null) return
if (kclCircle3Point.program.body[0].type !== 'ExpressionStatement')
return
const clonedStartSketchOnASTNode = structuredClone(startSketchOnASTNode)
startSketchOnASTNode.node.declaration.init = createPipeExpression([
clonedStartSketchOnASTNode.node.declaration.init,
kclCircle3Point.program.body[0].expression,
])
await kclManager.executeAstMock(astSnapshot)
await codeManager.updateEditorWithAstAndWriteToFile(astSnapshot)
done()
},
})
@ -2014,7 +1903,7 @@ export class SceneEntities {
kclManager.programMemory,
{
type: 'sourceRange',
sourceRange: topLevelRange(node.start, node.end),
sourceRange: [node.start, node.end, true],
},
getChangeSketchInput()
)
@ -2052,8 +1941,8 @@ export class SceneEntities {
)
if (!(sk instanceof Reason)) {
sketch = sk
} else if (maybeSketch && (maybeSketch.value as Solid)?.sketch) {
sketch = (maybeSketch.value as Solid).sketch
} else if ((maybeSketch as Solid).sketch) {
sketch = (maybeSketch as Solid).sketch
}
if (!sketch) return
@ -2265,7 +2154,7 @@ export class SceneEntities {
)
if (trap(_node, { suppress: true })) return
const node = _node.node
editorManager.setHighlightRange([topLevelRange(node.start, node.end)])
editorManager.setHighlightRange([[node.start, node.end, true]])
const yellow = 0xffff00
colorSegment(selected, yellow)
const extraSegmentGroup = parent.getObjectByName(EXTRA_SEGMENT_HANDLE)
@ -2542,7 +2431,7 @@ export function sketchFromPathToNode({
const varDec = _varDec.node
const result = programMemory.get(varDec?.id?.name || '')
if (result?.type === 'Solid') {
return result.value.sketch
return result.sketch
}
const sg = sketchFromKclValue(result, varDec?.id?.name)
if (err(sg)) {

View File

@ -9,9 +9,6 @@ import {
ExtrudeGeometry,
Group,
LineCurve3,
LineBasicMaterial,
LineDashedMaterial,
Line,
Mesh,
MeshBasicMaterial,
NormalBufferAttributes,
@ -61,7 +58,6 @@ import { SegmentInputs } from 'lang/std/stdTypes'
import { err } from 'lib/trap'
import { editorManager, sceneInfra } from 'lib/singletons'
import { Selections } from 'lib/selections'
import { commandBarActor } from 'machines/commandBarMachine'
interface CreateSegmentArgs {
input: SegmentInputs
@ -848,7 +844,7 @@ function createLengthIndicator({
})
// Command Bar
commandBarActor.send({
editorManager.commandBarSend({
type: 'Find and select command',
data: {
name: 'Constrain length',
@ -1007,49 +1003,6 @@ export function createArcGeometry({
return geo
}
// (lee) The above is much more complex than necessary.
// I've derived the new code from:
// https://threejs.org/docs/#api/en/extras/curves/EllipseCurve
// I'm not sure why it wasn't done like this in the first place?
// I don't touch the code above because it may break something else.
export function createCircleGeometry({
center,
radius,
color,
isDashed = false,
scale = 1,
}: {
center: Coords2d
radius: number
color: number
isDashed?: boolean
scale?: number
}): Line {
const circle = new EllipseCurve(
center[0],
center[1],
radius,
radius,
0,
Math.PI * 2,
true,
scale
)
const points = circle.getPoints(75) // just enough points to not see edges.
const geometry = new BufferGeometry().setFromPoints(points)
const material = !isDashed
? new LineBasicMaterial({ color })
: new LineDashedMaterial({
color,
scale,
dashSize: 6,
gapSize: 6,
})
const line = new Line(geometry, material)
line.computeLineDistances()
return line
}
export function dashedStraight(
from: Coords2d,
to: Coords2d,

View File

@ -1,11 +1,9 @@
import { Popover } from '@headlessui/react'
import { ActionButtonProps } from './ActionButton'
import { CustomIcon } from './CustomIcon'
import Tooltip from './Tooltip'
type ActionButtonSplitProps = ActionButtonProps & { Element: 'button' } & {
name?: string
dropdownTooltipText?: string
splitMenuItems: {
id: string
label: string
@ -19,7 +17,6 @@ type ActionButtonSplitProps = ActionButtonProps & { Element: 'button' } & {
export function ActionButtonDropdown({
splitMenuItems,
className,
dropdownTooltipText = 'More tools',
children,
...props
}: ActionButtonSplitProps) {
@ -29,14 +26,7 @@ export function ActionButtonDropdown({
{({ close }) => (
<>
{children}
<Popover.Button
className={
'!border-transparent dark:!border-transparent ' +
'bg-chalkboard-transparent dark:bg-transparent disabled:bg-transparent dark:disabled:bg-transparent ' +
'enabled:hover:bg-chalkboard-10 dark:enabled:hover:bg-chalkboard-100 ' +
'pressed:!bg-primary pressed:enabled:hover:!text-chalkboard-10 p-0 m-0 rounded-none !outline-none ui-open:border-primary ui-open:bg-primary'
}
>
<Popover.Button className="border-transparent dark:border-transparent p-0 m-0 rounded-none !outline-none ui-open:border-primary ui-open:bg-primary">
<CustomIcon
name="caretDown"
className={
@ -47,14 +37,6 @@ export function ActionButtonDropdown({
<span className="sr-only">
{props.name ? props.name + ': ' : ''}open menu
</span>
<Tooltip
delay={0}
position="bottom"
hoverOnly
wrapperClassName="ui-open:!hidden"
>
{dropdownTooltipText}
</Tooltip>
</Popover.Button>
<Popover.Panel
as="ul"

View File

@ -2,11 +2,11 @@ import { Toolbar } from '../Toolbar'
import UserSidebarMenu from 'components/UserSidebarMenu'
import { type IndexLoaderData } from 'lib/types'
import ProjectSidebarMenu from './ProjectSidebarMenu'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import styles from './AppHeader.module.css'
import { RefreshButton } from 'components/RefreshButton'
import { CommandBarOpenButton } from './CommandBarOpenButton'
import { isDesktop } from 'lib/isDesktop'
import { useUser } from 'machines/appMachine'
interface AppHeaderProps extends React.PropsWithChildren {
showToolbar?: boolean
@ -24,7 +24,8 @@ export const AppHeader = ({
style,
enableMenu = false,
}: AppHeaderProps) => {
const user = useUser()
const { auth } = useSettingsAuthContext()
const user = auth?.context?.user
return (
<header

View File

@ -1,12 +1,11 @@
import { useModelingContext } from 'hooks/useModelingContext'
import { editorManager, engineCommandManager, kclManager } from 'lib/singletons'
import { getNodeFromPath } from 'lang/queryAst'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
import { useEffect, useRef, useState } from 'react'
import { trap } from 'lib/trap'
import { codeToIdSelections } from 'lib/selections'
import { codeRefFromRange } from 'lang/std/artifactGraph'
import { defaultSourceRange, SourceRange, topLevelRange } from 'lang/wasm'
import { defaultSourceRange } from 'lang/wasm'
export function AstExplorer() {
const { context } = useModelingContext()
@ -119,19 +118,19 @@ function DisplayObj({
hasCursor ? 'bg-violet-100/80 dark:bg-violet-100/25' : ''
}`}
onMouseEnter={(e) => {
editorManager.setHighlightRange([
topLevelRange(obj?.start || 0, obj.end),
])
editorManager.setHighlightRange([[obj?.start || 0, obj.end, true]])
e.stopPropagation()
}}
onMouseMove={(e) => {
e.stopPropagation()
editorManager.setHighlightRange([
topLevelRange(obj?.start || 0, obj.end),
])
editorManager.setHighlightRange([[obj?.start || 0, obj.end, true]])
}}
onClick={(e) => {
const range = topLevelRange(obj?.start || 0, obj.end || 0)
const range: [number, number, boolean] = [
obj?.start || 0,
obj.end || 0,
true,
]
const idInfo = codeToIdSelections([
{ codeRef: codeRefFromRange(range, kclManager.ast) },
])[0]

View File

@ -1,8 +1,8 @@
import { Combobox } from '@headlessui/react'
import { useSelector } from '@xstate/react'
import Fuse from 'fuse.js'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { CommandArgument, CommandArgumentOption } from 'lib/commandTypes'
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
import { useEffect, useMemo, useRef, useState } from 'react'
import { AnyStateMachine, StateFrom } from 'xstate'
@ -23,7 +23,7 @@ function CommandArgOptionInput({
placeholder?: string
}) {
const actorContext = useSelector(arg.machineActor, contextSelector)
const commandBarState = useCommandBarState()
const { commandBarSend, commandBarState } = useCommandsContext()
const resolvedOptions = useMemo(
() =>
typeof arg.options === 'function'
@ -129,13 +129,11 @@ function CommandArgOptionInput({
<label
htmlFor="option-input"
className="capitalize px-2 py-1 rounded-l bg-chalkboard-100 dark:bg-chalkboard-80 text-chalkboard-10 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80"
data-testid="cmd-bar-arg-name"
>
{argName}
</label>
<Combobox.Input
id="option-input"
data-testid="cmd-bar-arg-value"
ref={inputRef}
onChange={(event) =>
!event.target.disabled && setQuery(event.target.value)
@ -143,7 +141,7 @@ function CommandArgOptionInput({
className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none"
onKeyDown={(event) => {
if (event.metaKey && event.key === 'k')
commandBarActor.send({ type: 'Close' })
commandBarSend({ type: 'Close' })
if (event.key === 'Backspace' && !event.currentTarget.value) {
stepBack()
}

View File

@ -1,5 +1,6 @@
import { Dialog, Popover, Transition } from '@headlessui/react'
import { Fragment, useEffect } from 'react'
import { useCommandsContext } from 'hooks/useCommandsContext'
import CommandBarArgument from './CommandBarArgument'
import CommandComboBox from '../CommandComboBox'
import CommandBarReview from './CommandBarReview'
@ -7,13 +8,12 @@ import { useLocation } from 'react-router-dom'
import useHotkeyWrapper from 'lib/hotkeyWrapper'
import { CustomIcon } from 'components/CustomIcon'
import Tooltip from 'components/Tooltip'
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
export const COMMAND_PALETTE_HOTKEY = 'mod+k'
export const CommandBar = () => {
const { pathname } = useLocation()
const commandBarState = useCommandBarState()
const { commandBarState, commandBarSend } = useCommandsContext()
const {
context: { selectedCommand, currentArgument, commands },
} = commandBarState
@ -22,17 +22,16 @@ export const CommandBar = () => {
// Close the command bar when navigating
useEffect(() => {
if (commandBarState.matches('Closed')) return
commandBarActor.send({ type: 'Close' })
commandBarSend({ type: 'Close' })
}, [pathname])
// Hook up keyboard shortcuts
useHotkeyWrapper([COMMAND_PALETTE_HOTKEY], () => {
if (commandBarState.context.commands.length === 0) return
if (commandBarState.matches('Closed')) {
commandBarActor.send({ type: 'Open' })
commandBarSend({ type: 'Open' })
} else {
commandBarActor.send({ type: 'Close' })
commandBarSend({ type: 'Close' })
}
})
@ -52,14 +51,14 @@ export const CommandBar = () => {
...entries[entries.length - 1][1],
}
commandBarActor.send({
commandBarSend({
type: 'Edit argument',
data: {
arg: currentArg,
},
})
} else {
commandBarActor.send({ type: 'Deselect command' })
commandBarSend({ type: 'Deselect command' })
}
} else {
const entries = Object.entries(selectedCommand?.args || {})
@ -68,9 +67,9 @@ export const CommandBar = () => {
)
if (index === 0) {
commandBarActor.send({ type: 'Deselect command' })
commandBarSend({ type: 'Deselect command' })
} else {
commandBarActor.send({
commandBarSend({
type: 'Change current argument',
data: {
arg: { name: entries[index - 1][0], ...entries[index - 1][1] },
@ -85,20 +84,19 @@ export const CommandBar = () => {
show={!commandBarState.matches('Closed') || false}
afterLeave={() => {
if (selectedCommand?.onCancel) selectedCommand.onCancel()
commandBarActor.send({ type: 'Clear' })
commandBarSend({ type: 'Clear' })
}}
as={Fragment}
>
<WrapperComponent
open={!commandBarState.matches('Closed') || isSelectionArgument}
onClose={() => {
commandBarActor.send({ type: 'Close' })
commandBarSend({ type: 'Close' })
}}
className={
'fixed inset-0 z-50 overflow-y-auto pb-4 pt-1 ' +
(isSelectionArgument ? 'pointer-events-none' : '')
}
data-testid="command-bar-wrapper"
>
<Transition.Child
enter="duration-100 ease-out"
@ -123,7 +121,7 @@ export const CommandBar = () => {
)
)}
<button
onClick={() => commandBarActor.send({ type: 'Close' })}
onClick={() => commandBarSend({ type: 'Close' })}
className="group block !absolute left-auto right-full top-[-3px] m-2.5 p-0 border-none bg-transparent hover:bg-transparent"
>
<CustomIcon

View File

@ -2,13 +2,13 @@ import CommandArgOptionInput from './CommandArgOptionInput'
import CommandBarBasicInput from './CommandBarBasicInput'
import CommandBarSelectionInput from './CommandBarSelectionInput'
import { CommandArgument } from 'lib/commandTypes'
import { useCommandsContext } from 'hooks/useCommandsContext'
import CommandBarHeader from './CommandBarHeader'
import CommandBarKclInput from './CommandBarKclInput'
import CommandBarTextareaInput from './CommandBarTextareaInput'
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
function CommandBarArgument({ stepBack }: { stepBack: () => void }) {
const commandBarState = useCommandBarState()
const { commandBarState, commandBarSend } = useCommandsContext()
const {
context: { currentArgument },
} = commandBarState
@ -16,7 +16,7 @@ function CommandBarArgument({ stepBack }: { stepBack: () => void }) {
function onSubmit(data: unknown) {
if (!currentArgument) return
commandBarActor.send({
commandBarSend({
type: 'Submit argument',
data: {
[currentArgument.name]: data,

View File

@ -1,5 +1,5 @@
import { useCommandsContext } from 'hooks/useCommandsContext'
import { CommandArgument } from 'lib/commandTypes'
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
import { useEffect, useRef } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
@ -15,8 +15,8 @@ function CommandBarBasicInput({
stepBack: () => void
onSubmit: (event: unknown) => void
}) {
const commandBarState = useCommandBarState()
useHotkeys('mod + k, mod + /', () => commandBarActor.send({ type: 'Close' }))
const { commandBarSend, commandBarState } = useCommandsContext()
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {

View File

@ -1,3 +1,4 @@
import { useCommandsContext } from 'hooks/useCommandsContext'
import { CustomIcon } from '../CustomIcon'
import React, { useState } from 'react'
import { ActionButton } from '../ActionButton'
@ -6,10 +7,9 @@ import { useHotkeys } from 'react-hotkeys-hook'
import { KclCommandValue, KclExpressionWithVariable } from 'lib/commandTypes'
import Tooltip from 'components/Tooltip'
import { roundOff } from 'lib/utils'
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
const commandBarState = useCommandBarState()
const { commandBarState, commandBarSend } = useCommandsContext()
const {
context: { selectedCommand, currentArgument, argumentsToSubmit },
} = commandBarState
@ -49,7 +49,7 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
]
const arg = selectedCommand?.args[argName]
if (!argName || !arg) return
commandBarActor.send({
commandBarSend({
type: 'Change current argument',
data: { arg: { ...arg, name: argName } },
})
@ -100,7 +100,7 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
}
disabled={!isReviewing && currentArgument?.name === argName}
onClick={() => {
commandBarActor.send({
commandBarSend({
type: isReviewing
? 'Edit argument'
: 'Change current argument',

View File

@ -7,6 +7,7 @@ import {
} from '@codemirror/autocomplete'
import { EditorView, keymap, ViewUpdate } from '@codemirror/view'
import { CustomIcon } from 'components/CustomIcon'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { CommandArgument, KclCommandValue } from 'lib/commandTypes'
import { getSystemTheme } from 'lib/theme'
@ -19,7 +20,6 @@ import styles from './CommandBarKclInput.module.css'
import { createIdentifier, createVariableDeclaration } from 'lang/modifyAst'
import { useCodeMirror } from 'components/ModelingSidebar/ModelingPanes/CodeEditor'
import { useSelector } from '@xstate/react'
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
const machineContextSelector = (snapshot?: {
context: Record<string, unknown>
@ -37,7 +37,7 @@ function CommandBarKclInput({
stepBack: () => void
onSubmit: (event: unknown) => void
}) {
const commandBarState = useCommandBarState()
const { commandBarSend, commandBarState } = useCommandsContext()
const previouslySetValue = commandBarState.context.argumentsToSubmit[
arg.name
] as KclCommandValue | undefined
@ -82,7 +82,7 @@ function CommandBarKclInput({
false
)
const [canSubmit, setCanSubmit] = useState(true)
useHotkeys('mod + k, mod + /', () => commandBarActor.send({ type: 'Close' }))
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
const editorRef = useRef<HTMLDivElement>(null)
const {

View File

@ -0,0 +1,43 @@
import { createActorContext } from '@xstate/react'
import { editorManager } from 'lib/singletons'
import { commandBarMachine } from 'machines/commandBarMachine'
import { useEffect } from 'react'
export const CommandsContext = createActorContext(
commandBarMachine.provide({
guards: {
'Command has no arguments': ({ context }) => {
return (
!context.selectedCommand?.args ||
Object.keys(context.selectedCommand?.args).length === 0
)
},
'All arguments are skippable': ({ context }) => {
return Object.values(context.selectedCommand!.args!).every(
(argConfig) => argConfig.skip
)
},
},
})
)
export const CommandBarProvider = ({
children,
}: {
children: React.ReactNode
}) => {
return (
<CommandsContext.Provider>
<CommandBarProviderInner>{children}</CommandBarProviderInner>
</CommandsContext.Provider>
)
}
function CommandBarProviderInner({ children }: { children: React.ReactNode }) {
const commandBarActor = CommandsContext.useActorRef()
useEffect(() => {
editorManager.setCommandBarSend(commandBarActor.send)
})
return children
}

View File

@ -1,9 +1,9 @@
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
import { useCommandsContext } from 'hooks/useCommandsContext'
import CommandBarHeader from './CommandBarHeader'
import { useHotkeys } from 'react-hotkeys-hook'
function CommandBarReview({ stepBack }: { stepBack: () => void }) {
const commandBarState = useCommandBarState()
const { commandBarState, commandBarSend } = useCommandsContext()
const {
context: { argumentsToSubmit, selectedCommand },
} = commandBarState
@ -33,7 +33,7 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) {
parseInt(b.keys[0], 10) - 1
]
const arg = selectedCommand?.args[argName]
commandBarActor.send({
commandBarSend({
type: 'Edit argument',
data: { arg: { ...arg, name: argName } },
})
@ -50,7 +50,7 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) {
function submitCommand(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
commandBarActor.send({
commandBarSend({
type: 'Submit command',
output: argumentsToSubmit,
})

View File

@ -1,4 +1,5 @@
import { useSelector } from '@xstate/react'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { Artifact } from 'lang/std/artifactGraph'
import { CommandArgument } from 'lib/commandTypes'
import {
@ -9,7 +10,6 @@ import {
import { kclManager } from 'lib/singletons'
import { reportRejection } from 'lib/trap'
import { toSync } from 'lib/utils'
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
import { modelingMachine } from 'machines/modelingMachine'
import { useEffect, useMemo, useRef, useState } from 'react'
import { StateFrom } from 'xstate'
@ -17,7 +17,7 @@ import { StateFrom } from 'xstate'
const semanticEntityNames: {
[key: string]: Array<Artifact['type'] | 'defaultPlane'>
} = {
face: ['wall', 'cap', 'solid2d'],
face: ['wall', 'cap', 'solid2D'],
edge: ['segment', 'sweepEdge', 'edgeCutEdge'],
point: [],
plane: ['defaultPlane'],
@ -49,7 +49,7 @@ function CommandBarSelectionInput({
onSubmit: (data: unknown) => void
}) {
const inputRef = useRef<HTMLInputElement>(null)
const commandBarState = useCommandBarState()
const { commandBarState, commandBarSend } = useCommandsContext()
const [hasSubmitted, setHasSubmitted] = useState(false)
const selection = useSelector(arg.machineActor, selectionSelector)
const selectionsByType = useMemo(() => {
@ -145,7 +145,7 @@ function CommandBarSelectionInput({
if (event.key === 'Backspace') {
stepBack()
} else if (event.key === 'Escape') {
commandBarActor.send({ type: 'Close' })
commandBarSend({ type: 'Close' })
}
}}
onChange={handleChange}

View File

@ -1,5 +1,5 @@
import { useCommandsContext } from 'hooks/useCommandsContext'
import { CommandArgument } from 'lib/commandTypes'
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
import { RefObject, useEffect, useRef } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
@ -15,8 +15,8 @@ function CommandBarTextareaInput({
stepBack: () => void
onSubmit: (event: unknown) => void
}) {
const commandBarState = useCommandBarState()
useHotkeys('mod + k, mod + /', () => commandBarActor.send({ type: 'Close' }))
const { commandBarSend, commandBarState } = useCommandsContext()
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
const formRef = useRef<HTMLFormElement>(null)
const inputRef = useRef<HTMLTextAreaElement>(null)
useTextareaAutoGrow(inputRef)

View File

@ -1,15 +1,16 @@
import { useCommandsContext } from 'hooks/useCommandsContext'
import usePlatform from 'hooks/usePlatform'
import { hotkeyDisplay } from 'lib/hotkeyWrapper'
import { COMMAND_PALETTE_HOTKEY } from './CommandBar/CommandBar'
import { commandBarActor } from 'machines/commandBarMachine'
export function CommandBarOpenButton() {
const { commandBarSend } = useCommandsContext()
const platform = usePlatform()
return (
<button
className="group rounded-full flex items-center justify-center gap-2 px-2 py-1 bg-primary/10 dark:bg-chalkboard-90 dark:backdrop-blur-sm border-primary hover:border-primary dark:border-chalkboard-50 dark:hover:border-inherit text-primary dark:text-inherit"
onClick={() => commandBarActor.send({ type: 'Open' })}
onClick={() => commandBarSend({ type: 'Open' })}
data-testid="command-bar-open-button"
>
<span>Commands</span>

View File

@ -1,11 +1,9 @@
import { Combobox } from '@headlessui/react'
import Fuse from 'fuse.js'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { Command } from 'lib/commandTypes'
import { useEffect, useState } from 'react'
import { CustomIcon } from './CustomIcon'
import { getActorNextEvents } from 'lib/utils'
import { sortCommands } from 'lib/commandUtils'
import { commandBarActor } from 'machines/commandBarMachine'
function CommandComboBox({
options,
@ -14,21 +12,14 @@ function CommandComboBox({
options: Command[]
placeholder?: string
}) {
const { commandBarSend } = useCommandsContext()
const [query, setQuery] = useState('')
const [filteredOptions, setFilteredOptions] = useState<typeof options>()
const defaultOption =
options.find((o) => 'isCurrent' in o && o.isCurrent) || null
// sort disabled commands to the bottom
const sortedOptions = options
.map((command) => ({
command,
disabled: optionIsDisabled(command),
}))
.sort(sortCommands)
.map(({ command }) => command)
const fuse = new Fuse(sortedOptions, {
const fuse = new Fuse(options, {
keys: ['displayName', 'name', 'description'],
threshold: 0.3,
ignoreLocation: true,
@ -36,11 +27,11 @@ function CommandComboBox({
useEffect(() => {
const results = fuse.search(query).map((result) => result.item)
setFilteredOptions(query.length > 0 ? results : sortedOptions)
setFilteredOptions(query.length > 0 ? results : options)
}, [query])
function handleSelection(command: Command) {
commandBarActor.send({ type: 'Select command', data: { command } })
commandBarSend({ type: 'Select command', data: { command } })
}
return (
@ -51,7 +42,6 @@ function CommandComboBox({
className="w-5 h-5 bg-primary/10 dark:bg-primary text-primary dark:text-inherit"
/>
<Combobox.Input
data-testid="cmd-bar-search"
onChange={(event) => setQuery(event.target.value)}
className="w-full bg-transparent focus:outline-none selection:bg-primary/20 dark:selection:bg-primary/40 dark:focus:outline-none"
onKeyDown={(event) => {
@ -60,7 +50,7 @@ function CommandComboBox({
(event.key === 'Backspace' && !event.currentTarget.value)
) {
event.preventDefault()
commandBarActor.send({ type: 'Close' })
commandBarSend({ type: 'Close' })
}
}}
placeholder={
@ -75,50 +65,34 @@ function CommandComboBox({
autoFocus
/>
</div>
{filteredOptions?.length ? (
<Combobox.Options
static
className="overflow-y-auto max-h-96 cursor-pointer"
>
{filteredOptions?.map((option) => (
<Combobox.Option
key={option.groupId + option.name + (option.displayName || '')}
value={option}
className="flex items-center gap-4 px-4 py-1.5 first:mt-2 last:mb-2 ui-active:bg-primary/10 dark:ui-active:bg-chalkboard-90 ui-disabled:!text-chalkboard-50"
disabled={optionIsDisabled(option)}
data-testid={`cmd-bar-option`}
>
{'icon' in option && option.icon && (
<CustomIcon name={option.icon} className="w-5 h-5" />
)}
<div className="flex-grow flex flex-col">
<p className="my-0 leading-tight">
{option.displayName || option.name}{' '}
<Combobox.Options
static
className="overflow-y-auto max-h-96 cursor-pointer"
>
{filteredOptions?.map((option) => (
<Combobox.Option
key={option.groupId + option.name + (option.displayName || '')}
value={option}
className="flex items-center gap-4 px-4 py-1.5 first:mt-2 last:mb-2 ui-active:bg-primary/10 dark:ui-active:bg-chalkboard-90"
>
{'icon' in option && option.icon && (
<CustomIcon name={option.icon} className="w-5 h-5" />
)}
<div className="flex-grow flex flex-col">
<p className="my-0 leading-tight">
{option.displayName || option.name}{' '}
</p>
{option.description && (
<p className="my-0 text-xs text-chalkboard-60 dark:text-chalkboard-50">
{option.description}
</p>
{option.description && (
<p className="my-0 text-xs text-chalkboard-60 dark:text-chalkboard-50">
{option.description}
</p>
)}
</div>
</Combobox.Option>
))}
</Combobox.Options>
) : (
<p className="px-4 pt-2 text-chalkboard-60 dark:text-chalkboard-50">
No results found
</p>
)}
)}
</div>
</Combobox.Option>
))}
</Combobox.Options>
</Combobox>
)
}
export default CommandComboBox
function optionIsDisabled(option: Command): boolean {
return (
'machineActor' in option &&
option.machineActor !== undefined &&
!getActorNextEvents(option.machineActor.getSnapshot()).includes(option.name)
)
}

View File

@ -538,16 +538,6 @@ const CustomIconMap = {
/>
</svg>
),
helix: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12.3796 6.35525C10.6758 5.64945 8.44129 5.27796 6.92519 5.64172C6.15726 5.82597 5.7318 6.05228 5.55779 6.21295C5.76304 6.32354 6.2288 6.43945 7.03653 6.43302C7.87009 6.42638 8.9975 6.29045 10.4229 5.9501L10.6551 6.92275C9.17724 7.27564 7.9725 7.42559 7.04449 7.43298C6.14216 7.44017 5.42343 7.31395 4.98579 7.03617C4.75792 6.89153 4.53857 6.65945 4.50435 6.32695C4.47054 5.99852 4.63374 5.72683 4.81912 5.53684C5.17998 5.16702 5.83926 4.87389 6.69188 4.66932C8.48928 4.23806 10.9508 4.68095 12.7623 5.43139C13.669 5.80697 14.4784 6.28567 14.9739 6.82869C15.2234 7.10197 15.4238 7.42493 15.4827 7.78937C15.5448 8.1741 15.4392 8.54567 15.1831 8.86785C14.9896 9.11133 14.6502 9.31092 14.327 9.47089C14.1575 9.55477 13.9707 9.63785 13.7736 9.71907C14.257 9.99254 14.6732 10.2984 14.9739 10.6279C15.2234 10.9011 15.4238 11.2241 15.4827 11.5885C15.5448 11.9733 15.4392 12.3448 15.1831 12.667C14.9896 12.9105 14.6502 13.1101 14.327 13.2701C14.1575 13.3539 13.9707 13.437 13.7735 13.5182C14.3755 13.8587 14.8991 14.2636 15.2067 14.7211L14.3767 15.2789C14.1912 15.0029 13.8109 14.6842 13.2483 14.3702C13.0112 14.2378 12.7496 14.1107 12.4694 13.9913C11.8027 14.2087 11.1417 14.3953 10.6642 14.5188L10.6552 14.5212L10.6551 14.5211C9.17724 14.874 7.9725 15.0239 7.04449 15.0313C6.14216 15.0385 5.42343 14.9123 4.98579 14.6345C4.75792 14.4899 4.53857 14.2578 4.50435 13.9253C4.47054 13.5969 4.63374 13.3252 4.81912 13.1352C5.17998 12.7653 5.83926 12.4722 6.69188 12.2677C8.12302 11.9243 9.96538 12.1368 11.5511 12.6039C11.5872 12.6145 11.6233 12.6253 11.6593 12.6363L10.0638 13.2745C8.93153 13.0645 7.80454 13.0291 6.92519 13.2401C6.15727 13.4243 5.73181 13.6506 5.5578 13.8113C5.76305 13.9219 6.2288 14.0378 7.03653 14.0313C7.8692 14.0247 8.99509 13.8891 10.4183 13.5495C10.5419 13.5175 10.678 13.4812 10.8233 13.4412C10.8184 13.4399 10.8134 13.4387 10.8085 13.4374L12.6 12.9L12.5922 12.8948C12.6584 12.8718 12.7243 12.8485 12.7894 12.825C13.2047 12.6754 13.5845 12.5217 13.8834 12.3738C14.2059 12.2142 14.359 12.0967 14.4003 12.0448C14.4964 11.9239 14.509 11.832 14.4955 11.748C14.4786 11.6437 14.4094 11.4927 14.2353 11.302C13.8963 10.9305 13.2766 10.536 12.4694 10.1921C11.8027 10.4096 11.1417 10.5962 10.6642 10.7197L10.6552 10.722L10.6551 10.7219C9.17724 11.0748 7.9725 11.2248 7.04449 11.2322C6.14216 11.2393 5.42343 11.1131 4.98579 10.8353C4.75792 10.6907 4.53857 10.4586 4.50435 10.1261C4.47054 9.79768 4.63374 9.526 4.81912 9.33601C5.17998 8.96618 5.83926 8.67306 6.69188 8.46848C8.14467 8.11991 10.0313 8.34242 11.6579 8.83682L10.0624 9.47503C8.9375 9.26666 7.80922 9.22878 6.92519 9.44089C6.15726 9.62514 5.7318 9.85144 5.55779 10.0121C5.76304 10.1227 6.2288 10.2386 7.03653 10.2322C7.86921 10.2255 8.9951 10.0899 10.4183 9.75035C10.542 9.71834 10.6781 9.68201 10.8235 9.64197L10.8072 9.63784L12.6 9.1L12.593 9.09536C12.659 9.0724 12.7245 9.04921 12.7894 9.02583C13.2047 8.87627 13.5845 8.72256 13.8834 8.57464C14.2059 8.41505 14.359 8.29757 14.4003 8.24564C14.4964 8.1247 14.509 8.0328 14.4955 7.94882C14.4786 7.84455 14.4094 7.69357 14.2353 7.50279C13.8839 7.11769 13.2307 6.7078 12.3796 6.35525ZM5.47539 9.95537C5.47546 9.95536 5.47623 9.95615 5.47745 9.95779C5.47592 9.9562 5.47531 9.95538 5.47539 9.95537ZM5.49369 10.0846C5.49289 10.0866 5.49232 10.0876 5.49223 10.0876C5.49215 10.0877 5.49255 10.0866 5.49369 10.0846ZM5.47539 13.7545C5.47546 13.7545 5.47623 13.7553 5.47745 13.757C5.47592 13.7554 5.47531 13.7546 5.47539 13.7545ZM5.49369 13.8838C5.49289 13.8858 5.49232 13.8868 5.49223 13.8868C5.49215 13.8868 5.49255 13.8858 5.49369 13.8838ZM5.47539 6.1562C5.47546 6.15619 5.47623 6.15698 5.47745 6.15862C5.47592 6.15704 5.47531 6.15622 5.47539 6.1562ZM5.49369 6.28544C5.49289 6.28746 5.49232 6.28848 5.49223 6.28848C5.49215 6.28849 5.49255 6.28748 5.49369 6.28544Z"
fill="currentColor"
/>
</svg>
),
hole: (
<svg
viewBox="0 0 20 20"

View File

@ -1,21 +1,24 @@
import { useMemo } from 'react'
import { engineCommandManager } from 'lib/singletons'
import { expandPlane, PlaneArtifactRich } from 'lang/std/artifactGraph'
import { ArtifactGraph } from 'lang/wasm'
import {
ArtifactGraph,
expandPlane,
PlaneArtifactRich,
} from 'lang/std/artifactGraph'
import { DebugDisplayArray, GenericObj } from './DebugDisplayObj'
export function DebugArtifactGraph() {
const artifactGraphTree = useMemo(() => {
export function DebugFeatureTree() {
const featureTree = useMemo(() => {
return computeTree(engineCommandManager.artifactGraph)
}, [engineCommandManager.artifactGraph])
const filterKeys: string[] = ['__meta', 'codeRef', 'pathToNode']
return (
<details data-testid="debug-feature-tree" className="relative">
<summary>Artifact Graph</summary>
{artifactGraphTree.length > 0 ? (
<summary>Feature Tree</summary>
{featureTree.length > 0 ? (
<pre className="text-xs">
<DebugDisplayArray arr={artifactGraphTree} filterKeys={filterKeys} />
<DebugDisplayArray arr={featureTree} filterKeys={filterKeys} />
</pre>
) : (
<p>(Empty)</p>

View File

@ -12,6 +12,7 @@ import {
StateFrom,
fromPromise,
} from 'xstate'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { fileMachine } from 'machines/fileMachine'
import { isDesktop } from 'lib/isDesktop'
import {
@ -29,8 +30,6 @@ import {
} from 'lib/getKclSamplesManifest'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { markOnce } from 'lib/performance'
import { commandBarActor } from 'machines/commandBarMachine'
import { useToken } from 'machines/appMachine'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
@ -48,10 +47,9 @@ export const FileMachineProvider = ({
children: React.ReactNode
}) => {
const navigate = useNavigate()
const { commandBarSend } = useCommandsContext()
const { settings } = useSettingsAuthContext()
const token = useToken()
const projectData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const { project, file } = projectData
const { project, file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const [kclSamples, setKclSamples] = React.useState<KclSamplesManifestItem[]>(
[]
)
@ -59,9 +57,7 @@ export const FileMachineProvider = ({
useEffect(() => {
markOnce('code/didLoadFile')
async function fetchKclSamples() {
const manifest = await getKclSamplesManifest()
const filteredFiles = manifest.filter((file) => !file.multipleFiles)
setKclSamples(filteredFiles)
setKclSamples(await getKclSamplesManifest())
}
fetchKclSamples().catch(reportError)
}, [])
@ -92,7 +88,7 @@ export const FileMachineProvider = ({
navigateToFile: ({ context, event }) => {
if (event.type !== 'xstate.done.actor.create-and-open-file') return
if (event.output && 'name' in event.output) {
commandBarActor.send({ type: 'Close' })
commandBarSend({ type: 'Close' })
navigate(
`..${PATHS.FILE}/${encodeURIComponent(
context.selectedDirectory +
@ -298,65 +294,55 @@ export const FileMachineProvider = ({
const kclCommandMemo = useMemo(
() =>
kclCommands({
authToken: token ?? '',
projectData,
settings: {
defaultUnit: settings?.context?.modeling.defaultUnit.current ?? 'mm',
},
specialPropsForSampleCommand: {
onSubmit: async (data) => {
if (data.method === 'overwrite') {
codeManager.updateCodeStateEditor(data.code)
await kclManager.executeCode(true)
await codeManager.writeToFile()
} else if (data.method === 'newFile' && isDesktop()) {
send({
type: 'Create file',
data: {
name: data.sampleName,
content: data.code,
makeDir: false,
},
})
}
kclCommands(
async (data) => {
if (data.method === 'overwrite') {
codeManager.updateCodeStateEditor(data.code)
await kclManager.executeCode(true)
await codeManager.writeToFile()
} else if (data.method === 'newFile' && isDesktop()) {
send({
type: 'Create file',
data: {
name: data.sampleName,
content: data.code,
makeDir: false,
},
})
}
// Either way, we want to overwrite the defaultUnit project setting
// with the sample's setting.
if (data.sampleUnits) {
settings.send({
type: 'set.modeling.defaultUnit',
data: {
level: 'project',
value: data.sampleUnits,
},
})
}
},
providedOptions: kclSamples.map((sample) => ({
value: sample.pathFromProjectDirectoryToFirstFile,
name: sample.title,
})),
// Either way, we want to overwrite the defaultUnit project setting
// with the sample's setting.
if (data.sampleUnits) {
settings.send({
type: 'set.modeling.defaultUnit',
data: {
level: 'project',
value: data.sampleUnits,
},
})
}
},
}).filter(
kclSamples.map((sample) => ({
value: sample.file,
name: sample.title,
}))
).filter(
(command) => kclSamples.length || command.name !== 'open-kcl-example'
),
[codeManager, kclManager, send, kclSamples]
)
useEffect(() => {
commandBarActor.send({
type: 'Add commands',
data: { commands: kclCommandMemo },
})
commandBarSend({ type: 'Add commands', data: { commands: kclCommandMemo } })
return () => {
commandBarActor.send({
commandBarSend({
type: 'Remove commands',
data: { commands: kclCommandMemo },
})
}
}, [commandBarActor.send, kclCommandMemo])
}, [commandBarSend, kclCommandMemo])
return (
<FileContext.Provider

View File

@ -148,7 +148,6 @@ function HelpMenuItem({
return (
<li className="p-0 m-0">
{as === 'a' ? (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<a
{...(props as React.ComponentProps<'a'>)}
onClick={openExternalBrowserIfDesktop(

View File

@ -27,7 +27,6 @@ import { PROJECT_ENTRYPOINT } from 'lib/constants'
import { err } from 'lib/trap'
import { isDesktop } from 'lib/isDesktop'
import { codeManager } from 'lib/singletons'
import { useToken } from 'machines/appMachine'
function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
return []
@ -70,7 +69,8 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const [isKclLspReady, setIsKclLspReady] = useState(false)
const [isCopilotLspReady, setIsCopilotLspReady] = useState(false)
const token = useToken()
const { auth } = useSettingsAuthContext()
const token = auth?.context.token
const navigate = useNavigate()
// So this is a bit weird, we need to initialize the lsp server and client.

View File

@ -1,11 +1,11 @@
import { createContext, useEffect, useState } from 'react'
import { engineCommandManager } from 'lib/singletons'
import { CommandsContext } from 'components/CommandBar/CommandBarProvider'
import { isDesktop } from 'lib/isDesktop'
import { components } from 'lib/machine-api'
import { reportRejection } from 'lib/trap'
import { toSync } from 'lib/utils'
import { commandBarActor } from 'machines/commandBarMachine'
export type MachinesListing = Array<
components['schemas']['MachineInfoResponse']
@ -42,6 +42,8 @@ export const MachineManagerProvider = ({
components['schemas']['MachineInfoResponse'] | null
>(null)
const commandBarActor = CommandsContext.useActorRef()
// Get the reason message for why there are no machines.
const noMachinesReason = (): string | undefined => {
if (machines.length > 0) {

View File

@ -1,4 +1,4 @@
import { useMachine, useSelector } from '@xstate/react'
import { useMachine } from '@xstate/react'
import React, {
createContext,
useEffect,
@ -11,7 +11,6 @@ import {
AnyStateMachine,
ContextFrom,
Prop,
SnapshotFrom,
StateFrom,
assign,
fromPromise,
@ -68,14 +67,18 @@ import {
startSketchOnDefault,
} from 'lang/modifyAst'
import { PathToNode, Program, parse, recast, resultIsOk } from 'lang/wasm'
import { artifactIsPlaneWithPaths, isSingleCursorInPipe } from 'lang/queryAst'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import {
artifactIsPlaneWithPaths,
getNodePathFromSourceRange,
isSingleCursorInPipe,
} from 'lang/queryAst'
import { exportFromEngine } from 'lib/exportFromEngine'
import { Models } from '@kittycad/lib/dist/types/src'
import toast from 'react-hot-toast'
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
import { err, reportRejection, trap } from 'lib/trap'
import { useCommandsContext } from 'hooks/useCommandsContext'
import {
ExportIntent,
EngineConnectionStateType,
@ -88,8 +91,6 @@ import { IndexLoaderData } from 'lib/types'
import { Node } from 'wasm-lib/kcl/bindings/Node'
import { promptToEditFlow } from 'lib/promptToEdit'
import { kclEditorActor } from 'machines/kclEditorMachine'
import { commandBarActor } from 'machines/commandBarMachine'
import { useToken } from 'machines/appMachine'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
@ -101,19 +102,16 @@ export const ModelingMachineContext = createContext(
{} as MachineContext<typeof modelingMachine>
)
const commandBarIsClosedSelector = (
state: SnapshotFrom<typeof commandBarActor>
) => state.matches('Closed')
export const ModelingMachineProvider = ({
children,
}: {
children: React.ReactNode
}) => {
const {
auth,
settings: {
context: {
app: { theme, enableSSAO, allowOrbitInSketchMode },
app: { theme, enableSSAO },
modeling: {
defaultUnit,
cameraProjection,
@ -123,21 +121,18 @@ export const ModelingMachineProvider = ({
},
},
} = useSettingsAuthContext()
const previousAllowOrbitInSketchMode = useRef(allowOrbitInSketchMode.current)
const navigate = useNavigate()
const { context, send: fileMachineSend } = useFileContext()
const { file } = useLoaderData() as IndexLoaderData
const token = useToken()
const token = auth?.context?.token
const streamRef = useRef<HTMLDivElement>(null)
const persistedContext = useMemo(() => getPersistedContext(), [])
let [searchParams] = useSearchParams()
const pool = searchParams.get('pool')
const isCommandBarClosed = useSelector(
commandBarActor,
commandBarIsClosedSelector
)
const { commandBarState, commandBarSend } = useCommandsContext()
// Settings machine setup
// const retrievedSettings = useRef(
// localStorage?.getItem(MODELING_PERSIST_KEY) || '{}'
@ -392,16 +387,7 @@ export const ModelingMachineProvider = ({
}
if (setSelections.selectionType === 'completeSelection') {
const codeMirrorSelection = editorManager.createEditorSelection(
setSelections.selection
)
kclEditorActor.send({
type: 'setLastSelectionEvent',
data: {
codeMirrorSelection,
scrollIntoView: false,
},
})
editorManager.selectRange(setSelections.selection)
if (!sketchDetails)
return {
selectionRanges: setSelections.selection,
@ -542,6 +528,7 @@ export const ModelingMachineProvider = ({
trimmedPrompt,
fileMachineSend,
navigate,
commandBarSend,
context,
token,
settings: {
@ -555,7 +542,7 @@ export const ModelingMachineProvider = ({
'has valid selection for deletion': ({
context: { selectionRanges },
}) => {
if (!isCommandBarClosed) return false
if (!commandBarState.matches('Closed')) return false
if (selectionRanges.graphSelections.length <= 0) return false
return true
},
@ -647,8 +634,7 @@ export const ModelingMachineProvider = ({
input.plane
)
await kclManager.updateAst(modifiedAst, false)
sceneInfra.camControls.enableRotate =
sceneInfra.camControls._setting_allowOrbitInSketchMode
sceneInfra.camControls.enableRotate = false
sceneInfra.camControls.syncDirection = 'clientToEngine'
await letEngineAnimateAndSyncCamAfter(
@ -661,7 +647,6 @@ export const ModelingMachineProvider = ({
zAxis: input.zAxis,
yAxis: input.yAxis,
origin: [0, 0, 0],
animateTargetId: input.planeId,
}
}),
'animate-to-sketch': fromPromise(
@ -686,7 +671,6 @@ export const ModelingMachineProvider = ({
origin: info.sketchDetails.origin.map(
(a) => a / sceneInfra._baseUnitMultiplier
) as [number, number, number],
animateTargetId: info?.sketchDetails?.faceId || '',
}
}
),
@ -1204,41 +1188,6 @@ export const ModelingMachineProvider = ({
}
}, [engineCommandManager.engineConnection, modelingSend])
useEffect(() => {
// Only trigger this if the state actually changes, if it stays the same do not reload the camera
if (
previousAllowOrbitInSketchMode.current === allowOrbitInSketchMode.current
) {
//no op
previousAllowOrbitInSketchMode.current = allowOrbitInSketchMode.current
return
}
const inSketchMode = modelingState.matches('Sketch')
// If you are in sketch mode and you disable the orbit, return back to the normal view to the target
if (!allowOrbitInSketchMode.current) {
const targetId = modelingState.context.sketchDetails?.animateTargetId
if (inSketchMode && targetId) {
letEngineAnimateAndSyncCamAfter(engineCommandManager, targetId)
.then(() => {})
.catch((e) => {
console.error(
'failed to sync engine and client scene after disabling allow orbit in sketch mode'
)
console.error(e)
})
}
}
// While you are in sketch mode you should be able to control the enable rotate
// Once you exit it goes back to normal
if (inSketchMode) {
sceneInfra.camControls.enableRotate = allowOrbitInSketchMode.current
}
previousAllowOrbitInSketchMode.current = allowOrbitInSketchMode.current
}, [allowOrbitInSketchMode])
// Allow using the delete key to delete solids
useHotkeys(['backspace', 'delete', 'del'], () => {
modelingSend({ type: 'Delete selection' })

View File

@ -1,4 +1,4 @@
import { DebugArtifactGraph } from 'components/DebugArtifactGraph'
import { DebugFeatureTree } from 'components/DebugFeatureTree'
import { AstExplorer } from '../../AstExplorer'
import { EngineCommands } from '../../EngineCommands'
import { CamDebugSettings } from 'clientSideScene/ClientSideSceneComp'
@ -14,7 +14,7 @@ export const DebugPane = () => {
<EngineCommands />
<CamDebugSettings />
<AstExplorer />
<DebugArtifactGraph />
<DebugFeatureTree />
</div>
</section>
</div>

View File

@ -3,7 +3,6 @@
@apply font-mono !no-underline text-xs font-bold select-none text-chalkboard-90;
@apply ui-active:bg-primary/10 ui-active:text-primary ui-active:text-inherit;
@apply transition-colors ease-out;
@apply m-0;
}
:global(.dark) .button {

View File

@ -9,15 +9,15 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { kclManager } from 'lib/singletons'
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { reportRejection } from 'lib/trap'
import { commandBarActor } from 'machines/commandBarMachine'
import { useCommandsContext } from 'hooks/useCommandsContext'
export const KclEditorMenu = ({ children }: PropsWithChildren) => {
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
useConvertToVariable()
const { commandBarSend } = useCommandsContext()
return (
<Menu>
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
<div
className="relative"
onClick={(e) => {
@ -84,7 +84,7 @@ export const KclEditorMenu = ({ children }: PropsWithChildren) => {
<Menu.Item>
<button
onClick={() => {
commandBarActor.send({
commandBarSend({
type: 'Find and select command',
data: {
groupId: 'code',

Some files were not shown because too many files have changed in this diff Show More