Compare commits

...

34 Commits

Author SHA1 Message Date
199d891f82 Merge remote-tracking branch 'origin/main' into paultag/refgraph 2025-01-23 10:42:26 -05:00
8ef31a0be1 Refactor: decouple command palette actor from React (#5108)
* Convert commandBarMachine to standalone actor

* Switch all uses of CommandBarProvider pattern to use actor and selector snapshots directly
2025-01-23 10:25:21 -05:00
3adb42b5f2 Supress stdio logs on e2e tests in CI (#5132)
* WIP: pw log error only

* Force tests to run on branch

* Remove all page.on('console', console.log)

* Remove context.console too

* Add --quiet flag

* Revert useless changes

* Supress stdio logs on e2e tests in CI
2025-01-23 16:13:49 +01:00
20016b101e Fix: Properly setting selection range when KCL editor is not mounted. (#4960)
* fix: fixed selection range issue when doing a constraint when the KCL editor is closed

* fix: linter and tsc errors

* fix: trying to reuse logic instead?

* fix: removed console log
2025-01-23 09:45:45 -05:00
8d9dbf36c3 Bump vite from 5.4.6 to 5.4.12 (#5129)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.6 to 5.4.12.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.12/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.12/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-22 20:58:53 -05:00
440704ed9f Remove extra margin on some code editor menu items (#5094)
* Extra padding on 'Load a sample model' menu item
Fixes #5047

* Update src/components/ModelingSidebar/ModelingPanes/KclEditorMenu.module.css

Co-authored-by: Frank Noirot <frank@zoo.dev>

---------

Co-authored-by: Frank Noirot <frank@zoo.dev>
2025-01-22 16:57:27 +01:00
2261217a5d Rename debug pane label for artifact graph (#5092)
* Rename debug pane label for artifact graph

* Rename component
2025-01-22 15:37:51 +00:00
10da986649 Add dry-run validation for Sweep (#5097)
* Add dry-run validation for Sweep
Fixes #5095

* Add sweep test failing validation

* Make naming more consistent with engine

* Fix tests after big rename

* Fix tsc after main merge
2025-01-22 15:59:47 +01:00
10789d9c3c set scene units based on a module's default units (#5127)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-01-22 15:23:55 +13:00
67cc4f5835 Tweaks to clarify tooltips from tool dropdown menus (#5123)
* Separate content from ToolbarItemTooltip, make simple and "rich" versions

* Add support for dropdown-arrow-only tooltip

* Add toolbar-wide hover timeouts and clears to switch between simple and rich tooltips

* Fix the dropdown arrow button hover styling now that they're separate

* Add missing doc links to rich toolbar tooltips

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Re-run CI after snapshots

* fix codespell

* fmt

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-21 18:32:56 -05:00
2692f2b73a Add units to geometry structs (#5075)
* Make all geometry KclValue variants into struct variants

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Add units to geometry types

Signed-off-by: Nick Cameron <nrc@ncameron.org>

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-01-21 20:42:09 +00:00
965cb18059 Parse units on numeric literals and keep them in the AST (#5061)
* Code changes

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* test changes

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Frontend changes

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Refactor asNum

Co-authored-by: Jonathan Tran <jonnytran@gmail.com>

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2025-01-22 08:29:30 +13:00
a022b8ef6c Fix suggestion for updating function decl syntax for anon functions (#5088)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-01-22 07:10:07 +13:00
4d24bf7c94 Add API Call ID log for debugging (#5107) 2025-01-20 19:49:02 +00:00
9a537da183 Show toolbar tooltips on hover only, hide when dropdowns are open (#5109)
* Show toolbar tooltips on hover only, hide when dropdowns are open

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Re-run CI

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-18 05:22:22 -05:00
df81b76b8b Bug fix follow-up for create project (#5105)
* fix dumb mistake in command flow for #5083

* Add e2e test for creating projects with the default interpolated name

* Drop that number to 12 ain't got all day

* Why do I have a kcl-samples submodule hanging around?

* Empty commit to remove the submodule
2025-01-17 23:10:28 +00:00
ac3f7ab712 Rust: Remove iai benchmark tests (#5102)
We don't get much value from these, we can always run criterion or valgrind locally.

If we want to measure instruction counts, we should be using codspeed.io instead because
they support visualizing and tracking over time.

If we want to track performance over time we should be using Kevin's perf monitor machine.
2025-01-17 15:42:51 -06:00
f04eafc7bf Merge remote-tracking branch 'origin/main' into paultag/refgraph 2025-01-17 09:52:42 -05:00
c162f2909f ok going to get to work on the minimizer now 2025-01-16 15:14:21 -05:00
3eb4625b8f Merge remote-tracking branch 'origin/main' into paultag/refgraph 2025-01-16 11:26:28 -05:00
f1ba113893 ignore program for now 2025-01-15 13:28:03 -05:00
872e9a9c4e Merge remote-tracking branch 'origin/main' into paultag/refgraph 2025-01-15 12:59:46 -05:00
171e5536e7 Merge remote-tracking branch 'origin/main' into paultag/refgraph 2025-01-14 11:37:41 -05:00
f984dad87f path start 2025-01-10 16:00:53 -05:00
84d9e76926 Merge remote-tracking branch 'origin/main' into paultag/refgraph 2025-01-09 16:09:02 -05:00
a17610a1f0 Merge remote-tracking branch 'origin/main' into paultag/refgraph 2025-01-06 14:52:36 -05:00
d2f52f4c7f Merge remote-tracking branch 'origin/main' into paultag/refgraph 2025-01-06 13:48:44 -05:00
b2fb0c10ca appendy bits 2024-12-19 12:54:31 -05:00
ea57c523b7 start to hack on diffybits 2024-12-19 12:32:11 -05:00
216f55c571 remove diff 2024-12-19 11:16:07 -05:00
9329036920 Merge remote-tracking branch 'origin/main' into paultag/refgraph 2024-12-19 11:15:08 -05:00
5d04dd4ef8 add program into edge 2024-12-19 11:14:53 -05:00
82ab3e9ba9 Merge remote-tracking branch 'origin/main' into paultag/refgraph 2024-12-18 14:05:44 -05:00
c72e7d9363 refgraph initial 2024-12-18 14:05:32 -05:00
306 changed files with 100284 additions and 70410 deletions

View File

@ -1,44 +0,0 @@
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}}

File diff suppressed because it is too large Load Diff

28
docs/kcl/types/Face.md Normal file
View File

@ -0,0 +1,28 @@
---
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,6 +20,7 @@ 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

@ -20,6 +20,7 @@ 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,7 +168,6 @@ Any KCL value.
----
A plane.
**Type:** `object`
@ -181,17 +180,10 @@ A plane.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Plane`](/docs/kcl/types/Plane)| | 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 |
| `value` |[`Plane`](/docs/kcl/types/Plane)| Any KCL value. | No |
----
A face.
**Type:** `object`
@ -203,14 +195,8 @@ A face.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `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 |
| `type` |enum: [`Face`](/docs/kcl/types/Face)| | No |
| `value` |[`Face`](/docs/kcl/types/Face)| Any KCL value. | No |
----
@ -246,7 +232,6 @@ A face.
----
An solid is a collection of extrude surfaces.
**Type:** `object`
@ -259,14 +244,7 @@ An solid is a collection of extrude surfaces.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Solid`](/docs/kcl/types/Solid)| | 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 |
| `value` |[`Solid`](/docs/kcl/types/Solid)| Any KCL value. | No |
----
@ -286,7 +264,6 @@ An solid is a collection of extrude surfaces.
----
A helix.
**Type:** `object`
@ -299,11 +276,7 @@ A helix.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Helix`](/docs/kcl/types/Helix)| | 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 |
| `value` |[`Helix`](/docs/kcl/types/Helix)| Any KCL value. | No |
----

View File

@ -22,6 +22,7 @@ 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,6 +21,7 @@ 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,6 +30,7 @@ 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,6 +31,7 @@ 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 |
@ -54,6 +55,7 @@ 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,6 +23,7 @@ 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,6 +32,7 @@ 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 |

107
docs/kcl/types/UnitLen.md Normal file
View File

@ -0,0 +1,107 @@
---
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

@ -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 feature tree.
// Scroll to the artifact graph.
await tree.scrollIntoViewIfNeeded()
// Expand the feature tree.
await tree.getByText('Feature Tree').click()
// Expand the artifact graph.
await tree.getByText('Artifact Graph').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 feature tree.
// Extract the artifact IDs from the debug artifact graph.
const initialSegmentIds = await segment.innerText({ timeout: 5_000 })
// The artifact ID should include a UUID.
expect(initialSegmentIds).toMatch(

View File

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

View File

@ -963,37 +963,31 @@ sketch002 = startSketchOn('XZ')
await toolbar.sweepButton.click()
await cmdBar.expectState({
commandName: 'Sweep',
currentArgKey: 'profile',
currentArgKey: 'target',
currentArgValue: '',
headerArguments: {
Path: '',
Profile: '',
Target: '',
Trajectory: '',
},
highlightedHeaderArg: 'profile',
highlightedHeaderArg: 'target',
stage: 'arguments',
})
await clickOnSketch1()
await cmdBar.expectState({
commandName: 'Sweep',
currentArgKey: 'path',
currentArgKey: 'trajectory',
currentArgValue: '',
headerArguments: {
Path: '',
Profile: '1 face',
Target: '1 face',
Trajectory: '',
},
highlightedHeaderArg: 'path',
highlightedHeaderArg: 'trajectory',
stage: 'arguments',
})
await clickOnSketch2()
await cmdBar.expectState({
commandName: 'Sweep',
headerArguments: {
Path: '1 face',
Profile: '1 face',
},
stage: 'review',
})
await page.waitForTimeout(500)
await cmdBar.progressCmdBar()
await page.waitForTimeout(500)
})
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
@ -1020,6 +1014,75 @@ 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 provided selection')
).toBeVisible()
})
})
test(`Fillet point-and-click`, async ({
context,
page,

View File

@ -1088,6 +1088,25 @@ test.describe(`Project management commands`, () => {
})
})
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(
'File in the file pane should open with a single click',
{ tag: '@electron' },

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

@ -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\"",
"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: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: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'",
@ -201,7 +201,7 @@
"ts-node": "^10.0.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.19.1",
"vite": "^5.4.6",
"vite": "^5.4.12",
"vite-plugin-package-version": "^1.1.0",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.6.0",

View File

@ -31,7 +31,6 @@ 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'
@ -58,23 +57,21 @@ 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: (
<CommandBarProvider>
<RouteProvider>
<SettingsAuthProvider>
<LspProvider>
<ProjectsContextProvider>
<KclContextProvider>
<AppStateProvider>
<MachineManagerProvider>
<Outlet />
</MachineManagerProvider>
</AppStateProvider>
</KclContextProvider>
</ProjectsContextProvider>
</LspProvider>
</SettingsAuthProvider>
</RouteProvider>
</CommandBarProvider>
<RouteProvider>
<SettingsAuthProvider>
<LspProvider>
<ProjectsContextProvider>
<KclContextProvider>
<AppStateProvider>
<MachineManagerProvider>
<Outlet />
</MachineManagerProvider>
</AppStateProvider>
</KclContextProvider>
</ProjectsContextProvider>
</LspProvider>
</SettingsAuthProvider>
</RouteProvider>
),
errorElement: <ErrorPage />,
children: [

View File

@ -1,8 +1,7 @@
import { useRef, useMemo, memo } from 'react'
import { useRef, useMemo, memo, useCallback, useState } 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'
@ -22,20 +21,19 @@ 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 hover:!border-chalkboard-20 dark:enabled:hover:!border-primary pressed:!border-primary ui-open:!border-primary'
const buttonBorderClassName = '!border-transparent'
const sketchPathId = useMemo(() => {
if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast))
@ -50,6 +48,7 @@ export function Toolbar({
const { overallState } = useNetworkContext()
const { isExecuting } = useKclContext()
const { isStreamReady } = useAppState()
const [showRichContent, setShowRichContent] = useState(false)
const disableAllButtons =
(overallState !== NetworkHealthState.Ok &&
@ -71,12 +70,45 @@ export function Toolbar({
() => ({
modelingState: state,
modelingSend: send,
commandBarSend,
sketchPathId,
}),
[state, send, commandBarSend, sketchPathId]
[state, send, commandBarActor.send, 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
@ -174,43 +206,64 @@ export function Toolbar({
status: itemConfig.status,
}))}
>
<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)
}
<div
className="contents"
// Mouse events do not fire on disabled buttons
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<span
className={!maybeIconConfig[0].showTitle ? 'sr-only' : ''}
<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)
}
>
{maybeIconConfig[0].title}
</span>
</ActionButton>
<ToolbarItemTooltip
itemConfig={maybeIconConfig[0]}
configCallbackProps={configCallbackProps}
/>
<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>
</ActionButtonDropdown>
)
}
@ -218,7 +271,13 @@ export function Toolbar({
// A single button
return (
<div className="relative" key={itemConfig.id}>
<div
className="relative"
key={itemConfig.id}
// Mouse events do not fire on disabled buttons
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<ActionButton
Element="button"
key={itemConfig.id}
@ -255,7 +314,18 @@ 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>
)
})}
@ -269,6 +339,12 @@ 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
@ -277,12 +353,10 @@ export function Toolbar({
const ToolbarItemTooltip = memo(function ToolbarItemContents({
itemConfig,
configCallbackProps,
}: {
itemConfig: ToolbarItemResolved
configCallbackProps: ToolbarItemCallbackProps
}) {
const { state } = useModelingContext()
wrapperClassName = '',
contentClassName = '',
children,
}: ToolbarItemContentsProps) {
useHotkeys(
itemConfig.hotkey || '',
() => {
@ -305,11 +379,50 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
? ({ '-webkit-app-region': 'no-drag' } as React.CSSProperties)
: {}
}
hoverOnly
position="bottom"
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"
wrapperClassName={'!p-4 !pointer-events-auto ' + wrapperClassName}
contentClassName={contentClassName}
delay={0}
>
{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'
@ -378,6 +491,6 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
</ul>
</>
)}
</Tooltip>
</>
)
})
}

View File

@ -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)
@ -510,7 +510,6 @@ const ConstraintSymbol = ({
constrainInfo: ConstrainInfo
verticalPosition: 'top' | 'bottom'
}) => {
const { commandBarSend } = useCommandsContext()
const { context } = useModelingContext()
const varNameMap: {
[key in ConstrainInfo['type']]: {
@ -630,7 +629,7 @@ const ConstraintSymbol = ({
// disabled={implicitDesc} TODO why does this change styles that are hard to override?
onClick={toSync(async () => {
if (!isConstrained) {
commandBarSend({
commandBarActor.send({
type: 'Find and select command',
data: {
name: 'Constrain with named value',
@ -756,7 +755,6 @@ export const CamDebugSettings = () => {
sceneInfra.camControls.reactCameraProperties
)
const [fov, setFov] = useState(12)
const { commandBarSend } = useCommandsContext()
useEffect(() => {
sceneInfra.camControls.setReactCameraPropertiesCallback(setCamSettings)
@ -775,7 +773,7 @@ export const CamDebugSettings = () => {
type="checkbox"
checked={camSettings.type === 'perspective'}
onChange={() =>
commandBarSend({
commandBarActor.send({
type: 'Find and select command',
data: {
groupId: 'settings',

View File

@ -1398,23 +1398,23 @@ export class SceneEntities {
const arg0 = arg(kclCircle3PointArgs[0])
if (!arg0) return kclManager.ast
arg0[0].value = points[0].x
arg0[0].value = { value: points[0].x, suffix: 'None' }
arg0[0].raw = points[0].x.toString()
arg0[1].value = points[0].y
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 = points[1].x
arg1[0].value = { value: points[1].x, suffix: 'None' }
arg1[0].raw = points[1].x.toString()
arg1[1].value = points[1].y
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 = points[2].x
arg2[0].value = { value: points[2].x, suffix: 'None' }
arg2[0].raw = points[2].x.toString()
arg2[1].value = points[2].y
arg2[1].value = { value: points[2].y, suffix: 'None' }
arg2[1].raw = points[2].y.toString()
const astSnapshot = structuredClone(kclManager.ast)
@ -2051,8 +2051,8 @@ export class SceneEntities {
)
if (!(sk instanceof Reason)) {
sketch = sk
} else if ((maybeSketch as Solid).sketch) {
sketch = (maybeSketch as Solid).sketch
} else if (maybeSketch && (maybeSketch.value as Solid)?.sketch) {
sketch = (maybeSketch.value as Solid).sketch
}
if (!sketch) return
@ -2541,7 +2541,7 @@ export function sketchFromPathToNode({
const varDec = _varDec.node
const result = programMemory.get(varDec?.id?.name || '')
if (result?.type === 'Solid') {
return result.sketch
return result.value.sketch
}
const sg = sketchFromKclValue(result, varDec?.id?.name)
if (err(sg)) {

View File

@ -61,6 +61,7 @@ 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
@ -847,7 +848,7 @@ function createLengthIndicator({
})
// Command Bar
editorManager.commandBarSend({
commandBarActor.send({
type: 'Find and select command',
data: {
name: 'Constrain length',

View File

@ -1,9 +1,11 @@
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
@ -17,6 +19,7 @@ type ActionButtonSplitProps = ActionButtonProps & { Element: 'button' } & {
export function ActionButtonDropdown({
splitMenuItems,
className,
dropdownTooltipText = 'More tools',
children,
...props
}: ActionButtonSplitProps) {
@ -26,7 +29,14 @@ export function ActionButtonDropdown({
{({ close }) => (
<>
{children}
<Popover.Button className="border-transparent dark:border-transparent p-0 m-0 rounded-none !outline-none ui-open:border-primary ui-open:bg-primary">
<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'
}
>
<CustomIcon
name="caretDown"
className={
@ -37,6 +47,14 @@ 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

@ -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 { commandBarSend, commandBarState } = useCommandsContext()
const commandBarState = useCommandBarState()
const resolvedOptions = useMemo(
() =>
typeof arg.options === 'function'
@ -142,7 +142,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')
commandBarSend({ type: 'Close' })
commandBarActor.send({ type: 'Close' })
if (event.key === 'Backspace' && !event.currentTarget.value) {
stepBack()
}

View File

@ -1,6 +1,5 @@
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'
@ -8,12 +7,13 @@ 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, commandBarSend } = useCommandsContext()
const commandBarState = useCommandBarState()
const {
context: { selectedCommand, currentArgument, commands },
} = commandBarState
@ -23,16 +23,16 @@ export const CommandBar = () => {
// Close the command bar when navigating
useEffect(() => {
if (commandBarState.matches('Closed')) return
commandBarSend({ type: 'Close' })
commandBarActor.send({ type: 'Close' })
}, [pathname])
// Hook up keyboard shortcuts
useHotkeyWrapper([COMMAND_PALETTE_HOTKEY], () => {
if (commandBarState.context.commands.length === 0) return
if (commandBarState.matches('Closed')) {
commandBarSend({ type: 'Open' })
commandBarActor.send({ type: 'Open' })
} else {
commandBarSend({ type: 'Close' })
commandBarActor.send({ type: 'Close' })
}
})
@ -52,14 +52,14 @@ export const CommandBar = () => {
...entries[entries.length - 1][1],
}
commandBarSend({
commandBarActor.send({
type: 'Edit argument',
data: {
arg: currentArg,
},
})
} else {
commandBarSend({ type: 'Deselect command' })
commandBarActor.send({ type: 'Deselect command' })
}
} else {
const entries = Object.entries(selectedCommand?.args || {})
@ -68,9 +68,9 @@ export const CommandBar = () => {
)
if (index === 0) {
commandBarSend({ type: 'Deselect command' })
commandBarActor.send({ type: 'Deselect command' })
} else {
commandBarSend({
commandBarActor.send({
type: 'Change current argument',
data: {
arg: { name: entries[index - 1][0], ...entries[index - 1][1] },
@ -85,14 +85,14 @@ export const CommandBar = () => {
show={!commandBarState.matches('Closed') || false}
afterLeave={() => {
if (selectedCommand?.onCancel) selectedCommand.onCancel()
commandBarSend({ type: 'Clear' })
commandBarActor.send({ type: 'Clear' })
}}
as={Fragment}
>
<WrapperComponent
open={!commandBarState.matches('Closed') || isSelectionArgument}
onClose={() => {
commandBarSend({ type: 'Close' })
commandBarActor.send({ type: 'Close' })
}}
className={
'fixed inset-0 z-50 overflow-y-auto pb-4 pt-1 ' +
@ -122,7 +122,7 @@ export const CommandBar = () => {
)
)}
<button
onClick={() => commandBarSend({ type: 'Close' })}
onClick={() => commandBarActor.send({ 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, commandBarSend } = useCommandsContext()
const commandBarState = useCommandBarState()
const {
context: { currentArgument },
} = commandBarState
@ -16,7 +16,7 @@ function CommandBarArgument({ stepBack }: { stepBack: () => void }) {
function onSubmit(data: unknown) {
if (!currentArgument) return
commandBarSend({
commandBarActor.send({
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 { commandBarSend, commandBarState } = useCommandsContext()
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
const commandBarState = useCommandBarState()
useHotkeys('mod + k, mod + /', () => commandBarActor.send({ type: 'Close' }))
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {

View File

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

View File

@ -7,7 +7,6 @@ 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'
@ -20,6 +19,7 @@ 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 { commandBarSend, commandBarState } = useCommandsContext()
const commandBarState = useCommandBarState()
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 + /', () => commandBarSend({ type: 'Close' }))
useHotkeys('mod + k, mod + /', () => commandBarActor.send({ type: 'Close' }))
const editorRef = useRef<HTMLDivElement>(null)
const {

View File

@ -1,43 +0,0 @@
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 { useCommandsContext } from 'hooks/useCommandsContext'
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
import CommandBarHeader from './CommandBarHeader'
import { useHotkeys } from 'react-hotkeys-hook'
function CommandBarReview({ stepBack }: { stepBack: () => void }) {
const { commandBarState, commandBarSend } = useCommandsContext()
const commandBarState = useCommandBarState()
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]
commandBarSend({
commandBarActor.send({
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()
commandBarSend({
commandBarActor.send({
type: 'Submit command',
output: argumentsToSubmit,
})

View File

@ -1,5 +1,4 @@
import { useSelector } from '@xstate/react'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { Artifact } from 'lang/std/artifactGraph'
import { CommandArgument } from 'lib/commandTypes'
import {
@ -10,6 +9,7 @@ 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'
@ -49,7 +49,7 @@ function CommandBarSelectionInput({
onSubmit: (data: unknown) => void
}) {
const inputRef = useRef<HTMLInputElement>(null)
const { commandBarState, commandBarSend } = useCommandsContext()
const commandBarState = useCommandBarState()
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') {
commandBarSend({ type: 'Close' })
commandBarActor.send({ 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 { commandBarSend, commandBarState } = useCommandsContext()
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
const commandBarState = useCommandBarState()
useHotkeys('mod + k, mod + /', () => commandBarActor.send({ type: 'Close' }))
const formRef = useRef<HTMLFormElement>(null)
const inputRef = useRef<HTMLTextAreaElement>(null)
useTextareaAutoGrow(inputRef)

View File

@ -1,16 +1,15 @@
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={() => commandBarSend({ type: 'Open' })}
onClick={() => commandBarActor.send({ type: 'Open' })}
data-testid="command-bar-open-button"
>
<span>Commands</span>

View File

@ -1,11 +1,11 @@
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,7 +14,6 @@ function CommandComboBox({
options: Command[]
placeholder?: string
}) {
const { commandBarSend } = useCommandsContext()
const [query, setQuery] = useState('')
const [filteredOptions, setFilteredOptions] = useState<typeof options>()
@ -41,7 +40,7 @@ function CommandComboBox({
}, [query])
function handleSelection(command: Command) {
commandBarSend({ type: 'Select command', data: { command } })
commandBarActor.send({ type: 'Select command', data: { command } })
}
return (
@ -61,7 +60,7 @@ function CommandComboBox({
(event.key === 'Backspace' && !event.currentTarget.value)
) {
event.preventDefault()
commandBarSend({ type: 'Close' })
commandBarActor.send({ type: 'Close' })
}
}}
placeholder={

View File

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

View File

@ -12,7 +12,6 @@ import {
StateFrom,
fromPromise,
} from 'xstate'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { fileMachine } from 'machines/fileMachine'
import { isDesktop } from 'lib/isDesktop'
import {
@ -30,6 +29,7 @@ import {
} from 'lib/getKclSamplesManifest'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { markOnce } from 'lib/performance'
import { commandBarActor } from 'machines/commandBarMachine'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
@ -47,7 +47,6 @@ export const FileMachineProvider = ({
children: React.ReactNode
}) => {
const navigate = useNavigate()
const { commandBarSend } = useCommandsContext()
const { settings } = useSettingsAuthContext()
const { project, file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const [kclSamples, setKclSamples] = React.useState<KclSamplesManifestItem[]>(
@ -90,7 +89,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) {
commandBarSend({ type: 'Close' })
commandBarActor.send({ type: 'Close' })
navigate(
`..${PATHS.FILE}/${encodeURIComponent(
context.selectedDirectory +
@ -336,15 +335,18 @@ export const FileMachineProvider = ({
)
useEffect(() => {
commandBarSend({ type: 'Add commands', data: { commands: kclCommandMemo } })
commandBarActor.send({
type: 'Add commands',
data: { commands: kclCommandMemo },
})
return () => {
commandBarSend({
commandBarActor.send({
type: 'Remove commands',
data: { commands: kclCommandMemo },
})
}
}, [commandBarSend, kclCommandMemo])
}, [commandBarActor.send, kclCommandMemo])
return (
<FileContext.Provider

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,8 +42,6 @@ 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 } from '@xstate/react'
import { useMachine, useSelector } from '@xstate/react'
import React, {
createContext,
useEffect,
@ -11,6 +11,7 @@ import {
AnyStateMachine,
ContextFrom,
Prop,
SnapshotFrom,
StateFrom,
assign,
fromPromise,
@ -78,7 +79,6 @@ 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,
@ -91,6 +91,7 @@ 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'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
@ -102,6 +103,10 @@ export const ModelingMachineContext = createContext(
{} as MachineContext<typeof modelingMachine>
)
const commandBarIsClosedSelector = (
state: SnapshotFrom<typeof commandBarActor>
) => state.matches('Closed')
export const ModelingMachineProvider = ({
children,
}: {
@ -132,8 +137,10 @@ export const ModelingMachineProvider = ({
let [searchParams] = useSearchParams()
const pool = searchParams.get('pool')
const { commandBarState, commandBarSend } = useCommandsContext()
const isCommandBarClosed = useSelector(
commandBarActor,
commandBarIsClosedSelector
)
// Settings machine setup
// const retrievedSettings = useRef(
// localStorage?.getItem(MODELING_PERSIST_KEY) || '{}'
@ -388,7 +395,16 @@ export const ModelingMachineProvider = ({
}
if (setSelections.selectionType === 'completeSelection') {
editorManager.selectRange(setSelections.selection)
const codeMirrorSelection = editorManager.createEditorSelection(
setSelections.selection
)
kclEditorActor.send({
type: 'setLastSelectionEvent',
data: {
codeMirrorSelection,
scrollIntoView: false,
},
})
if (!sketchDetails)
return {
selectionRanges: setSelections.selection,
@ -529,7 +545,6 @@ export const ModelingMachineProvider = ({
trimmedPrompt,
fileMachineSend,
navigate,
commandBarSend,
context,
token,
settings: {
@ -543,7 +558,7 @@ export const ModelingMachineProvider = ({
'has valid selection for deletion': ({
context: { selectionRanges },
}) => {
if (!commandBarState.matches('Closed')) return false
if (!isCommandBarClosed) return false
if (selectionRanges.graphSelections.length <= 0) return false
return true
},

View File

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

View File

@ -3,6 +3,7 @@
@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,12 +9,11 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { kclManager } from 'lib/singletons'
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { reportRejection } from 'lib/trap'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { commandBarActor } from 'machines/commandBarMachine'
export const KclEditorMenu = ({ children }: PropsWithChildren) => {
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
useConvertToVariable()
const { commandBarSend } = useCommandsContext()
return (
<Menu>
@ -85,7 +84,7 @@ export const KclEditorMenu = ({ children }: PropsWithChildren) => {
<Menu.Item>
<button
onClick={() => {
commandBarSend({
commandBarActor.send({
type: 'Find and select command',
data: {
groupId: 'code',

View File

@ -95,9 +95,11 @@ export const processMemory = (programMemory: ProgramMemory) => {
) {
const sk = sketchFromKclValueOptional(val, key)
if (val.type === 'Solid') {
processedMemory[key] = val.value.map(({ ...rest }: ExtrudeSurface) => {
return rest
})
processedMemory[key] = val.value.value.map(
({ ...rest }: ExtrudeSurface) => {
return rest
}
)
} else if (!(sk instanceof Reason)) {
processedMemory[key] = sk.paths.map(({ __geoMeta, ...rest }: Path) => {
return rest

View File

@ -15,12 +15,12 @@ import { ModelingPane } from './ModelingPane'
import { isDesktop } from 'lib/isDesktop'
import { useModelingContext } from 'hooks/useModelingContext'
import { CustomIconName } from 'components/CustomIcon'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
import { useKclContext } from 'lang/KclProvider'
import { MachineManagerContext } from 'components/MachineManagerProvider'
import { onboardingPaths } from 'routes/Onboarding/paths'
import { SIDEBAR_BUTTON_SUFFIX } from 'lib/constants'
import { commandBarActor } from 'machines/commandBarMachine'
interface ModelingSidebarProps {
paneOpacity: '' | 'opacity-20' | 'opacity-40'
@ -37,7 +37,6 @@ function getPlatformString(): 'web' | 'desktop' {
export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
const machineManager = useContext(MachineManagerContext)
const { commandBarSend } = useCommandsContext()
const kclContext = useKclContext()
const { settings } = useSettingsAuthContext()
const onboardingStatus = settings.context.app.onboardingStatus
@ -66,7 +65,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
icon: 'floppyDiskArrow',
keybinding: 'Ctrl + Shift + E',
action: () =>
commandBarSend({
commandBarActor.send({
type: 'Find and select command',
data: { name: 'Export', groupId: 'modeling' },
}),
@ -79,7 +78,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
keybinding: 'Ctrl + Shift + M',
// eslint-disable-next-line @typescript-eslint/no-misused-promises
action: async () => {
commandBarSend({
commandBarActor.send({
type: 'Find and select command',
data: { name: 'Make', groupId: 'modeling' },
})

View File

@ -1,7 +1,6 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { BrowserRouter } from 'react-router-dom'
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
import { CommandBarProvider } from './CommandBar/CommandBarProvider'
import {
NETWORK_HEALTH_TEXT,
NetworkHealthIndicator,
@ -12,9 +11,7 @@ function TestWrap({ children }: { children: React.ReactNode }) {
// wrap in router and xState context
return (
<BrowserRouter>
<CommandBarProvider>
<SettingsAuthProviderJest>{children}</SettingsAuthProviderJest>
</CommandBarProvider>
<SettingsAuthProviderJest>{children}</SettingsAuthProviderJest>
</BrowserRouter>
)
}

View File

@ -2,7 +2,6 @@ import { render, screen } from '@testing-library/react'
import { BrowserRouter } from 'react-router-dom'
import ProjectSidebarMenu from './ProjectSidebarMenu'
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
import { CommandBarProvider } from './CommandBar/CommandBarProvider'
import { Project } from 'lib/project'
const now = new Date()
@ -33,11 +32,9 @@ describe('ProjectSidebarMenu tests', () => {
test('Disables popover menu by default', () => {
render(
<BrowserRouter>
<CommandBarProvider>
<SettingsAuthProviderJest>
<ProjectSidebarMenu project={projectWellFormed} />
</SettingsAuthProviderJest>
</CommandBarProvider>
<SettingsAuthProviderJest>
<ProjectSidebarMenu project={projectWellFormed} />
</SettingsAuthProviderJest>
</BrowserRouter>
)

View File

@ -7,7 +7,6 @@ import { Link, useLocation, useNavigate } from 'react-router-dom'
import { Fragment, useMemo, useContext } from 'react'
import { Logo } from './Logo'
import { APP_NAME } from 'lib/constants'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { CustomIcon } from './CustomIcon'
import { useLspContext } from './LspProvider'
import { engineCommandManager, kclManager } from 'lib/singletons'
@ -15,6 +14,9 @@ import { MachineManagerContext } from 'components/MachineManagerProvider'
import usePlatform from 'hooks/usePlatform'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import Tooltip from './Tooltip'
import { SnapshotFrom } from 'xstate'
import { commandBarActor } from 'machines/commandBarMachine'
import { useSelector } from '@xstate/react'
const ProjectSidebarMenu = ({
project,
@ -84,6 +86,9 @@ function AppLogoLink({
)
}
const commandsSelector = (state: SnapshotFrom<typeof commandBarActor>) =>
state.context.commands
function ProjectMenuPopover({
project,
file,
@ -96,16 +101,14 @@ function ProjectMenuPopover({
const navigate = useNavigate()
const filePath = useAbsoluteFilePath()
const machineManager = useContext(MachineManagerContext)
const commands = useSelector(commandBarActor, commandsSelector)
const { commandBarState, commandBarSend } = useCommandsContext()
const { onProjectClose } = useLspContext()
const exportCommandInfo = { name: 'Export', groupId: 'modeling' }
const makeCommandInfo = { name: 'Make', groupId: 'modeling' }
const findCommand = (obj: { name: string; groupId: string }) =>
Boolean(
commandBarState.context.commands.find(
(c) => c.name === obj.name && c.groupId === obj.groupId
)
commands.find((c) => c.name === obj.name && c.groupId === obj.groupId)
)
const machineCount = machineManager.machines.length
@ -150,7 +153,7 @@ function ProjectMenuPopover({
),
disabled: !findCommand(exportCommandInfo),
onClick: () =>
commandBarSend({
commandBarActor.send({
type: 'Find and select command',
data: exportCommandInfo,
}),
@ -175,7 +178,7 @@ function ProjectMenuPopover({
),
disabled: !findCommand(makeCommandInfo) || machineCount === 0,
onClick: () => {
commandBarSend({
commandBarActor.send({
type: 'Find and select command',
data: makeCommandInfo,
})
@ -200,7 +203,7 @@ function ProjectMenuPopover({
[
platform,
findCommand,
commandBarSend,
commandBarActor.send,
engineCommandManager,
onProjectClose,
isDesktop,

View File

@ -1,5 +1,4 @@
import { useMachine } from '@xstate/react'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
import { useProjectsLoader } from 'hooks/useProjectsLoader'
import { projectsMachine } from 'machines/projectsMachine'
@ -24,6 +23,7 @@ import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import useStateMachineCommands from 'hooks/useStateMachineCommands'
import { projectsCommandBarConfig } from 'lib/commandBarConfigs/projectsCommandConfig'
import { isDesktop } from 'lib/isDesktop'
import { commandBarActor } from 'machines/commandBarMachine'
type MachineContext<T extends AnyStateMachine> = {
state?: StateFrom<T>
@ -73,7 +73,6 @@ const ProjectsContextDesktop = ({
}) => {
const navigate = useNavigate()
const location = useLocation()
const { commandBarSend } = useCommandsContext()
const { onProjectOpen } = useLspContext()
const {
settings: { context: settings },
@ -126,7 +125,7 @@ const ProjectsContextDesktop = ({
},
null
)
commandBarSend({ type: 'Close' })
commandBarActor.send({ type: 'Close' })
const newPathName = `${PATHS.FILE}/${encodeURIComponent(
projectPath
)}`
@ -201,7 +200,7 @@ const ProjectsContextDesktop = ({
return {
message: `Successfully created "${uniqueName}"`,
name,
name: uniqueName,
}
}),
renameProject: fromPromise(async ({ input }) => {

View File

@ -29,7 +29,6 @@ import {
createSettingsCommand,
settingsWithCommandConfigs,
} from 'lib/commandBarConfigs/settingsCommandConfig'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { Command } from 'lib/commandTypes'
import { BaseUnit } from 'lib/settings/settingsTypes'
import {
@ -42,6 +41,7 @@ import { isDesktop } from 'lib/isDesktop'
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
import { codeManager } from 'lib/singletons'
import { createRouteCommands } from 'lib/commandBarConfigs/routeCommandConfig'
import { commandBarActor } from 'machines/commandBarMachine'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
@ -109,7 +109,6 @@ export const SettingsAuthProviderBase = ({
}) => {
const location = useLocation()
const navigate = useNavigate()
const { commandBarSend } = useCommandsContext()
const [settingsPath, setSettingsPath] = useState<string | undefined>(
undefined
)
@ -278,10 +277,10 @@ export const SettingsAuthProviderBase = ({
)
.filter((c) => c !== null) as Command[]
commandBarSend({ type: 'Add commands', data: { commands: commands } })
commandBarActor.send({ type: 'Add commands', data: { commands: commands } })
return () => {
commandBarSend({
commandBarActor.send({
type: 'Remove commands',
data: { commands },
})
@ -290,7 +289,7 @@ export const SettingsAuthProviderBase = ({
settingsState,
settingsSend,
settingsActor,
commandBarSend,
commandBarActor.send,
settingsWithCommandConfigs,
])
@ -303,7 +302,7 @@ export const SettingsAuthProviderBase = ({
encodeURIComponent(loadedProject?.file?.path || BROWSER_PATH)
const { RouteTelemetryCommand, RouteHomeCommand, RouteSettingsCommand } =
createRouteCommands(navigate, location, filePath)
commandBarSend({
commandBarActor.send({
type: 'Remove commands',
data: {
commands: [
@ -314,12 +313,12 @@ export const SettingsAuthProviderBase = ({
},
})
if (location.pathname === PATHS.HOME) {
commandBarSend({
commandBarActor.send({
type: 'Add commands',
data: { commands: [RouteTelemetryCommand, RouteSettingsCommand] },
})
} else if (location.pathname.includes(PATHS.FILE)) {
commandBarSend({
commandBarActor.send({
type: 'Add commands',
data: {
commands: [

View File

@ -17,10 +17,11 @@ import {
import { useRouteLoaderData } from 'react-router-dom'
import { PATHS } from 'lib/paths'
import { IndexLoaderData } from 'lib/types'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { err, reportRejection } from 'lib/trap'
import { getArtifactOfTypes } from 'lang/std/artifactGraph'
import { ViewControlContextMenu } from './ViewControlMenu'
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
import { useSelector } from '@xstate/react'
enum StreamState {
Playing = 'playing',
@ -35,7 +36,7 @@ export const Stream = () => {
const videoRef = useRef<HTMLVideoElement>(null)
const { settings } = useSettingsAuthContext()
const { state, send } = useModelingContext()
const { commandBarState } = useCommandsContext()
const commandBarState = useCommandBarState()
const { mediaStream } = useAppStream()
const { overallState, immediateState } = useNetworkContext()
const [streamState, setStreamState] = useState(StreamState.Unset)

View File

@ -28,7 +28,7 @@ import { base64Decode } from 'lang/wasm'
import { sendTelemetry } from 'lib/textToCad'
import { Themes } from 'lib/theme'
import { ActionButton } from './ActionButton'
import { commandBarMachine } from 'machines/commandBarMachine'
import { commandBarActor, commandBarMachine } from 'machines/commandBarMachine'
import { EventFrom } from 'xstate'
import { fileMachine } from 'machines/fileMachine'
import { reportRejection } from 'lib/trap'
@ -43,15 +43,10 @@ export function ToastTextToCadError({
toastId,
message,
prompt,
commandBarSend,
}: {
toastId: string
message: string
prompt: string
commandBarSend: (
event: EventFrom<typeof commandBarMachine>,
data?: unknown
) => void
}) {
return (
<div className="flex flex-col justify-between gap-6">
@ -81,7 +76,7 @@ export function ToastTextToCadError({
}}
name="Edit prompt"
onClick={() => {
commandBarSend({
commandBarActor.send({
type: 'Find and select command',
data: {
groupId: 'modeling',

View File

@ -8,7 +8,6 @@ import {
} from 'react-router-dom'
import { Models } from '@kittycad/lib'
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
import { CommandBarProvider } from './CommandBar/CommandBarProvider'
type User = Models['User_type']
@ -124,9 +123,7 @@ function TestWrap({ children }: { children: React.ReactNode }) {
<Route
path="/file/:id"
element={
<CommandBarProvider>
<SettingsAuthProviderJest>{children}</SettingsAuthProviderJest>
</CommandBarProvider>
<SettingsAuthProviderJest>{children}</SettingsAuthProviderJest>
}
/>
),

View File

@ -5,7 +5,6 @@ import { engineCommandManager, kclManager } from 'lib/singletons'
import { modelingMachine, ModelingMachineEvent } from 'machines/modelingMachine'
import { Selections, Selection, processCodeMirrorRanges } from 'lib/selections'
import { undo, redo } from '@codemirror/commands'
import { CommandBarMachineEvent } from 'machines/commandBarMachine'
import { addLineHighlight, addLineHighlightEvent } from './highlightextension'
import {
Diagnostic,
@ -52,9 +51,6 @@ export default class EditorManager {
private _modelingSend: (eventInfo: ModelingMachineEvent) => void = () => {}
private _modelingState: StateFrom<typeof modelingMachine> | null = null
private _commandBarSend: (eventInfo: CommandBarMachineEvent) => void =
() => {}
private _convertToVariableEnabled: boolean = false
private _convertToVariableCallback: () => void = () => {}
@ -161,14 +157,6 @@ export default class EditorManager {
this._modelingState = state
}
setCommandBarSend(send: (eventInfo: CommandBarMachineEvent) => void) {
this._commandBarSend = send
}
commandBarSend(eventInfo: CommandBarMachineEvent): void {
return this._commandBarSend(eventInfo)
}
get highlightRange(): Array<[number, number]> {
return this._highlightRange
}
@ -315,6 +303,21 @@ export default class EditorManager {
if (selections?.graphSelections?.length === 0) {
return
}
if (!this._editorView) {
return
}
const codeBaseSelections = this.createEditorSelection(selections)
this._editorView.dispatch({
selection: codeBaseSelections,
annotations: [
updateOutsideEditorEvent,
Transaction.addToHistory.of(false),
],
})
}
createEditorSelection(selections: Selections) {
let codeBasedSelections = []
for (const selection of selections.graphSelections) {
const safeEnd = Math.min(
@ -331,18 +334,7 @@ export default class EditorManager {
.range[1]
const safeEnd = Math.min(end, this._editorView?.state.doc.length || end)
codeBasedSelections.push(EditorSelection.cursor(safeEnd))
if (!this._editorView) {
return
}
this._editorView.dispatch({
selection: EditorSelection.create(codeBasedSelections, 1),
annotations: [
updateOutsideEditorEvent,
Transaction.addToHistory.of(false),
],
})
return EditorSelection.create(codeBasedSelections, 1)
}
// We will ONLY get here if the user called a select event.

View File

@ -1,10 +0,0 @@
import { CommandsContext } from 'components/CommandBar/CommandBarProvider'
export const useCommandsContext = () => {
const commandBarActor = CommandsContext.useActorRef()
const commandBarState = CommandsContext.useSelector((state) => state)
return {
commandBarSend: commandBarActor.send,
commandBarState,
}
}

View File

@ -1,7 +1,6 @@
import { useEffect } from 'react'
import { AnyStateMachine, Actor, StateFrom, EventFrom } from 'xstate'
import { createMachineCommand } from '../lib/createMachineCommand'
import { useCommandsContext } from './useCommandsContext'
import { modelingMachine } from 'machines/modelingMachine'
import { authMachine } from 'machines/authMachine'
import { settingsMachine } from 'machines/settingsMachine'
@ -15,6 +14,7 @@ import { useKclContext } from 'lang/KclProvider'
import { useNetworkContext } from 'hooks/useNetworkContext'
import { NetworkHealthState } from 'hooks/useNetworkStatus'
import { useAppState } from 'AppState'
import { commandBarActor } from 'machines/commandBarMachine'
// This might not be necessary, AnyStateMachine from xstate is working
export type AllMachines =
@ -48,7 +48,6 @@ export default function useStateMachineCommands<
allCommandsRequireNetwork = false,
onCancel,
}: UseStateMachineCommandsArgs<T, S>) {
const { commandBarSend } = useCommandsContext()
const { overallState } = useNetworkContext()
const { isExecuting } = useKclContext()
const { isStreamReady } = useAppState()
@ -76,10 +75,13 @@ export default function useStateMachineCommands<
})
.filter((c) => c !== null) as Command[] // TS isn't smart enough to know this filter removes nulls
commandBarSend({ type: 'Add commands', data: { commands: newCommands } })
commandBarActor.send({
type: 'Add commands',
data: { commands: newCommands },
})
return () => {
commandBarSend({
commandBarActor.send({
type: 'Remove commands',
data: { commands: newCommands },
})

View File

@ -24,7 +24,10 @@ describe('testing AST', () => {
type: 'Literal',
start: 0,
end: 1,
value: 5,
value: {
suffix: 'None',
value: 5,
},
raw: '5',
},
operator: '+',
@ -32,7 +35,10 @@ describe('testing AST', () => {
type: 'Literal',
start: 3,
end: 4,
value: 6,
value: {
suffix: 'None',
value: 6,
},
raw: '6',
},
},

View File

@ -54,6 +54,9 @@ const mySketch001 = startSketchOn('XY')
},
],
id: expect.any(String),
units: {
type: 'Mm',
},
__meta: [{ sourceRange: [46, 71, 0] }],
},
})
@ -72,56 +75,65 @@ const mySketch001 = startSketchOn('XY')
const sketch001 = execState.memory.get('mySketch001')
expect(sketch001).toEqual({
type: 'Solid',
id: expect.any(String),
value: [
{
type: 'extrudePlane',
faceId: expect.any(String),
tag: null,
id: expect.any(String),
sourceRange: [77, 102, 0],
},
{
type: 'extrudePlane',
faceId: expect.any(String),
tag: null,
id: expect.any(String),
sourceRange: [108, 132, 0],
},
],
sketch: {
value: {
type: 'Solid',
id: expect.any(String),
__meta: expect.any(Array),
on: expect.any(Object),
start: expect.any(Object),
type: 'Sketch',
paths: [
value: [
{
type: 'ToPoint',
from: [0, 0],
to: [-1.59, -1.54],
type: 'extrudePlane',
faceId: expect.any(String),
tag: null,
__geoMeta: {
id: expect.any(String),
sourceRange: [77, 102, 0],
},
id: expect.any(String),
sourceRange: [77, 102, 0],
},
{
type: 'ToPoint',
from: [-1.59, -1.54],
to: [0.46, -5.82],
type: 'extrudePlane',
faceId: expect.any(String),
tag: null,
__geoMeta: {
id: expect.any(String),
sourceRange: [108, 132, 0],
},
id: expect.any(String),
sourceRange: [108, 132, 0],
},
],
sketch: {
id: expect.any(String),
units: {
type: 'Mm',
},
__meta: expect.any(Array),
on: expect.any(Object),
start: expect.any(Object),
type: 'Sketch',
paths: [
{
type: 'ToPoint',
from: [0, 0],
to: [-1.59, -1.54],
tag: null,
__geoMeta: {
id: expect.any(String),
sourceRange: [77, 102, 0],
},
},
{
type: 'ToPoint',
from: [-1.59, -1.54],
to: [0.46, -5.82],
tag: null,
__geoMeta: {
id: expect.any(String),
sourceRange: [108, 132, 0],
},
},
],
},
height: 2,
startCapId: expect.any(String),
endCapId: expect.any(String),
units: {
type: 'Mm',
},
__meta: [{ sourceRange: [46, 71, 0] }],
},
height: 2,
startCapId: expect.any(String),
endCapId: expect.any(String),
__meta: [{ sourceRange: [46, 71, 0] }],
})
})
test('sketch extrude and sketch on one of the faces', async () => {
@ -154,187 +166,205 @@ const sk2 = startSketchOn('XY')
expect(geos).toEqual([
{
type: 'Solid',
id: expect.any(String),
value: [
{
type: 'extrudePlane',
faceId: expect.any(String),
tag: null,
id: expect.any(String),
sourceRange: [69, 89, 0],
},
{
type: 'extrudePlane',
faceId: expect.any(String),
tag: {
end: 116,
start: 114,
type: 'TagDeclarator',
value: 'p',
},
id: expect.any(String),
sourceRange: [95, 117, 0],
},
{
type: 'extrudePlane',
faceId: expect.any(String),
tag: null,
id: expect.any(String),
sourceRange: [123, 142, 0],
},
],
sketch: {
value: {
type: 'Solid',
id: expect.any(String),
__meta: expect.any(Array),
on: expect.any(Object),
start: expect.any(Object),
type: 'Sketch',
tags: {
p: {
__meta: [
{
sourceRange: [114, 116, 0],
},
],
type: 'TagIdentifier',
value: 'p',
info: expect.any(Object),
},
},
paths: [
value: [
{
type: 'ToPoint',
from: [0, 0],
to: [-2.5, 0],
type: 'extrudePlane',
faceId: expect.any(String),
tag: null,
__geoMeta: {
id: expect.any(String),
sourceRange: [69, 89, 0],
},
id: expect.any(String),
sourceRange: [69, 89, 0],
},
{
type: 'ToPoint',
from: [-2.5, 0],
to: [0, 10],
type: 'extrudePlane',
faceId: expect.any(String),
tag: {
end: 116,
start: 114,
type: 'TagDeclarator',
value: 'p',
},
__geoMeta: {
id: expect.any(String),
sourceRange: [95, 117, 0],
},
id: expect.any(String),
sourceRange: [95, 117, 0],
},
{
type: 'ToPoint',
from: [0, 10],
to: [2.5, 0],
type: 'extrudePlane',
faceId: expect.any(String),
tag: null,
__geoMeta: {
id: expect.any(String),
sourceRange: [123, 142, 0],
},
id: expect.any(String),
sourceRange: [123, 142, 0],
},
],
sketch: {
id: expect.any(String),
__meta: expect.any(Array),
on: expect.any(Object),
start: expect.any(Object),
type: 'Sketch',
units: {
type: 'Mm',
},
tags: {
p: {
__meta: [
{
sourceRange: [114, 116, 0],
},
],
type: 'TagIdentifier',
value: 'p',
info: expect.any(Object),
},
},
paths: [
{
type: 'ToPoint',
from: [0, 0],
to: [-2.5, 0],
tag: null,
__geoMeta: {
id: expect.any(String),
sourceRange: [69, 89, 0],
},
},
{
type: 'ToPoint',
from: [-2.5, 0],
to: [0, 10],
tag: {
end: 116,
start: 114,
type: 'TagDeclarator',
value: 'p',
},
__geoMeta: {
id: expect.any(String),
sourceRange: [95, 117, 0],
},
},
{
type: 'ToPoint',
from: [0, 10],
to: [2.5, 0],
tag: null,
__geoMeta: {
id: expect.any(String),
sourceRange: [123, 142, 0],
},
},
],
},
height: 2,
startCapId: expect.any(String),
endCapId: expect.any(String),
units: {
type: 'Mm',
},
__meta: [{ sourceRange: [38, 63, 0] }],
},
height: 2,
startCapId: expect.any(String),
endCapId: expect.any(String),
__meta: [{ sourceRange: [38, 63, 0] }],
},
{
type: 'Solid',
id: expect.any(String),
value: [
{
type: 'extrudePlane',
faceId: expect.any(String),
tag: null,
id: expect.any(String),
sourceRange: [373, 393, 0],
},
{
type: 'extrudePlane',
faceId: expect.any(String),
tag: {
end: 419,
start: 417,
type: 'TagDeclarator',
value: 'o',
},
id: expect.any(String),
sourceRange: [399, 420, 0],
},
{
type: 'extrudePlane',
faceId: expect.any(String),
tag: null,
id: expect.any(String),
sourceRange: [426, 445, 0],
},
],
sketch: {
value: {
type: 'Solid',
id: expect.any(String),
__meta: expect.any(Array),
on: expect.any(Object),
start: expect.any(Object),
type: 'Sketch',
tags: {
o: {
__meta: [
{
sourceRange: [417, 419, 0],
},
],
type: 'TagIdentifier',
value: 'o',
info: expect.any(Object),
},
},
paths: [
value: [
{
type: 'ToPoint',
from: [0, 0],
to: [-2.5, 0],
type: 'extrudePlane',
faceId: expect.any(String),
tag: null,
__geoMeta: {
id: expect.any(String),
sourceRange: [373, 393, 0],
},
id: expect.any(String),
sourceRange: [373, 393, 0],
},
{
type: 'ToPoint',
from: [-2.5, 0],
to: [0, 3],
type: 'extrudePlane',
faceId: expect.any(String),
tag: {
end: 419,
start: 417,
type: 'TagDeclarator',
value: 'o',
},
__geoMeta: {
id: expect.any(String),
sourceRange: [399, 420, 0],
},
id: expect.any(String),
sourceRange: [399, 420, 0],
},
{
type: 'ToPoint',
from: [0, 3],
to: [2.5, 0],
type: 'extrudePlane',
faceId: expect.any(String),
tag: null,
__geoMeta: {
id: expect.any(String),
sourceRange: [426, 445, 0],
},
id: expect.any(String),
sourceRange: [426, 445, 0],
},
],
sketch: {
id: expect.any(String),
units: {
type: 'Mm',
},
__meta: expect.any(Array),
on: expect.any(Object),
start: expect.any(Object),
type: 'Sketch',
tags: {
o: {
__meta: [
{
sourceRange: [417, 419, 0],
},
],
type: 'TagIdentifier',
value: 'o',
info: expect.any(Object),
},
},
paths: [
{
type: 'ToPoint',
from: [0, 0],
to: [-2.5, 0],
tag: null,
__geoMeta: {
id: expect.any(String),
sourceRange: [373, 393, 0],
},
},
{
type: 'ToPoint',
from: [-2.5, 0],
to: [0, 3],
tag: {
end: 419,
start: 417,
type: 'TagDeclarator',
value: 'o',
},
__geoMeta: {
id: expect.any(String),
sourceRange: [399, 420, 0],
},
},
{
type: 'ToPoint',
from: [0, 3],
to: [2.5, 0],
tag: null,
__geoMeta: {
id: expect.any(String),
sourceRange: [426, 445, 0],
},
},
],
},
height: 2,
startCapId: expect.any(String),
endCapId: expect.any(String),
__meta: [{ sourceRange: [342, 367, 0] }],
units: {
type: 'Mm',
},
},
height: 2,
startCapId: expect.any(String),
endCapId: expect.any(String),
__meta: [{ sourceRange: [342, 367, 0] }],
},
])
})

View File

@ -221,6 +221,9 @@ const newVar = myVar + 1`
},
],
id: expect.any(String),
units: {
type: 'Mm',
},
__meta: [{ sourceRange: [39, 63, 0] }],
},
})

View File

@ -39,7 +39,7 @@ describe('Testing createLiteral', () => {
it('should create a literal', () => {
const result = createLiteral(5)
expect(result.type).toBe('Literal')
expect(result.value).toBe(5)
expect((result as any).value.value).toBe(5)
})
})
describe('Testing createIdentifier', () => {
@ -56,7 +56,7 @@ describe('Testing createCallExpression', () => {
expect(result.callee.type).toBe('Identifier')
expect(result.callee.name).toBe('myFunc')
expect(result.arguments[0].type).toBe('Literal')
expect((result.arguments[0] as any).value).toBe(5)
expect((result.arguments[0] as any).value.value).toBe(5)
})
})
describe('Testing createObjectExpression', () => {
@ -68,7 +68,7 @@ describe('Testing createObjectExpression', () => {
expect(result.properties[0].type).toBe('ObjectProperty')
expect(result.properties[0].key.name).toBe('myProp')
expect(result.properties[0].value.type).toBe('Literal')
expect((result.properties[0].value as any).value).toBe(5)
expect((result.properties[0].value as any).value.value).toBe(5)
})
})
describe('Testing createArrayExpression', () => {
@ -76,7 +76,7 @@ describe('Testing createArrayExpression', () => {
const result = createArrayExpression([createLiteral(5)])
expect(result.type).toBe('ArrayExpression')
expect(result.elements[0].type).toBe('Literal')
expect((result.elements[0] as any).value).toBe(5)
expect((result.elements[0] as any).value.value).toBe(5)
})
})
describe('Testing createPipeSubstitution', () => {
@ -93,7 +93,7 @@ describe('Testing createVariableDeclaration', () => {
expect(result.declaration.id.type).toBe('Identifier')
expect(result.declaration.id.name).toBe('myVar')
expect(result.declaration.init.type).toBe('Literal')
expect((result.declaration.init as any).value).toBe(5)
expect((result.declaration.init as any).value.value).toBe(5)
})
})
describe('Testing createPipeExpression', () => {
@ -101,7 +101,7 @@ describe('Testing createPipeExpression', () => {
const result = createPipeExpression([createLiteral(5)])
expect(result.type).toBe('PipeExpression')
expect(result.body[0].type).toBe('Literal')
expect((result.body[0] as any).value).toBe(5)
expect((result.body[0] as any).value.value).toBe(5)
})
})

View File

@ -743,14 +743,18 @@ export function splitPathAtPipeExpression(pathToNode: PathToNode): {
return splitPathAtPipeExpression(pathToNode.slice(0, -1))
}
export function createLiteral(value: LiteralValue): Node<Literal> {
export function createLiteral(value: LiteralValue | number): Node<Literal> {
const raw = `${value}`
if (typeof value === 'number') {
value = { value, suffix: 'None' }
}
return {
type: 'Literal',
start: 0,
end: 0,
moduleId: 0,
value,
raw: `${value}`,
raw,
}
}

View File

@ -660,7 +660,7 @@ myNestedVar = [
enter: (node, path) => {
if (
node.type === 'Literal' &&
String(node.value) === literalOfInterest
String((node as any).value.value) === literalOfInterest
) {
pathToNode = path
} else if (

View File

@ -717,16 +717,6 @@ function isTypeInArrayExp(
return node.elements.some((el) => isTypeInValue(el, syntaxType))
}
export function isValueZero(val?: Expr): boolean {
return (
(val?.type === 'Literal' && Number(val.value) === 0) ||
(val?.type === 'UnaryExpression' &&
val.operator === '-' &&
val.argument.type === 'Literal' &&
Number(val.argument.value) === 0)
)
}
export function isLinesParallelAndConstrained(
ast: Program,
artifactGraph: ArtifactGraph,

View File

@ -1014,6 +1014,11 @@ class EngineConnection extends EventTarget {
this.pingPongSpan.pong = new Date()
break
case 'modeling_session_data':
let api_call_id = resp.data?.session?.api_call_id
console.log(`API Call ID: ${api_call_id}`)
break
// Only fires on successful authentication.
case 'ice_server_info':
let ice_servers = resp.data?.ice_servers

View File

@ -20,12 +20,12 @@ import {
sketchFromKclValue,
Literal,
SourceRange,
LiteralValue,
} from '../wasm'
import {
getNodeFromPath,
getNodeFromPathCurry,
getNodePathFromSourceRange,
isValueZero,
} from '../queryAst'
import {
createArrayExpression,
@ -79,11 +79,32 @@ export type ConstraintType =
| 'setAngleBetween'
const REF_NUM_ERR = new Error('Referenced segment does not have a to value')
function asNum(val: LiteralValue): number | Error {
if (typeof val === 'object') return val.value
return REF_NUM_ERR
}
function forceNum(arg: Literal): number {
if (typeof arg.value === 'boolean' || typeof arg.value === 'string') {
return Number(arg.value)
} else {
return arg.value.value
}
}
function isUndef(val: any): val is undefined {
return typeof val === 'undefined'
}
function isNum(val: any): val is number {
return typeof val === 'number'
function isValueZero(val?: Expr): boolean {
return (
(val?.type === 'Literal' && forceNum(val) === 0) ||
(val?.type === 'UnaryExpression' &&
val.operator === '-' &&
val.argument.type === 'Literal' &&
Number(val.argument.value) === 0)
)
}
function createCallWrapper(
@ -190,7 +211,7 @@ const xyLineSetLength =
: referenceSeg
? segRef
: args[0].expr
const literalARg = getArgLiteralVal(args[0].expr)
const literalARg = asNum(args[0].expr.value)
if (err(literalARg)) return literalARg
return createCallWrapper(xOrY, lineVal, tag, literalARg)
}
@ -211,13 +232,14 @@ const basicAngledLineCreateNode =
referencedSegment: path,
}) => {
const refAng = path ? getAngle(path?.from, path?.to) : 0
if (!isNum(args[0].expr.value)) return REF_NUM_ERR
const argValue = asNum(args[0].expr.value)
if (err(argValue)) return argValue
const nonForcedAng =
varValToUse === 'ang'
? inputs[0].expr
: referenceSeg === 'ang'
? getClosesAngleDirection(
args[0].expr.value,
argValue,
refAng,
createSegAngle(referenceSegName)
)
@ -230,8 +252,8 @@ const basicAngledLineCreateNode =
: args[1].expr
const shouldForceAng = valToForce === 'ang' && forceValueUsedInTransform
const shouldForceLen = valToForce === 'len' && forceValueUsedInTransform
const literalArg = getArgLiteralVal(
valToForce === 'ang' ? args[0].expr : args[1].expr
const literalArg = asNum(
valToForce === 'ang' ? args[0].expr.value : args[1].expr.value
)
if (err(literalArg)) return literalArg
return createCallWrapper(
@ -283,7 +305,7 @@ const getMinAndSegAngVals = (
}
const getSignedLeg = (arg: Literal, legLenVal: BinaryPart) =>
Number(arg.value) < 0 ? createUnaryExpression(legLenVal) : legLenVal
forceNum(arg) < 0 ? createUnaryExpression(legLenVal) : legLenVal
const getLegAng = (ang: number, legAngleVal: BinaryPart) => {
const normalisedAngle = ((ang % 360) + 360) % 360 // between 0 and 360
@ -322,8 +344,7 @@ const setHorzVertDistanceCreateNode =
referencedSegment,
}) => {
const refNum = referencedSegment?.to?.[index]
const literalArg = getArgLiteralVal(args?.[index].expr)
if (err(literalArg)) return literalArg
const literalArg = asNum(args?.[index].expr.value)
if (isUndef(refNum) || err(literalArg)) return REF_NUM_ERR
const valueUsedInTransform = roundOff(literalArg - refNum, 2)
@ -352,7 +373,7 @@ const setHorzVertDistanceForAngleLineCreateNode =
referencedSegment,
}) => {
const refNum = referencedSegment?.to?.[index]
const literalArg = getArgLiteralVal(args?.[1].expr)
const literalArg = asNum(args?.[1].expr.value)
if (isUndef(refNum) || err(literalArg)) return REF_NUM_ERR
const valueUsedInTransform = roundOff(literalArg - refNum, 2)
const binExp = createBinaryExpressionWithUnary([
@ -374,8 +395,8 @@ const setAbsDistanceCreateNode =
index = xOrY === 'x' ? 0 : 1
): CreateStdLibSketchCallExpr =>
({ tag, forceValueUsedInTransform, rawArgs: args }) => {
const literalArg = getArgLiteralVal(args?.[index].expr)
if (err(literalArg)) return REF_NUM_ERR
const literalArg = asNum(args?.[index].expr.value)
if (err(literalArg)) return literalArg
const valueUsedInTransform = roundOff(literalArg, 2)
const val = forceValueUsedInTransform || createLiteral(valueUsedInTransform)
if (isXOrYLine) {
@ -396,8 +417,8 @@ const setAbsDistanceCreateNode =
const setAbsDistanceForAngleLineCreateNode =
(xOrY: 'x' | 'y'): CreateStdLibSketchCallExpr =>
({ tag, forceValueUsedInTransform, inputs, rawArgs: args }) => {
const literalArg = getArgLiteralVal(args?.[1].expr)
if (err(literalArg)) return REF_NUM_ERR
const literalArg = asNum(args?.[1].expr.value)
if (err(literalArg)) return literalArg
const valueUsedInTransform = roundOff(literalArg, 2)
const val = forceValueUsedInTransform || createLiteral(valueUsedInTransform)
return createCallWrapper(
@ -419,7 +440,7 @@ const setHorVertDistanceForXYLines =
}) => {
const index = xOrY === 'x' ? 0 : 1
const refNum = referencedSegment?.to?.[index]
const literalArg = getArgLiteralVal(args?.[index].expr)
const literalArg = asNum(args?.[index].expr.value)
if (isUndef(refNum) || err(literalArg)) return REF_NUM_ERR
const valueUsedInTransform = roundOff(literalArg - refNum, 2)
const makeBinExp = createBinaryExpressionWithUnary([
@ -445,9 +466,9 @@ const setHorzVertDistanceConstraintLineCreateNode =
])
const makeBinExp = (index: 0 | 1) => {
const arg = getArgLiteralVal(args?.[index].expr)
const arg = asNum(args?.[index].expr.value)
const refNum = referencedSegment?.to?.[index]
if (err(arg) || !isNum(refNum)) return REF_NUM_ERR
if (err(arg) || isUndef(refNum)) return REF_NUM_ERR
return createBinaryExpressionWithUnary([
createSegEnd(referenceSegName, isX),
createLiteral(roundOff(arg - refNum, 2)),
@ -468,9 +489,9 @@ const setAngledIntersectLineForLines: CreateStdLibSketchCallExpr = ({
forceValueUsedInTransform,
rawArgs: args,
}) => {
const val = args[1].expr.value,
angle = args[0].expr.value
if (!isNum(val) || !isNum(angle)) return REF_NUM_ERR
const val = asNum(args[1].expr.value),
angle = asNum(args[0].expr.value)
if (err(val) || err(angle)) return REF_NUM_ERR
const valueUsedInTransform = roundOff(val, 2)
const varNamMap: { [key: number]: string } = {
0: 'ZERO',
@ -498,8 +519,8 @@ const setAngledIntersectForAngledLines: CreateStdLibSketchCallExpr = ({
inputs,
rawArgs: args,
}) => {
const val = args[1].expr.value
if (!isNum(val)) return REF_NUM_ERR
const val = asNum(args[1].expr.value)
if (err(val)) return val
const valueUsedInTransform = roundOff(val, 2)
return intersectCallWrapper({
fnName: 'angledLineThatIntersects',
@ -524,8 +545,8 @@ const setAngleBetweenCreateNode =
const refAngle = referencedSegment
? getAngle(referencedSegment?.from, referencedSegment?.to)
: 0
const val = args[0].expr.value
if (!isNum(val)) return REF_NUM_ERR
const val = asNum(args[0].expr.value)
if (err(val)) return val
let valueUsedInTransform = roundOff(normaliseAngle(val - refAngle))
let firstHalfValue = createSegAngle(referenceSegName)
if (Math.abs(valueUsedInTransform) > 90) {
@ -706,13 +727,11 @@ const transformMap: TransformMap = {
createPipeSubstitution(),
]
)
if (!isNum(args[0].expr.value)) return REF_NUM_ERR
const val = asNum(args[0].expr.value)
if (err(val)) return val
return createCallWrapper(
'angledLineToX',
[
getAngleLengthSign(args[0].expr.value, angleToMatchLengthXCall),
inputs[0].expr,
],
[getAngleLengthSign(val, angleToMatchLengthXCall), inputs[0].expr],
tag
)
},
@ -739,13 +758,11 @@ const transformMap: TransformMap = {
createPipeSubstitution(),
]
)
if (!isNum(args[0].expr.value)) return REF_NUM_ERR
const val = asNum(args[0].expr.value)
if (err(val)) return val
return createCallWrapper(
'angledLineToY',
[
getAngleLengthSign(args[0].expr.value, angleToMatchLengthYCall),
inputs[1].expr,
],
[getAngleLengthSign(val, angleToMatchLengthYCall), inputs[1].expr],
tag
)
},
@ -763,7 +780,7 @@ const transformMap: TransformMap = {
forceValueUsedInTransform,
rawArgs: args,
}) => {
const val = getArgLiteralVal(args[0].expr)
const val = asNum(args[0].expr.value)
if (err(val)) return val
return createCallWrapper(
'angledLineToY',
@ -844,7 +861,7 @@ const transformMap: TransformMap = {
tooltip: 'yLine',
createNode: ({ inputs, tag, rawArgs: args }) => {
const expr = inputs[1].expr
if (Number(args[0].expr.value) >= 0)
if (forceNum(args[0].expr) >= 0)
return createCallWrapper('yLine', expr, tag)
if (isExprBinaryPart(expr))
return createCallWrapper('yLine', createUnaryExpression(expr), tag)
@ -856,7 +873,7 @@ const transformMap: TransformMap = {
tooltip: 'xLine',
createNode: ({ inputs, tag, rawArgs: args }) => {
const expr = inputs[1].expr
if (Number(args[0].expr.value) >= 0)
if (forceNum(args[0].expr) >= 0)
return createCallWrapper('xLine', expr, tag)
if (isExprBinaryPart(expr))
return createCallWrapper('xLine', createUnaryExpression(expr), tag)
@ -900,10 +917,11 @@ const transformMap: TransformMap = {
referenceSegName,
getInputOfType(inputs, 'xRelative').expr
)
if (!isNum(args[0].expr.value)) return REF_NUM_ERR
const val = asNum(args[0].expr.value)
if (err(val)) return val
return createCallWrapper(
'angledLineOfXLength',
[getLegAng(args[0].expr.value, legAngle), minVal],
[getLegAng(val, legAngle), minVal],
tag
)
},
@ -912,7 +930,7 @@ const transformMap: TransformMap = {
tooltip: 'xLine',
createNode: ({ inputs, tag, rawArgs: args }) => {
const expr = inputs[1].expr
if (Number(args[0].expr.value) >= 0)
if (forceNum(args[0].expr) >= 0)
return createCallWrapper('xLine', expr, tag)
if (isExprBinaryPart(expr))
return createCallWrapper('xLine', createUnaryExpression(expr), tag)
@ -953,10 +971,11 @@ const transformMap: TransformMap = {
inputs[1].expr,
'legAngY'
)
if (!isNum(args[0].expr.value)) return REF_NUM_ERR
const val = asNum(args[0].expr.value)
if (err(val)) return val
return createCallWrapper(
'angledLineOfXLength',
[getLegAng(args[0].expr.value, legAngle), minVal],
[getLegAng(val, legAngle), minVal],
tag
)
},
@ -965,7 +984,7 @@ const transformMap: TransformMap = {
tooltip: 'yLine',
createNode: ({ inputs, tag, rawArgs: args }) => {
const expr = inputs[1].expr
if (Number(args[0].expr.value) >= 0)
if (forceNum(args[0].expr) >= 0)
return createCallWrapper('yLine', expr, tag)
if (isExprBinaryPart(expr))
return createCallWrapper('yLine', createUnaryExpression(expr), tag)
@ -1005,13 +1024,11 @@ const transformMap: TransformMap = {
createPipeSubstitution(),
]
)
if (!isNum(args[0].expr.value)) return REF_NUM_ERR
const val = asNum(args[0].expr.value)
if (err(val)) return val
return createCallWrapper(
'angledLineToX',
[
getAngleLengthSign(args[0].expr.value, angleToMatchLengthXCall),
inputs[1].expr,
],
[getAngleLengthSign(val, angleToMatchLengthXCall), inputs[1].expr],
tag
)
},
@ -1057,13 +1074,11 @@ const transformMap: TransformMap = {
createPipeSubstitution(),
]
)
if (!isNum(args[0].expr.value)) return REF_NUM_ERR
const val = asNum(args[0].expr.value)
if (err(val)) return val
return createCallWrapper(
'angledLineToY',
[
getAngleLengthSign(args[0].expr.value, angleToMatchLengthXCall),
inputs[1].expr,
],
[getAngleLengthSign(val, angleToMatchLengthXCall), inputs[1].expr],
tag
)
},
@ -1080,7 +1095,7 @@ const transformMap: TransformMap = {
equalLength: {
tooltip: 'xLine',
createNode: ({ referenceSegName, tag, rawArgs: args }) => {
const argVal = getArgLiteralVal(args[0].expr)
const argVal = asNum(args[0].expr.value)
if (err(argVal)) return argVal
const segLen = createSegLen(referenceSegName)
if (argVal > 0) return createCallWrapper('xLine', segLen, tag, argVal)
@ -1118,7 +1133,7 @@ const transformMap: TransformMap = {
equalLength: {
tooltip: 'yLine',
createNode: ({ referenceSegName, tag, rawArgs: args }) => {
const argVal = getArgLiteralVal(args[0].expr)
const argVal = asNum(args[0].expr.value)
if (err(argVal)) return argVal
let segLen = createSegLen(referenceSegName)
if (argVal < 0) segLen = createUnaryExpression(segLen)
@ -1714,7 +1729,7 @@ export function transformAstSketchLines({
let kclVal = programMemory.get(varName)
let sketch
if (kclVal?.type === 'Solid') {
sketch = kclVal.sketch
sketch = kclVal.value.sketch
} else {
sketch = sketchFromKclValue(kclVal, varName)
if (err(sketch)) {
@ -1823,11 +1838,6 @@ function createLastSeg(isX: boolean): Node<CallExpression> {
])
}
function getArgLiteralVal(arg: Literal): number | Error {
if (!isNum(arg.value)) return REF_NUM_ERR
return arg.value
}
export type ConstraintLevel = 'free' | 'partial' | 'full'
export function getConstraintLevelFromSourceRange(

View File

@ -539,7 +539,8 @@ export function sketchFromKclValueOptional(
): Sketch | Reason {
if (obj?.value?.type === 'Sketch') return obj.value
if (obj?.value?.type === 'Solid') return obj.value.sketch
if (obj?.type === 'Solid') return obj.sketch
if (obj?.type === 'Sketch') return obj.value
if (obj?.type === 'Solid') return obj.value.sketch
if (!varName) {
varName = 'a KCL value'
}

View File

@ -13,6 +13,7 @@ import {
loftValidator,
revolveAxisValidator,
shellValidator,
sweepValidator,
} from './validators'
type OutputFormat = Models['OutputFormat_type']
@ -42,8 +43,8 @@ export type ModelingCommandSchema = {
distance: KclCommandValue
}
Sweep: {
path: Selections
profile: Selections
target: Selections
trajectory: Selections
}
Loft: {
selection: Selections
@ -308,25 +309,24 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
'Create a 3D body by moving a sketch region along an arbitrary path.',
icon: 'sweep',
status: 'development',
needsReview: true,
needsReview: false,
args: {
profile: {
target: {
inputType: 'selection',
selectionTypes: ['solid2d'],
required: true,
skip: true,
multiple: false,
// TODO: add dry-run validation
warningMessage:
'The sweep workflow is new and under tested. Please break it and report issues.',
},
path: {
trajectory: {
inputType: 'selection',
selectionTypes: ['segment', 'path'],
required: true,
skip: true,
skip: false,
multiple: false,
// TODO: add dry-run validation
validation: sweepValidator,
},
},
},

View File

@ -207,3 +207,64 @@ export const shellValidator = async ({
return 'Unable to shell with the provided selection'
}
export const sweepValidator = async ({
context,
data,
}: {
context: CommandBarContext
data: { trajectory: Selections }
}): Promise<boolean | string> => {
if (!isSelections(data.trajectory)) {
console.log('Unable to sweep, selections are missing')
return 'Unable to sweep, selections are missing'
}
// Retrieve the parent path from the segment selection directly
const trajectoryArtifact = data.trajectory.graphSelections[0].artifact
if (!trajectoryArtifact) {
return "Unable to sweep, couldn't find the trajectory artifact"
}
if (trajectoryArtifact.type !== 'segment') {
return "Unable to sweep, couldn't find the target from a non-segment selection"
}
const trajectory = trajectoryArtifact.pathId
// Get the former arg in the command bar flow, and retrieve the path from the solid2d directly
const targetArg = context.argumentsToSubmit['target'] as Selections
const targetArtifact = targetArg.graphSelections[0].artifact
if (!targetArtifact) {
return "Unable to sweep, couldn't find the profile artifact"
}
if (targetArtifact.type !== 'solid2d') {
return "Unable to sweep, couldn't find the target from a non-solid2d selection"
}
const target = targetArtifact.pathId
const sweepCommand = async () => {
// TODO: second look on defaults here
const DEFAULT_TOLERANCE: Models['LengthUnit_type'] = 1e-7
const DEFAULT_SECTIONAL = false
const cmdArgs = {
target,
trajectory,
sectional: DEFAULT_SECTIONAL,
tolerance: DEFAULT_TOLERANCE,
}
return await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'sweep',
...cmdArgs,
},
})
}
const attemptSweep = await dryRunWrapper(sweepCommand)
if (attemptSweep?.success) {
return true
}
return 'Unable to sweep with the provided selection'
}

View File

@ -68,10 +68,6 @@ interface TextToKclProps {
data?: unknown
) => unknown
navigate: NavigateFunction
commandBarSend: (
type: EventFrom<typeof commandBarMachine>,
data?: unknown
) => unknown
context: ContextFrom<typeof fileMachine>
token?: string
settings: {
@ -84,7 +80,6 @@ export async function submitAndAwaitTextToKcl({
trimmedPrompt,
fileMachineSend,
navigate,
commandBarSend,
context,
token,
settings,
@ -96,7 +91,6 @@ export async function submitAndAwaitTextToKcl({
ToastTextToCadError({
toastId,
message,
commandBarSend,
prompt: trimmedPrompt,
}),
{
@ -195,7 +189,7 @@ export async function submitAndAwaitTextToKcl({
.toLowerCase()}${FILE_EXT}`
if (isDesktop()) {
// We have to pre-emptively run our unique file name logic,
// We have to preemptively run our unique file name logic,
// so that we can pass the unique file name to the toast,
// and by extension the file-deletion-on-reject logic.
newFileName = getNextFileName({

View File

@ -1,6 +1,6 @@
import { CustomIconName } from 'components/CustomIcon'
import { DEV } from 'env'
import { commandBarMachine } from 'machines/commandBarMachine'
import { commandBarActor, commandBarMachine } from 'machines/commandBarMachine'
import {
canRectangleOrCircleTool,
isClosedSketch,
@ -21,7 +21,6 @@ type ToolbarMode = {
export interface ToolbarItemCallbackProps {
modelingState: StateFrom<typeof modelingMachine>
modelingSend: (event: EventFrom<typeof modelingMachine>) => void
commandBarSend: (event: EventFrom<typeof commandBarMachine>) => void
sketchPathId: string | false
}
@ -84,8 +83,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
'break',
{
id: 'extrude',
onClick: ({ commandBarSend }) =>
commandBarSend({
onClick: () =>
commandBarActor.send({
type: 'Find and select command',
data: { name: 'Extrude', groupId: 'modeling' },
}),
@ -98,8 +97,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
},
{
id: 'revolve',
onClick: ({ commandBarSend }) =>
commandBarSend({
onClick: () =>
commandBarActor.send({
type: 'Find and select command',
data: { name: 'Revolve', groupId: 'modeling' },
}),
@ -119,8 +118,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
},
{
id: 'sweep',
onClick: ({ commandBarSend }) =>
commandBarSend({
onClick: () =>
commandBarActor.send({
type: 'Find and select command',
data: { name: 'Sweep', groupId: 'modeling' },
}),
@ -139,8 +138,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
},
{
id: 'loft',
onClick: ({ commandBarSend }) =>
commandBarSend({
onClick: () =>
commandBarActor.send({
type: 'Find and select command',
data: { name: 'Loft', groupId: 'modeling' },
}),
@ -160,8 +159,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
'break',
{
id: 'fillet3d',
onClick: ({ commandBarSend }) =>
commandBarSend({
onClick: () =>
commandBarActor.send({
type: 'Find and select command',
data: { name: 'Fillet', groupId: 'modeling' },
}),
@ -174,8 +173,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
},
{
id: 'chamfer3d',
onClick: ({ commandBarSend }) =>
commandBarSend({
onClick: () =>
commandBarActor.send({
type: 'Find and select command',
data: { name: 'Chamfer', groupId: 'modeling' },
}),
@ -188,8 +187,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
},
{
id: 'shell',
onClick: ({ commandBarSend }) => {
commandBarSend({
onClick: () => {
commandBarActor.send({
type: 'Find and select command',
data: { name: 'Shell', groupId: 'modeling' },
})
@ -269,8 +268,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
[
{
id: 'plane-offset',
onClick: ({ commandBarSend }) => {
commandBarSend({
onClick: () => {
commandBarActor.send({
type: 'Find and select command',
data: { name: 'Offset plane', groupId: 'modeling' },
})
@ -280,7 +279,12 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
status: 'available',
title: 'Offset plane',
description: 'Create a plane parallel to an existing plane.',
links: [],
links: [
{
label: 'KCL docs',
url: 'https://zoo.dev/docs/kcl/offsetPlane',
},
],
},
{
id: 'plane-points',
@ -296,8 +300,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
[
{
id: 'text-to-cad',
onClick: ({ commandBarSend }) =>
commandBarSend({
onClick: () =>
commandBarActor.send({
type: 'Find and select command',
data: { name: 'Text-to-CAD', groupId: 'modeling' },
}),
@ -305,12 +309,17 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
status: 'available',
title: 'Text-to-CAD',
description: 'Generate geometry from a text prompt.',
links: [],
links: [
{
label: 'API docs',
url: 'https://zoo.dev/docs/api/ml/generate-a-cad-model-from-text',
},
],
},
{
id: 'prompt-to-edit',
onClick: ({ commandBarSend }) =>
commandBarSend({
onClick: () =>
commandBarActor.send({
type: 'Find and select command',
data: { name: 'Prompt-to-edit', groupId: 'modeling' },
}),
@ -583,8 +592,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
{
id: 'constraint-length',
disabled: (state) => !state.matches({ Sketch: 'SketchIdle' }),
onClick: ({ commandBarSend }) =>
commandBarSend({
onClick: () =>
commandBarActor.send({
type: 'Find and select command',
data: {
name: 'Constrain length',

View File

@ -1,4 +1,4 @@
import { assign, fromPromise, setup } from 'xstate'
import { assign, createActor, fromPromise, setup, SnapshotFrom } from 'xstate'
import {
Command,
CommandArgument,
@ -9,6 +9,7 @@ import { Selections__old } from 'lib/selections'
import { getCommandArgumentKclValuesOnly } from 'lib/commandUtils'
import { MachineManager } from 'components/MachineManagerProvider'
import toast from 'react-hot-toast'
import { useSelector } from '@xstate/react'
export type CommandBarContext = {
commands: Command[]
@ -247,8 +248,17 @@ export const commandBarMachine = setup({
guards: {
'Command needs review': ({ context }) =>
context.selectedCommand?.needsReview || false,
'Command has no arguments': () => false,
'All arguments are skippable': () => false,
'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
)
},
'Has selected command': ({ context }) => !!context.selectedCommand,
},
actors: {
@ -620,3 +630,12 @@ function sortCommands(a: Command, b: Command) {
if (a.groupId === 'settings' && !(b.groupId === 'settings')) return 1
return a.name.localeCompare(b.name)
}
export const commandBarActor = createActor(commandBarMachine).start()
/** Basic state snapshot selector */
const cmdBarStateSelector = (state: SnapshotFrom<typeof commandBarActor>) =>
state
export const useCommandBarState = () => {
return useSelector(commandBarActor, cmdBarStateSelector)
}

View File

@ -1561,40 +1561,40 @@ export const modelingMachine = setup({
if (!input) return new Error('No input provided')
// Extract inputs
const ast = kclManager.ast
const { profile, path } = input
const { target, trajectory } = input
// Find the profile declaration
const profileNodePath = getNodePathFromSourceRange(
const targetNodePath = getNodePathFromSourceRange(
ast,
profile.graphSelections[0].codeRef.range
target.graphSelections[0].codeRef.range
)
const profileNode = getNodeFromPath<VariableDeclarator>(
const targetNode = getNodeFromPath<VariableDeclarator>(
ast,
profileNodePath,
targetNodePath,
'VariableDeclarator'
)
if (err(profileNode)) {
if (err(targetNode)) {
return new Error("Couldn't parse profile selection")
}
const profileDeclarator = profileNode.node
const targetDeclarator = targetNode.node
// Find the path declaration
const pathNodePath = getNodePathFromSourceRange(
const trajectoryNodePath = getNodePathFromSourceRange(
ast,
path.graphSelections[0].codeRef.range
trajectory.graphSelections[0].codeRef.range
)
const pathNode = getNodeFromPath<VariableDeclarator>(
const trajectoryNode = getNodeFromPath<VariableDeclarator>(
ast,
pathNodePath,
trajectoryNodePath,
'VariableDeclarator'
)
if (err(pathNode)) {
if (err(trajectoryNode)) {
return new Error("Couldn't parse path selection")
}
const pathDeclarator = pathNode.node
const trajectoryDeclarator = trajectoryNode.node
// Perform the sweep
const sweepRes = addSweep(ast, profileDeclarator, pathDeclarator)
const sweepRes = addSweep(ast, targetDeclarator, trajectoryDeclarator)
const updateAstResult = await kclManager.updateAst(
sweepRes.modifiedAst,
true,

View File

@ -24,13 +24,12 @@ import { markOnce } from 'lib/performance'
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
import { useProjectsLoader } from 'hooks/useProjectsLoader'
import { useProjectsContext } from 'hooks/useProjectsContext'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { commandBarActor } from 'machines/commandBarMachine'
// This route only opens in the desktop context for now,
// as defined in Router.tsx, so we can use the desktop APIs and types.
const Home = () => {
const { state, send } = useProjectsContext()
const { commandBarSend } = useCommandsContext()
const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0)
const { projectsDir } = useProjectsLoader([projectsLoaderTrigger])
@ -128,7 +127,7 @@ const Home = () => {
<ActionButton
Element="button"
onClick={() =>
commandBarSend({
commandBarActor.send({
type: 'Find and select command',
data: {
groupId: 'projects',

View File

@ -1382,12 +1382,6 @@ dependencies = [
"tracing",
]
[[package]]
name = "iai"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71a816c97c42258aa5834d07590b718b4c9a598944cd39a52dc25b351185d678"
[[package]]
name = "iana-time-zone"
version = "0.1.61"
@ -1739,7 +1733,6 @@ dependencies = [
"gltf-json",
"handlebars",
"http 1.2.0",
"iai",
"image",
"indexmap 2.7.0",
"insta",

View File

@ -9,6 +9,12 @@ rust-version = "1.73"
[lib]
crate-type = ["cdylib"]
# DO NOT MERGE ME WITH THIS PLEASE
#
# FOR THE LOVE OF GOD DON'T HIT APPROVE PR WITHOUT ASKING ME TO REMOVE THIS
[package.metadata.wasm-pack.profile.release]
wasm-opt = false
[dependencies]
bson = { version = "2.13.0", features = ["uuid-1", "chrono"] }
data-encoding = "2.6.0"

View File

@ -113,7 +113,6 @@ base64 = "0.22.1"
criterion = { version = "0.5.1", features = ["async_tokio"] }
expectorate = "1.1.0"
handlebars = "6.3.0"
iai = "0.1"
image = { version = "0.25.5", default-features = false, features = ["png"] }
insta = { version = "1.41.1", features = ["json", "filters", "redactions"] }
itertools = "0.13.0"
@ -129,10 +128,6 @@ workspace = true
name = "compiler_benchmark_criterion"
harness = false
[[bench]]
name = "compiler_benchmark_iai"
harness = false
[[bench]]
name = "digest_benchmark"
harness = false
@ -142,15 +137,7 @@ name = "lsp_semantic_tokens_benchmark_criterion"
harness = false
required-features = ["lsp-test-util"]
[[bench]]
name = "lsp_semantic_tokens_benchmark_iai"
harness = false
required-features = ["lsp-test-util"]
[[bench]]
name = "executor_benchmark_criterion"
harness = false
[[bench]]
name = "executor_benchmark_iai"
harness = false

View File

@ -1,35 +0,0 @@
use iai::black_box;
pub fn parse(program: &str) {
black_box(kcl_lib::Program::parse(program).unwrap());
}
fn parse_kitt() {
parse(KITT_PROGRAM)
}
fn parse_pipes() {
parse(PIPES_PROGRAM)
}
fn parse_cube() {
parse(CUBE_PROGRAM)
}
fn parse_math() {
parse(MATH_PROGRAM)
}
fn parse_lsystem() {
parse(LSYSTEM_PROGRAM)
}
iai::main! {
parse_kitt,
parse_pipes,
parse_cube,
parse_math,
parse_lsystem,
}
const KITT_PROGRAM: &str = include_str!("../../tests/executor/inputs/kittycad_svg.kcl");
const PIPES_PROGRAM: &str = include_str!("../../tests/executor/inputs/pipes_on_pipes.kcl");
const CUBE_PROGRAM: &str = include_str!("../../tests/executor/inputs/cube.kcl");
const MATH_PROGRAM: &str = include_str!("../../tests/executor/inputs/math.kcl");
const LSYSTEM_PROGRAM: &str = include_str!("../../tests/executor/inputs/lsystem.kcl");

View File

@ -1,27 +0,0 @@
use iai::black_box;
async fn execute_server_rack_heavy() {
let code = SERVER_RACK_HEAVY_PROGRAM;
black_box(
kcl_lib::test_server::execute_and_snapshot(code, kcl_lib::UnitLength::Mm, None)
.await
.unwrap(),
);
}
async fn execute_server_rack_lite() {
let code = SERVER_RACK_LITE_PROGRAM;
black_box(
kcl_lib::test_server::execute_and_snapshot(code, kcl_lib::UnitLength::Mm, None)
.await
.unwrap(),
);
}
iai::main! {
execute_server_rack_lite,
execute_server_rack_heavy,
}
const SERVER_RACK_HEAVY_PROGRAM: &str = include_str!("../../tests/executor/inputs/server-rack-heavy.kcl");
const SERVER_RACK_LITE_PROGRAM: &str = include_str!("../../tests/executor/inputs/server-rack-lite.kcl");

View File

@ -1,45 +0,0 @@
use iai::black_box;
use kcl_lib::kcl_lsp_server;
use tower_lsp::LanguageServer;
async fn kcl_lsp_semantic_tokens(code: &str) {
let server = kcl_lsp_server(false).await.unwrap();
// Send open file.
server
.did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams {
text_document: tower_lsp::lsp_types::TextDocumentItem {
uri: "file:///test.kcl".try_into().unwrap(),
language_id: "kcl".to_string(),
version: 1,
text: code.to_string(),
},
})
.await;
// Send semantic tokens request.
black_box(
server
.semantic_tokens_full(tower_lsp::lsp_types::SemanticTokensParams {
text_document: tower_lsp::lsp_types::TextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(),
},
partial_result_params: Default::default(),
work_done_progress_params: Default::default(),
})
.await
.unwrap()
.unwrap(),
);
}
async fn semantic_tokens_global_tags() {
let code = GLOBAL_TAGS_FILE;
kcl_lsp_semantic_tokens(code).await;
}
iai::main! {
semantic_tokens_global_tags,
}
const GLOBAL_TAGS_FILE: &str = include_str!("../../tests/executor/inputs/global-tags.kcl");

View File

@ -370,8 +370,6 @@ impl From<KclError> for pyo3::PyErr {
pub struct CompilationError {
#[serde(rename = "sourceRange")]
pub source_range: SourceRange,
#[serde(rename = "contextRange")]
pub context_range: Option<SourceRange>,
pub message: String,
pub suggestion: Option<Suggestion>,
pub severity: Severity,
@ -382,7 +380,6 @@ impl CompilationError {
pub(crate) fn err(source_range: SourceRange, message: impl ToString) -> CompilationError {
CompilationError {
source_range,
context_range: None,
message: message.to_string(),
suggestion: None,
severity: Severity::Error,
@ -393,7 +390,6 @@ impl CompilationError {
pub(crate) fn fatal(source_range: SourceRange, message: impl ToString) -> CompilationError {
CompilationError {
source_range,
context_range: None,
message: message.to_string(),
suggestion: None,
severity: Severity::Fatal,
@ -402,22 +398,18 @@ impl CompilationError {
}
pub(crate) fn with_suggestion(
source_range: SourceRange,
context_range: Option<SourceRange>,
message: impl ToString,
suggestion: Option<(impl ToString, impl ToString)>,
self,
suggestion_title: impl ToString,
suggestion_insert: impl ToString,
tag: Tag,
) -> CompilationError {
CompilationError {
source_range,
context_range,
message: message.to_string(),
suggestion: suggestion.map(|(t, i)| Suggestion {
title: t.to_string(),
insert: i.to_string(),
suggestion: Some(Suggestion {
title: suggestion_title.to_string(),
insert: suggestion_insert.to_string(),
}),
severity: Severity::Error,
tag,
..self
}
}

View File

@ -11,6 +11,11 @@ pub(super) const SETTINGS: &str = "settings";
pub(super) const SETTINGS_UNIT_LENGTH: &str = "defaultLengthUnit";
pub(super) const SETTINGS_UNIT_ANGLE: &str = "defaultAngleUnit";
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub(super) enum AnnotationScope {
Module,
}
pub(super) fn expect_properties<'a>(
for_key: &'static str,
annotation: &'a NonCodeValue,

View File

@ -121,8 +121,8 @@ impl Node<MemberExpression> {
source_ranges: vec![self.clone().into()],
}))
}
(KclValue::Solid(solid), Property::String(prop)) if prop == "sketch" => Ok(KclValue::Sketch {
value: Box::new(solid.sketch),
(KclValue::Solid { value }, Property::String(prop)) if prop == "sketch" => Ok(KclValue::Sketch {
value: Box::new(value.sketch),
}),
(KclValue::Sketch { value: sk }, Property::String(prop)) if prop == "tags" => Ok(KclValue::Object {
meta: vec![Metadata {
@ -662,11 +662,11 @@ fn update_memory_for_tags_of_geometry(result: &mut KclValue, exec_state: &mut Ex
exec_state.mut_memory().update_tag(&tag.value, tag.clone())?;
}
}
KclValue::Solid(ref mut solid) => {
for value in &solid.value {
if let Some(tag) = value.get_tag() {
KclValue::Solid { ref mut value } => {
for v in &value.value {
if let Some(tag) = v.get_tag() {
// Get the past tag and update it.
let mut t = if let Some(t) = solid.sketch.tags.get(&tag.name) {
let mut t = if let Some(t) = value.sketch.tags.get(&tag.name) {
t.clone()
} else {
// It's probably a fillet or a chamfer.
@ -674,10 +674,10 @@ fn update_memory_for_tags_of_geometry(result: &mut KclValue, exec_state: &mut Ex
TagIdentifier {
value: tag.name.clone(),
info: Some(TagEngineInfo {
id: value.get_id(),
surface: Some(value.clone()),
id: v.get_id(),
surface: Some(v.clone()),
path: None,
sketch: solid.id,
sketch: value.id,
}),
meta: vec![Metadata {
source_range: tag.clone().into(),
@ -693,21 +693,21 @@ fn update_memory_for_tags_of_geometry(result: &mut KclValue, exec_state: &mut Ex
};
let mut info = info.clone();
info.surface = Some(value.clone());
info.sketch = solid.id;
info.surface = Some(v.clone());
info.sketch = value.id;
t.info = Some(info);
exec_state.mut_memory().update_tag(&tag.name, t.clone())?;
// update the sketch tags.
solid.sketch.tags.insert(tag.name.clone(), t);
value.sketch.tags.insert(tag.name.clone(), t);
}
}
// Find the stale sketch in memory and update it.
let cur_env_index = exec_state.memory().current_env.index();
if let Some(current_env) = exec_state.mut_memory().environments.get_mut(cur_env_index) {
current_env.update_sketch_tags(&solid.sketch);
current_env.update_sketch_tags(&value.sketch);
}
}
_ => {}
@ -929,13 +929,13 @@ impl Property {
LiteralIdentifier::Literal(literal) => {
let value = literal.value.clone();
match value {
LiteralValue::Number(x) => {
if let Some(x) = crate::try_f64_to_usize(x) {
LiteralValue::Number { value, .. } => {
if let Some(x) = crate::try_f64_to_usize(value) {
Ok(Property::UInt(x))
} else {
Err(KclError::Semantic(KclErrorDetails {
source_ranges: property_sr,
message: format!("{x} is not a valid index, indices must be whole numbers >= 0"),
message: format!("{value} is not a valid index, indices must be whole numbers >= 0"),
}))
}
}

View File

@ -62,19 +62,27 @@ pub enum KclValue {
},
TagIdentifier(Box<TagIdentifier>),
TagDeclarator(crate::parsing::ast::types::BoxNode<TagDeclarator>),
Plane(Box<Plane>),
Face(Box<Face>),
Plane {
value: Box<Plane>,
},
Face {
value: Box<Face>,
},
Sketch {
value: Box<Sketch>,
},
Sketches {
value: Vec<Box<Sketch>>,
},
Solid(Box<Solid>),
Solid {
value: Box<Solid>,
},
Solids {
value: Vec<Box<Solid>>,
},
Helix(Box<Helix>),
Helix {
value: Box<Helix>,
},
ImportedGeometry(ImportedGeometry),
#[ts(skip)]
Function {
@ -120,7 +128,7 @@ impl From<Vec<Box<Sketch>>> for KclValue {
impl From<SolidSet> for KclValue {
fn from(eg: SolidSet) -> Self {
match eg {
SolidSet::Solid(eg) => KclValue::Solid(eg),
SolidSet::Solid(eg) => KclValue::Solid { value: eg },
SolidSet::Solids(egs) => KclValue::Solids { value: egs },
}
}
@ -129,7 +137,7 @@ impl From<SolidSet> for KclValue {
impl From<Vec<Box<Solid>>> for KclValue {
fn from(eg: Vec<Box<Solid>>) -> Self {
if eg.len() == 1 {
KclValue::Solid(eg[0].clone())
KclValue::Solid { value: eg[0].clone() }
} else {
KclValue::Solids { value: eg }
}
@ -140,15 +148,15 @@ impl From<KclValue> for Vec<SourceRange> {
match item {
KclValue::TagDeclarator(t) => vec![SourceRange::new(t.start, t.end, t.module_id)],
KclValue::TagIdentifier(t) => to_vec_sr(&t.meta),
KclValue::Solid(e) => to_vec_sr(&e.meta),
KclValue::Solid { value } => to_vec_sr(&value.meta),
KclValue::Solids { value } => value.iter().flat_map(|eg| to_vec_sr(&eg.meta)).collect(),
KclValue::Sketch { value } => to_vec_sr(&value.meta),
KclValue::Sketches { value } => value.iter().flat_map(|eg| to_vec_sr(&eg.meta)).collect(),
KclValue::Helix(e) => to_vec_sr(&e.meta),
KclValue::Helix { value } => to_vec_sr(&value.meta),
KclValue::ImportedGeometry(i) => to_vec_sr(&i.meta),
KclValue::Function { meta, .. } => to_vec_sr(&meta),
KclValue::Plane(p) => to_vec_sr(&p.meta),
KclValue::Face(f) => to_vec_sr(&f.meta),
KclValue::Plane { value } => to_vec_sr(&value.meta),
KclValue::Face { value } => to_vec_sr(&value.meta),
KclValue::Bool { meta, .. } => to_vec_sr(&meta),
KclValue::Number { meta, .. } => to_vec_sr(&meta),
KclValue::Int { meta, .. } => to_vec_sr(&meta),
@ -171,15 +179,15 @@ impl From<&KclValue> for Vec<SourceRange> {
match item {
KclValue::TagDeclarator(t) => vec![SourceRange::new(t.start, t.end, t.module_id)],
KclValue::TagIdentifier(t) => to_vec_sr(&t.meta),
KclValue::Solid(e) => to_vec_sr(&e.meta),
KclValue::Solid { value } => to_vec_sr(&value.meta),
KclValue::Solids { value } => value.iter().flat_map(|eg| to_vec_sr(&eg.meta)).collect(),
KclValue::Sketch { value } => to_vec_sr(&value.meta),
KclValue::Sketches { value } => value.iter().flat_map(|eg| to_vec_sr(&eg.meta)).collect(),
KclValue::Helix(x) => to_vec_sr(&x.meta),
KclValue::Helix { value } => to_vec_sr(&value.meta),
KclValue::ImportedGeometry(i) => to_vec_sr(&i.meta),
KclValue::Function { meta, .. } => to_vec_sr(meta),
KclValue::Plane(p) => to_vec_sr(&p.meta),
KclValue::Face(f) => to_vec_sr(&f.meta),
KclValue::Plane { value } => to_vec_sr(&value.meta),
KclValue::Face { value } => to_vec_sr(&value.meta),
KclValue::Bool { meta, .. } => to_vec_sr(meta),
KclValue::Number { meta, .. } => to_vec_sr(meta),
KclValue::Int { meta, .. } => to_vec_sr(meta),
@ -205,13 +213,13 @@ impl KclValue {
KclValue::Object { value: _, meta } => meta.clone(),
KclValue::TagIdentifier(x) => x.meta.clone(),
KclValue::TagDeclarator(x) => vec![x.metadata()],
KclValue::Plane(x) => x.meta.clone(),
KclValue::Face(x) => x.meta.clone(),
KclValue::Plane { value } => value.meta.clone(),
KclValue::Face { value } => value.meta.clone(),
KclValue::Sketch { value } => value.meta.clone(),
KclValue::Sketches { value } => value.iter().flat_map(|sketch| &sketch.meta).copied().collect(),
KclValue::Solid(x) => x.meta.clone(),
KclValue::Solid { value } => value.meta.clone(),
KclValue::Solids { value } => value.iter().flat_map(|sketch| &sketch.meta).copied().collect(),
KclValue::Helix(x) => x.meta.clone(),
KclValue::Helix { value } => value.meta.clone(),
KclValue::ImportedGeometry(x) => x.meta.clone(),
KclValue::Function { meta, .. } => meta.clone(),
KclValue::Module { meta, .. } => meta.clone(),
@ -230,7 +238,7 @@ impl KclValue {
pub(crate) fn get_solid_set(&self) -> Result<SolidSet> {
match self {
KclValue::Solid(e) => Ok(SolidSet::Solid(e.clone())),
KclValue::Solid { value } => Ok(SolidSet::Solid(value.clone())),
KclValue::Solids { value } => Ok(SolidSet::Solids(value.clone())),
KclValue::Array { value, .. } => {
let solids: Vec<_> = value
@ -266,15 +274,15 @@ impl KclValue {
KclValue::Uuid { .. } => "Unique ID (uuid)",
KclValue::TagDeclarator(_) => "TagDeclarator",
KclValue::TagIdentifier(_) => "TagIdentifier",
KclValue::Solid(_) => "Solid",
KclValue::Solid { .. } => "Solid",
KclValue::Solids { .. } => "Solids",
KclValue::Sketch { .. } => "Sketch",
KclValue::Sketches { .. } => "Sketches",
KclValue::Helix(_) => "Helix",
KclValue::Helix { .. } => "Helix",
KclValue::ImportedGeometry(_) => "ImportedGeometry",
KclValue::Function { .. } => "Function",
KclValue::Plane(_) => "Plane",
KclValue::Face(_) => "Face",
KclValue::Plane { .. } => "Plane",
KclValue::Face { .. } => "Face",
KclValue::Bool { .. } => "boolean (true/false value)",
KclValue::Number { .. } => "number",
KclValue::Int { .. } => "integer",
@ -288,7 +296,7 @@ impl KclValue {
pub(crate) fn from_literal(literal: LiteralValue, meta: Vec<Metadata>) -> Self {
match literal {
LiteralValue::Number(value) => KclValue::Number { value, meta },
LiteralValue::Number { value, .. } => KclValue::Number { value, meta },
LiteralValue::String(value) => KclValue::String { value, meta },
LiteralValue::Bool(value) => KclValue::Bool { value, meta },
}
@ -383,7 +391,7 @@ impl KclValue {
}
pub fn as_plane(&self) -> Option<&Plane> {
if let KclValue::Plane(value) = &self {
if let KclValue::Plane { value } = &self {
Some(value)
} else {
None
@ -391,7 +399,7 @@ impl KclValue {
}
pub fn as_solid(&self) -> Option<&Solid> {
if let KclValue::Solid(value) = &self {
if let KclValue::Solid { value } = &self {
Some(value)
} else {
None
@ -614,6 +622,19 @@ impl From<crate::UnitLength> for UnitLen {
}
}
impl From<UnitLen> for crate::UnitLength {
fn from(unit: UnitLen) -> Self {
match unit {
UnitLen::Cm => crate::UnitLength::Cm,
UnitLen::Feet => crate::UnitLength::Ft,
UnitLen::Inches => crate::UnitLength::In,
UnitLen::M => crate::UnitLength::M,
UnitLen::Mm => crate::UnitLength::Mm,
UnitLen::Yards => crate::UnitLength::Yd,
}
}
}
#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq)]
#[ts(export)]
#[serde(tag = "type")]

View File

@ -1,7 +1,8 @@
//! The executor for the AST.
use std::{path::PathBuf, sync::Arc};
use std::{cmp::Ordering, path::PathBuf, sync::Arc};
use annotations::AnnotationScope;
use anyhow::Result;
use artifact::build_artifact_graph;
use async_recursion::async_recursion;
@ -42,6 +43,7 @@ use crate::{
BodyItem, Expr, FunctionExpression, ImportSelector, ItemVisibility, Node, NodeRef, NonCodeValue,
Program as AstProgram, TagDeclarator, TagNode,
},
refgraph::extract_refgraph,
settings::types::UnitLength,
source_range::{ModuleId, SourceRange},
std::{args::Arg, StdLib},
@ -391,7 +393,7 @@ impl ProgramMemory {
env.bindings
.values()
.filter_map(|item| match item {
KclValue::Solid(eg) if eg.sketch.id == sketch_id => Some(eg.clone()),
KclValue::Solid { value } if value.sketch.id == sketch_id => Some(value.clone()),
_ => None,
})
.collect::<Vec<_>>()
@ -505,8 +507,8 @@ impl DynamicState {
fn append(&mut self, memory: &ProgramMemory) {
for env in &memory.environments {
for item in env.bindings.values() {
if let KclValue::Solid(eg) = item {
self.solid_ids.push(SolidLazyIds::from(eg.as_ref()));
if let KclValue::Solid { value } = item {
self.solid_ids.push(SolidLazyIds::from(value.as_ref()));
}
}
}
@ -759,6 +761,7 @@ pub struct Helix {
pub angle_start: f64,
/// Is the helix rotation counter clockwise?
pub ccw: bool,
pub units: UnitLen,
#[serde(rename = "__meta")]
pub meta: Vec<Metadata>,
}
@ -780,6 +783,7 @@ pub struct Plane {
pub y_axis: Point3d,
/// The z-axis (normal).
pub z_axis: Point3d,
pub units: UnitLen,
#[serde(rename = "__meta")]
pub meta: Vec<Metadata>,
}
@ -795,6 +799,7 @@ impl Plane {
y_axis: Point3d::new(0.0, 1.0, 0.0),
z_axis: Point3d::new(0.0, 0.0, 1.0),
value: PlaneType::XY,
units: exec_state.length_unit(),
meta: vec![],
},
crate::std::sketch::PlaneData::NegXY => Plane {
@ -804,6 +809,7 @@ impl Plane {
y_axis: Point3d::new(0.0, 1.0, 0.0),
z_axis: Point3d::new(0.0, 0.0, -1.0),
value: PlaneType::XY,
units: exec_state.length_unit(),
meta: vec![],
},
crate::std::sketch::PlaneData::XZ => Plane {
@ -813,6 +819,7 @@ impl Plane {
y_axis: Point3d::new(0.0, 0.0, 1.0),
z_axis: Point3d::new(0.0, -1.0, 0.0),
value: PlaneType::XZ,
units: exec_state.length_unit(),
meta: vec![],
},
crate::std::sketch::PlaneData::NegXZ => Plane {
@ -822,6 +829,7 @@ impl Plane {
y_axis: Point3d::new(0.0, 0.0, 1.0),
z_axis: Point3d::new(0.0, 1.0, 0.0),
value: PlaneType::XZ,
units: exec_state.length_unit(),
meta: vec![],
},
crate::std::sketch::PlaneData::YZ => Plane {
@ -831,6 +839,7 @@ impl Plane {
y_axis: Point3d::new(0.0, 0.0, 1.0),
z_axis: Point3d::new(1.0, 0.0, 0.0),
value: PlaneType::YZ,
units: exec_state.length_unit(),
meta: vec![],
},
crate::std::sketch::PlaneData::NegYZ => Plane {
@ -840,6 +849,7 @@ impl Plane {
y_axis: Point3d::new(0.0, 0.0, 1.0),
z_axis: Point3d::new(-1.0, 0.0, 0.0),
value: PlaneType::YZ,
units: exec_state.length_unit(),
meta: vec![],
},
crate::std::sketch::PlaneData::Plane {
@ -854,6 +864,7 @@ impl Plane {
y_axis: *y_axis,
z_axis: *z_axis,
value: PlaneType::Custom,
units: exec_state.length_unit(),
meta: vec![],
},
}
@ -900,6 +911,7 @@ pub struct Face {
pub z_axis: Point3d,
/// The solid the face is on.
pub solid: Box<Solid>,
pub units: UnitLen,
#[serde(rename = "__meta")]
pub meta: Vec<Metadata>,
}
@ -1018,6 +1030,7 @@ pub struct Sketch {
/// is sketched on face etc.
#[serde(skip)]
pub original_id: uuid::Uuid,
pub units: UnitLen,
/// Metadata.
#[serde(rename = "__meta")]
pub meta: Vec<Metadata>,
@ -1141,6 +1154,7 @@ pub struct Solid {
/// Chamfers or fillets on this solid.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub edge_cuts: Vec<EdgeCut>,
pub units: UnitLen,
/// Metadata.
#[serde(rename = "__meta")]
pub meta: Vec<Metadata>,
@ -2304,6 +2318,36 @@ impl ExecutorContext {
}
}
async fn handle_annotations(
&self,
annotations: impl Iterator<Item = (&NonCodeValue, SourceRange)>,
scope: AnnotationScope,
exec_state: &mut ExecState,
) -> Result<(), KclError> {
for (annotation, source_range) in annotations {
if annotation.annotation_name() == Some(annotations::SETTINGS) {
if scope == AnnotationScope::Module {
let old_units = exec_state.length_unit();
exec_state
.mod_local
.settings
.update_from_annotation(annotation, source_range)?;
let new_units = exec_state.length_unit();
if old_units != new_units {
self.engine.set_units(new_units.into(), source_range).await?;
}
} else {
return Err(KclError::Semantic(KclErrorDetails {
message: "Settings can only be modified at the top level scope of a file".to_owned(),
source_ranges: vec![source_range],
}));
}
}
// TODO warn on unknown annotations
}
Ok(())
}
/// Execute an AST's program.
#[async_recursion]
pub(crate) async fn inner_execute<'a>(
@ -2312,21 +2356,16 @@ impl ExecutorContext {
exec_state: &mut ExecState,
body_type: BodyType,
) -> Result<Option<KclValue>, KclError> {
if let Some((annotation, source_range)) = program
.non_code_meta
.start_nodes
.iter()
.filter_map(|n| {
n.annotation(annotations::SETTINGS)
.map(|result| (result, n.as_source_range()))
})
.next()
{
exec_state
.mod_local
.settings
.update_from_annotation(annotation, source_range)?;
}
self.handle_annotations(
program
.non_code_meta
.start_nodes
.iter()
.filter_map(|n| n.annotation().map(|result| (result, n.as_source_range()))),
AnnotationScope::Module,
exec_state,
)
.await?;
let mut last_expr = None;
// Iterate over the body of the program.
@ -2509,6 +2548,7 @@ impl ExecutorContext {
exec_kind: ExecutionKind,
source_range: SourceRange,
) -> Result<(Option<KclValue>, ProgramMemory, Vec<String>), KclError> {
let old_units = exec_state.length_unit();
// TODO It sucks that we have to clone the whole module AST here
let info = exec_state.global.module_infos[&module_id].clone();
@ -2525,7 +2565,11 @@ impl ExecutorContext {
.inner_execute(&info.parsed.unwrap(), exec_state, crate::execution::BodyType::Root)
.await;
let new_units = exec_state.length_unit();
std::mem::swap(&mut exec_state.mod_local, &mut local_state);
if new_units != old_units {
self.engine.set_units(old_units.into(), Default::default()).await?;
}
self.engine.replace_execution_kind(original_execution);
let result = result.map_err(|err| {

View File

@ -66,6 +66,7 @@ pub mod lint;
mod log;
mod lsp;
mod parsing;
mod refgraph;
mod settings;
#[cfg(test)]
mod simulation_tests;

View File

@ -163,7 +163,7 @@ fn get_xyz(point: &ObjectExpression) -> Option<(f64, f64, f64)> {
fn unlitafy(lit: &LiteralValue) -> Option<f64> {
Some(match lit {
LiteralValue::Number(value) => *value,
LiteralValue::Number { value, .. } => *value,
_ => {
return None;
}

View File

@ -1,6 +1,6 @@
use sha2::{Digest as DigestTrait, Sha256};
use super::types::{DefaultParamVal, ItemVisibility, LabelledExpression, VariableKind};
use super::types::{DefaultParamVal, ItemVisibility, LabelledExpression, LiteralValue, VariableKind};
use crate::parsing::ast::types::{
ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryPart, BodyItem, CallExpression, CallExpressionKw,
ElseIf, Expr, ExpressionStatement, FnArgType, FunctionExpression, Identifier, IfExpression, ImportItem,
@ -277,6 +277,26 @@ impl Literal {
});
}
impl LiteralValue {
fn digestable_id(&self) -> Vec<u8> {
match self {
LiteralValue::Number { value, suffix } => {
let mut result: Vec<u8> = value.to_ne_bytes().into();
result.extend((*suffix as u32).to_ne_bytes());
result
}
LiteralValue::String(st) => st.as_bytes().into(),
LiteralValue::Bool(b) => {
if *b {
vec![1]
} else {
vec![0]
}
}
}
}
}
impl Identifier {
compute_digest!(|slf, hasher| {
let name = slf.name.as_bytes();

View File

@ -18,6 +18,8 @@ use crate::{
Program,
};
use super::types::LiteralValue;
type Point3d = kcmc::shared::Point3d<f64>;
#[derive(Debug)]
@ -201,8 +203,8 @@ fn create_start_sketch_on(
"startProfileAt",
vec![
ArrayExpression::new(vec![
Literal::new(round_before_recast(start[0]).into()).into(),
Literal::new(round_before_recast(start[1]).into()).into(),
Literal::new(LiteralValue::from_f64_no_uom(round_before_recast(start[0]))).into(),
Literal::new(LiteralValue::from_f64_no_uom(round_before_recast(start[1]))).into(),
])
.into(),
PipeSubstitution::new().into(),
@ -221,8 +223,8 @@ fn create_start_sketch_on(
"line",
vec![
ArrayExpression::new(vec![
Literal::new(round_before_recast(end[0]).into()).into(),
Literal::new(round_before_recast(end[1]).into()).into(),
Literal::new(LiteralValue::from_f64_no_uom(round_before_recast(end[0]))).into(),
Literal::new(LiteralValue::from_f64_no_uom(round_before_recast(end[1]))).into(),
])
.into(),
PipeSubstitution::new().into(),
@ -254,8 +256,8 @@ fn create_start_sketch_on(
"line",
vec![
ArrayExpression::new(vec![
Literal::new(round_before_recast(line[0]).into()).into(),
Literal::new(round_before_recast(line[1]).into()).into(),
Literal::new(LiteralValue::from_f64_no_uom(round_before_recast(line[0]))).into(),
Literal::new(LiteralValue::from_f64_no_uom(round_before_recast(line[1]))).into(),
])
.into(),
PipeSubstitution::new().into(),

View File

@ -1,31 +1,49 @@
use std::fmt;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value as JValue;
use super::Node;
use crate::parsing::ast::types::{Expr, Literal};
use crate::parsing::{
ast::types::{Expr, Literal},
token::NumericSuffix,
};
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(untagged, rename_all = "snake_case")]
pub enum LiteralValue {
Number(f64),
Number { value: f64, suffix: NumericSuffix },
String(String),
Bool(bool),
}
impl LiteralValue {
pub fn digestable_id(&self) -> Vec<u8> {
pub fn from_f64_no_uom(value: f64) -> Self {
LiteralValue::Number {
value,
suffix: NumericSuffix::None,
}
}
}
impl fmt::Display for LiteralValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LiteralValue::Number(frac) => frac.to_ne_bytes().into(),
LiteralValue::String(st) => st.as_bytes().into(),
LiteralValue::Bool(b) => {
if *b {
vec![1]
LiteralValue::Number { value, suffix } => {
let int_value = *value as u64;
if int_value as f64 == *value {
write!(f, "{int_value}")?;
} else {
vec![0]
write!(f, "{value}")?;
}
if *suffix != NumericSuffix::None {
write!(f, "{suffix}")?;
}
Ok(())
}
LiteralValue::String(s) => write!(f, "\"{s}\""),
LiteralValue::Bool(b) => write!(f, "{b}"),
}
}
}
@ -36,49 +54,12 @@ impl From<Node<Literal>> for Expr {
}
}
impl From<LiteralValue> for JValue {
fn from(value: LiteralValue) -> Self {
match value {
LiteralValue::Number(x) => x.into(),
LiteralValue::String(x) => x.into(),
LiteralValue::Bool(b) => b.into(),
}
}
}
impl From<f64> for LiteralValue {
fn from(value: f64) -> Self {
Self::Number(value)
}
}
impl From<i64> for LiteralValue {
fn from(value: i64) -> Self {
Self::Number(value as f64)
}
}
impl From<String> for LiteralValue {
fn from(value: String) -> Self {
Self::String(value)
}
}
impl From<u32> for LiteralValue {
fn from(value: u32) -> Self {
Self::Number(value as f64)
}
}
impl From<u16> for LiteralValue {
fn from(value: u16) -> Self {
Self::Number(value as f64)
}
}
impl From<u8> for LiteralValue {
fn from(value: u8) -> Self {
Self::Number(value as f64)
}
}
impl From<&'static str> for LiteralValue {
fn from(value: &'static str) -> Self {
// TODO: Make this Cow<str>

View File

@ -13,7 +13,6 @@ use anyhow::Result;
use parse_display::{Display, FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value as JValue;
use tower_lsp::lsp_types::{
CompletionItem, CompletionItemKind, DocumentSymbol, FoldingRange, FoldingRangeKind, Range as LspRange, SymbolKind,
};
@ -1012,9 +1011,9 @@ impl NonCodeNode {
}
}
pub fn annotation(&self, expected_name: &str) -> Option<&NonCodeValue> {
pub fn annotation(&self) -> Option<&NonCodeValue> {
match &self.value {
a @ NonCodeValue::Annotation { name, .. } if name.name == expected_name => Some(a),
a @ NonCodeValue::Annotation { .. } => Some(a),
_ => None,
}
}
@ -1072,6 +1071,15 @@ pub enum NonCodeValue {
},
}
impl NonCodeValue {
pub fn annotation_name(&self) -> Option<&str> {
match self {
NonCodeValue::Annotation { name, .. } => Some(&name.name),
_ => None,
}
}
}
#[derive(Debug, Default, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
@ -1867,7 +1875,7 @@ impl Node<Literal> {
impl Literal {
pub fn new(value: LiteralValue) -> Node<Self> {
Node::no_src(Self {
raw: JValue::from(value.clone()).to_string(),
raw: value.to_string(),
value,
digest: None,
})
@ -1878,7 +1886,7 @@ impl From<Node<Literal>> for KclValue {
fn from(literal: Node<Literal>) -> Self {
let meta = vec![literal.metadata()];
match literal.inner.value {
LiteralValue::Number(value) => KclValue::Number { value, meta },
LiteralValue::Number { value, .. } => KclValue::Number { value, meta },
LiteralValue::String(value) => KclValue::String { value, meta },
LiteralValue::Bool(value) => KclValue::Bool { value, meta },
}

View File

@ -126,7 +126,13 @@ impl From<BinaryOperator> for BinaryExpressionToken {
#[cfg(test)]
mod tests {
use super::*;
use crate::{parsing::ast::types::Literal, source_range::ModuleId};
use crate::{
parsing::{
ast::types::{Literal, LiteralValue},
token::NumericSuffix,
},
source_range::ModuleId,
};
#[test]
fn parse_and_evaluate() {
@ -134,7 +140,10 @@ mod tests {
fn lit(n: u8) -> BinaryPart {
BinaryPart::Literal(Box::new(Node::new(
Literal {
value: n.into(),
value: LiteralValue::Number {
value: n as f64,
suffix: NumericSuffix::None,
},
raw: n.to_string(),
digest: None,
},

View File

@ -483,7 +483,7 @@ pub(crate) fn unsigned_number_literal(i: &mut TokenSlice) -> PResult<Node<Litera
let (value, token) = any
.try_map(|token: Token| match token.token_type {
TokenType::Number => {
let x: f64 = token.numeric_value().ok_or_else(|| {
let value: f64 = token.numeric_value().ok_or_else(|| {
CompilationError::fatal(token.as_source_range(), format!("Invalid float: {}", token.value))
})?;
@ -494,7 +494,13 @@ pub(crate) fn unsigned_number_literal(i: &mut TokenSlice) -> PResult<Node<Litera
));
}
Ok((LiteralValue::Number(x), token))
Ok((
LiteralValue::Number {
value,
suffix: token.numeric_suffix(),
},
token,
))
}
_ => Err(CompilationError::fatal(token.as_source_range(), "invalid literal")),
})
@ -844,13 +850,13 @@ fn object_property(i: &mut TokenSlice) -> PResult<Node<ObjectProperty>> {
};
if sep.token_type == TokenType::Colon {
ParseContext::warn(CompilationError::with_suggestion(
sep.into(),
Some(result.as_source_range()),
"Using `:` to initialize objects is deprecated, prefer using `=`.",
Some(("Replace `:` with `=`", " =")),
Tag::Deprecated,
));
ParseContext::warn(
CompilationError::err(
sep.into(),
"Using `:` to initialize objects is deprecated, prefer using `=`.",
)
.with_suggestion("Replace `:` with `=`", " =", Tag::Deprecated),
);
}
Ok(result)
@ -1069,9 +1075,19 @@ fn function_expr(i: &mut TokenSlice) -> PResult<Expr> {
let fn_tok = opt(fun).parse_next(i)?;
ignore_whitespace(i);
let (result, has_arrow) = function_decl.parse_next(i)?;
if fn_tok.is_none() && !has_arrow {
let err = CompilationError::fatal(result.as_source_range(), "Anonymous function requires `fn` before `(`");
return Err(ErrMode::Cut(err.into()));
if fn_tok.is_none() {
if has_arrow {
ParseContext::warn(
CompilationError::err(
result.as_source_range().start_as_range(),
"Missing `fn` in function declaration",
)
.with_suggestion("Add `fn`", "fn", Tag::None),
);
} else {
let err = CompilationError::fatal(result.as_source_range(), "Anonymous function requires `fn` before `(`");
return Err(ErrMode::Cut(err.into()));
}
}
Ok(Expr::FunctionExpression(Box::new(result)))
}
@ -1113,18 +1129,16 @@ fn function_decl(i: &mut TokenSlice) -> PResult<(Node<FunctionExpression>, bool)
open.module_id,
);
let has_arrow = if let Some(arrow) = arrow {
ParseContext::warn(CompilationError::with_suggestion(
arrow.as_source_range(),
Some(result.as_source_range()),
"Unnecessary `=>` in function declaration",
Some(("Remove `=>`", "")),
Tag::Unnecessary,
));
true
} else {
false
};
let has_arrow =
if let Some(arrow) = arrow {
ParseContext::warn(
CompilationError::err(arrow.as_source_range(), "Unnecessary `=>` in function declaration")
.with_suggestion("Remove `=>`", "", Tag::Unnecessary),
);
true
} else {
false
};
Ok((result, has_arrow))
}
@ -1825,67 +1839,60 @@ fn declaration(i: &mut TokenSlice) -> PResult<BoxNode<VariableDeclaration>> {
ignore_whitespace(i);
let val = if kind == VariableKind::Fn {
let eq = opt(equals).parse_next(i)?;
ignore_whitespace(i);
let val =
if kind == VariableKind::Fn {
let eq = opt(equals).parse_next(i)?;
ignore_whitespace(i);
let val = function_decl
.map(|t| Box::new(t.0))
.map(Expr::FunctionExpression)
.context(expected("a KCL function expression, like () { return 1 }"))
.parse_next(i);
let val = function_decl
.map(|t| Box::new(t.0))
.map(Expr::FunctionExpression)
.context(expected("a KCL function expression, like () { return 1 }"))
.parse_next(i);
if let Some(t) = eq {
let ctxt_end = val.as_ref().map(|e| e.end()).unwrap_or(t.end);
ParseContext::warn(CompilationError::with_suggestion(
t.as_source_range(),
Some(SourceRange::new(id.start, ctxt_end, id.module_id)),
"Unnecessary `=` in function declaration",
Some(("Remove `=`", "")),
Tag::Unnecessary,
));
if let Some(t) = eq {
ParseContext::warn(
CompilationError::err(t.as_source_range(), "Unnecessary `=` in function declaration")
.with_suggestion("Remove `=`", "", Tag::Unnecessary),
);
}
val
} else {
equals(i)?;
ignore_whitespace(i);
let val = expression
.try_map(|val| {
// Function bodies can be used if and only if declaring a function.
// Check the 'if' direction:
if matches!(val, Expr::FunctionExpression(_)) {
return Err(CompilationError::fatal(
SourceRange::new(start, dec_end, id.module_id),
format!("Expected a `fn` variable kind, found: `{}`", kind),
));
}
Ok(val)
})
.context(expected("a KCL value, which is being bound to a variable"))
.parse_next(i);
if let Some((_, tok)) = decl_token {
ParseContext::warn(
CompilationError::err(
tok.as_source_range(),
format!(
"Using `{}` to declare constants is deprecated; no keyword is required",
tok.value
),
)
.with_suggestion(format!("Remove `{}`", tok.value), "", Tag::Deprecated),
);
}
val
}
val
} else {
equals(i)?;
ignore_whitespace(i);
let val = expression
.try_map(|val| {
// Function bodies can be used if and only if declaring a function.
// Check the 'if' direction:
if matches!(val, Expr::FunctionExpression(_)) {
return Err(CompilationError::fatal(
SourceRange::new(start, dec_end, id.module_id),
format!("Expected a `fn` variable kind, found: `{}`", kind),
));
}
Ok(val)
})
.context(expected("a KCL value, which is being bound to a variable"))
.parse_next(i);
if let Some((_, tok)) = decl_token {
ParseContext::warn(CompilationError::with_suggestion(
tok.as_source_range(),
Some(SourceRange::new(
id.start,
val.as_ref().map(|e| e.end()).unwrap_or(dec_end),
id.module_id,
)),
format!(
"Using `{}` to declare constants is deprecated; no keyword is required",
tok.value
),
Some((format!("Remove `{}`", tok.value), "")),
Tag::Deprecated,
));
}
val
}
.map_err(|e| e.cut())?;
.map_err(|e| e.cut())?;
let end = val.end();
Ok(Box::new(Node {
@ -2856,7 +2863,10 @@ mySk1 = startSketchAt([0, 0])"#;
ReturnStatement {
argument: Expr::Literal(Box::new(Node::new(
Literal {
value: 2u32.into(),
value: LiteralValue::Number {
value: 2.0,
suffix: NumericSuffix::None
},
raw: "2".to_owned(),
digest: None,
},
@ -3057,7 +3067,15 @@ mySk1 = startSketchAt([0, 0])"#;
match &rhs.right {
BinaryPart::Literal(lit) => {
assert!(lit.start == 9 && lit.end == 10);
assert!(lit.value == 3u32.into() && &lit.raw == "3" && lit.digest.is_none());
assert!(
lit.value
== LiteralValue::Number {
value: 3.0,
suffix: NumericSuffix::None
}
&& &lit.raw == "3"
&& lit.digest.is_none()
);
}
_ => panic!(),
}
@ -3128,11 +3146,23 @@ mySk1 = startSketchAt([0, 0])"#;
let BinaryPart::Literal(left) = actual.inner.left else {
panic!("should be expression");
};
assert_eq!(left.value, 1u32.into());
assert_eq!(
left.value,
LiteralValue::Number {
value: 1.0,
suffix: NumericSuffix::None
}
);
let BinaryPart::Literal(right) = actual.inner.right else {
panic!("should be expression");
};
assert_eq!(right.value, 2u32.into());
assert_eq!(
right.value,
LiteralValue::Number {
value: 2.0,
suffix: NumericSuffix::None
}
);
}
}
@ -3449,7 +3479,10 @@ mySk1 = startSketchAt([0, 0])"#;
operator: BinaryOperator::Add,
left: BinaryPart::Literal(Box::new(Node::new(
Literal {
value: 5u32.into(),
value: LiteralValue::Number {
value: 5.0,
suffix: NumericSuffix::None,
},
raw: "5".to_owned(),
digest: None,
},
@ -3498,7 +3531,10 @@ mySk1 = startSketchAt([0, 0])"#;
BinaryExpression {
left: BinaryPart::Literal(Box::new(Node::new(
Literal {
value: 5u32.into(),
value: LiteralValue::Number {
value: 5.0,
suffix: NumericSuffix::None,
},
raw: "5".to_string(),
digest: None,
},
@ -3509,7 +3545,10 @@ mySk1 = startSketchAt([0, 0])"#;
operator: BinaryOperator::Add,
right: BinaryPart::Literal(Box::new(Node::new(
Literal {
value: 6u32.into(),
value: LiteralValue::Number {
value: 6.0,
suffix: NumericSuffix::None,
},
raw: "6".to_string(),
digest: None,
},
@ -4345,6 +4384,20 @@ sketch001 = startSketchOn('XZ') |> startProfileAt([90.45, 119.09, %)"#;
return 0
}"#
);
let some_program_string = r#"myMap = map([0..5], (n) => {
return n * 2
})"#;
let (_, errs) = assert_no_err(some_program_string);
assert_eq!(errs.len(), 2);
let replaced = errs[0].apply_suggestion(some_program_string).unwrap();
let replaced = errs[1].apply_suggestion(&replaced).unwrap();
assert_eq!(
replaced,
r#"myMap = map([0..5], fn(n) {
return n * 2
})"#
);
}
#[test]

View File

@ -1,8 +1,6 @@
---
source: kcl/src/parsing/parser.rs
assertion_line: 3851
expression: actual
snapshot_kind: text
---
{
"type": "BinaryExpression",
@ -10,7 +8,10 @@ snapshot_kind: text
"left": {
"type": "Literal",
"type": "Literal",
"value": 1.0,
"value": {
"value": 1.0,
"suffix": "None"
},
"raw": "1",
"start": 0,
"end": 1
@ -18,7 +19,10 @@ snapshot_kind: text
"right": {
"type": "Literal",
"type": "Literal",
"value": 2.0,
"value": {
"value": 2.0,
"suffix": "None"
},
"raw": "2",
"start": 4,
"end": 5

View File

@ -1,8 +1,6 @@
---
source: kcl/src/parsing/parser.rs
assertion_line: 3852
expression: actual
snapshot_kind: text
---
{
"type": "BinaryExpression",
@ -10,7 +8,10 @@ snapshot_kind: text
"left": {
"type": "Literal",
"type": "Literal",
"value": 1.0,
"value": {
"value": 1.0,
"suffix": "None"
},
"raw": "1",
"start": 0,
"end": 1
@ -18,7 +19,10 @@ snapshot_kind: text
"right": {
"type": "Literal",
"type": "Literal",
"value": 2.0,
"value": {
"value": 2.0,
"suffix": "None"
},
"raw": "2",
"start": 2,
"end": 3

View File

@ -1,8 +1,6 @@
---
source: kcl/src/parsing/parser.rs
assertion_line: 3853
expression: actual
snapshot_kind: text
---
{
"type": "BinaryExpression",
@ -10,7 +8,10 @@ snapshot_kind: text
"left": {
"type": "Literal",
"type": "Literal",
"value": 1.0,
"value": {
"value": 1.0,
"suffix": "None"
},
"raw": "1",
"start": 0,
"end": 1
@ -18,7 +19,10 @@ snapshot_kind: text
"right": {
"type": "Literal",
"type": "Literal",
"value": 2.0,
"value": {
"value": 2.0,
"suffix": "None"
},
"raw": "2",
"start": 3,
"end": 4

View File

@ -1,8 +1,6 @@
---
source: kcl/src/parsing/parser.rs
assertion_line: 3854
expression: actual
snapshot_kind: text
---
{
"type": "BinaryExpression",
@ -10,7 +8,10 @@ snapshot_kind: text
"left": {
"type": "Literal",
"type": "Literal",
"value": 1.0,
"value": {
"value": 1.0,
"suffix": "None"
},
"raw": "1",
"start": 0,
"end": 1
@ -22,7 +23,10 @@ snapshot_kind: text
"left": {
"type": "Literal",
"type": "Literal",
"value": 2.0,
"value": {
"value": 2.0,
"suffix": "None"
},
"raw": "2",
"start": 4,
"end": 5
@ -30,7 +34,10 @@ snapshot_kind: text
"right": {
"type": "Literal",
"type": "Literal",
"value": 3.0,
"value": {
"value": 3.0,
"suffix": "None"
},
"raw": "3",
"start": 8,
"end": 9

View File

@ -1,8 +1,6 @@
---
source: kcl/src/parsing/parser.rs
assertion_line: 3855
expression: actual
snapshot_kind: text
---
{
"type": "BinaryExpression",
@ -10,7 +8,10 @@ snapshot_kind: text
"left": {
"type": "Literal",
"type": "Literal",
"value": 1.0,
"value": {
"value": 1.0,
"suffix": "None"
},
"raw": "1",
"start": 0,
"end": 1
@ -22,7 +23,10 @@ snapshot_kind: text
"left": {
"type": "Literal",
"type": "Literal",
"value": 2.0,
"value": {
"value": 2.0,
"suffix": "None"
},
"raw": "2",
"start": 6,
"end": 7
@ -30,7 +34,10 @@ snapshot_kind: text
"right": {
"type": "Literal",
"type": "Literal",
"value": 3.0,
"value": {
"value": 3.0,
"suffix": "None"
},
"raw": "3",
"start": 10,
"end": 11

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