Compare commits

..

1 Commits

Author SHA1 Message Date
4322d3633e Revert "Add uniqueness check to "Create project" command (#5100)"
This reverts commit dac91d3b79.
2025-01-17 15:15:24 -05:00
349 changed files with 70835 additions and 101728 deletions

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

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@ -22,7 +22,6 @@ A plane.
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the planes X axis be? | No | | `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 | | `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 | | `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 | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |

View File

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

View File

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

View File

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

View File

@ -23,7 +23,6 @@ An solid is a collection of extrude surfaces.
| `startCapId` |`string`| The id of the extrusion start cap | No | | `startCapId` |`string`| The id of the extrusion start cap | No |
| `endCapId` |`string`| The id of the extrusion end 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 | | `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 | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |

View File

@ -32,7 +32,6 @@ An solid is a collection of extrude surfaces.
| `startCapId` |`string`| The id of the extrusion start cap | No | | `startCapId` |`string`| The id of the extrusion start cap | No |
| `endCapId` |`string`| The id of the extrusion end 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 | | `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 | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |

View File

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

View File

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

View File

@ -1,8 +1,7 @@
import { test, expect } from './zoo-test' import { test, expect } from './zoo-test'
import * as fsp from 'fs/promises'
import { executorInputPath, getUtils } from './test-utils' import { getUtils } from './test-utils'
import { KCL_DEFAULT_LENGTH } from 'lib/constants' import { KCL_DEFAULT_LENGTH } from 'lib/constants'
import path from 'path'
test.describe('Command bar tests', () => { test.describe('Command bar tests', () => {
test('Extrude from command bar selects extrude line after', async ({ test('Extrude from command bar selects extrude line after', async ({
@ -306,132 +305,4 @@ test.describe('Command bar tests', () => {
await arcToolCommand.click() await arcToolCommand.click()
await expect(arcToolButton).toHaveAttribute('aria-pressed', 'true') await expect(arcToolButton).toHaveAttribute('aria-pressed', 'true')
}) })
test(`Reacts to query param to open "import from URL" command`, async ({
page,
cmdBar,
editor,
homePage,
}) => {
await test.step(`Prepare and navigate to home page with query params`, async () => {
const targetURL = `?create-file&name=test&units=mm&code=ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg%3D%3D&ask-open-desktop`
await homePage.expectState({
projectCards: [],
sortBy: 'last-modified-desc',
})
await page.goto(page.url() + targetURL)
expect(page.url()).toContain(targetURL)
})
await test.step(`Submit the command`, async () => {
await cmdBar.expectState({
stage: 'arguments',
commandName: 'Import file from URL',
currentArgKey: 'method',
currentArgValue: '',
headerArguments: {
Method: '',
Name: 'test',
Code: '1 line',
},
highlightedHeaderArg: 'method',
})
await cmdBar.selectOption({ name: 'New Project' }).click()
await cmdBar.expectState({
stage: 'review',
commandName: 'Import file from URL',
headerArguments: {
Method: 'New project',
Name: 'test',
Code: '1 line',
},
})
await cmdBar.progressCmdBar()
})
await test.step(`Ensure we created the project and are in the modeling scene`, async () => {
await editor.expectEditor.toContain('extrusionDistance = 12')
})
})
test(`"import from URL" can add to existing project`, async ({
page,
cmdBar,
editor,
homePage,
toolbar,
context,
}) => {
await context.folderSetupFn(async (dir) => {
const testProjectDir = path.join(dir, 'testProjectDir')
await Promise.all([fsp.mkdir(testProjectDir, { recursive: true })])
await Promise.all([
fsp.copyFile(
executorInputPath('cylinder.kcl'),
path.join(testProjectDir, 'main.kcl')
),
])
})
await test.step(`Prepare and navigate to home page with query params`, async () => {
const targetURL = `?create-file&name=test&units=mm&code=ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg%3D%3D&ask-open-desktop`
await homePage.expectState({
projectCards: [
{
fileCount: 1,
title: 'testProjectDir',
},
],
sortBy: 'last-modified-desc',
})
await page.goto(page.url() + targetURL)
expect(page.url()).toContain(targetURL)
})
await test.step(`Submit the command`, async () => {
await cmdBar.expectState({
stage: 'arguments',
commandName: 'Import file from URL',
currentArgKey: 'method',
currentArgValue: '',
headerArguments: {
Method: '',
Name: 'test',
Code: '1 line',
},
highlightedHeaderArg: 'method',
})
await cmdBar.selectOption({ name: 'Existing Project' }).click()
await cmdBar.expectState({
stage: 'arguments',
commandName: 'Import file from URL',
currentArgKey: 'projectName',
currentArgValue: '',
headerArguments: {
Method: 'Existing project',
Name: 'test',
ProjectName: '',
Code: '1 line',
},
highlightedHeaderArg: 'projectName',
})
await cmdBar.selectOption({ name: 'testProjectDir' }).click()
await cmdBar.expectState({
stage: 'review',
commandName: 'Import file from URL',
headerArguments: {
Method: 'Existing project',
ProjectName: 'testProjectDir',
Name: 'test',
Code: '1 line',
},
})
await cmdBar.progressCmdBar()
})
await test.step(`Ensure we created the project and are in the modeling scene`, async () => {
await editor.expectEditor.toContain('extrusionDistance = 12')
await toolbar.openPane('files')
await toolbar.expectFileTreeState(['main.kcl', 'test.kcl'])
})
})
}) })

View File

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

View File

@ -135,27 +135,4 @@ export class CmdBarFixture {
await promptEditCommand.first().click() await promptEditCommand.first().click()
} }
} }
get cmdSearchInput() {
return this.page.getByTestId('cmd-bar-search')
}
get argumentInput() {
return this.page.getByTestId('cmd-bar-arg-value')
}
get cmdOptions() {
return this.page.getByTestId('cmd-bar-option')
}
chooseCommand = async (commandName: string) => {
await this.cmdOptions.getByText(commandName).click()
}
/**
* Select an option from the command bar
*/
selectOption = (options: Parameters<typeof this.page.getByRole>[1]) => {
return this.page.getByRole('option', options)
}
} }

View File

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

View File

@ -63,10 +63,6 @@ export class ToolbarFixture {
this.exeIndicator = page.getByTestId('model-state-indicator-execution-done') this.exeIndicator = page.getByTestId('model-state-indicator-execution-done')
} }
get logoLink() {
return this.page.getByTestId('app-logo')
}
startSketchPlaneSelection = async () => startSketchPlaneSelection = async () =>
doAndWaitForImageDiff(this.page, () => this.startSketchBtn.click(), 500) doAndWaitForImageDiff(this.page, () => this.startSketchBtn.click(), 500)

View File

@ -963,31 +963,37 @@ sketch002 = startSketchOn('XZ')
await toolbar.sweepButton.click() await toolbar.sweepButton.click()
await cmdBar.expectState({ await cmdBar.expectState({
commandName: 'Sweep', commandName: 'Sweep',
currentArgKey: 'target', currentArgKey: 'profile',
currentArgValue: '', currentArgValue: '',
headerArguments: { headerArguments: {
Target: '', Path: '',
Trajectory: '', Profile: '',
}, },
highlightedHeaderArg: 'target', highlightedHeaderArg: 'profile',
stage: 'arguments', stage: 'arguments',
}) })
await clickOnSketch1() await clickOnSketch1()
await cmdBar.expectState({ await cmdBar.expectState({
commandName: 'Sweep', commandName: 'Sweep',
currentArgKey: 'trajectory', currentArgKey: 'path',
currentArgValue: '', currentArgValue: '',
headerArguments: { headerArguments: {
Target: '1 face', Path: '',
Trajectory: '', Profile: '1 face',
}, },
highlightedHeaderArg: 'trajectory', highlightedHeaderArg: 'path',
stage: 'arguments', stage: 'arguments',
}) })
await clickOnSketch2() await clickOnSketch2()
await page.waitForTimeout(500) await cmdBar.expectState({
commandName: 'Sweep',
headerArguments: {
Path: '1 face',
Profile: '1 face',
},
stage: 'review',
})
await cmdBar.progressCmdBar() await cmdBar.progressCmdBar()
await page.waitForTimeout(500)
}) })
await test.step(`Confirm code is added to the editor, scene has changed`, async () => { await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
@ -1014,75 +1020,6 @@ sketch002 = startSketchOn('XZ')
}) })
}) })
test(`Sweep point-and-click failing validation`, async ({
context,
page,
homePage,
scene,
toolbar,
cmdBar,
}) => {
const initialCode = `sketch001 = startSketchOn('YZ')
|> circle({
center = [0, 0],
radius = 500
}, %)
sketch002 = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> xLine(-500, %)
|> lineTo([-2000, 500], %)
`
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
// One dumb hardcoded screen pixel value
const testPoint = { x: 700, y: 250 }
const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const [clickOnSketch2] = scene.makeMouseHelpers(testPoint.x - 50, testPoint.y)
await test.step(`Look for sketch001`, async () => {
await toolbar.closePane('code')
await scene.expectPixelColor([53, 53, 53], testPoint, 15)
})
await test.step(`Go through the command bar flow and fail validation with a toast`, async () => {
await toolbar.sweepButton.click()
await cmdBar.expectState({
commandName: 'Sweep',
currentArgKey: 'target',
currentArgValue: '',
headerArguments: {
Target: '',
Trajectory: '',
},
highlightedHeaderArg: 'target',
stage: 'arguments',
})
await clickOnSketch1()
await cmdBar.expectState({
commandName: 'Sweep',
currentArgKey: 'trajectory',
currentArgValue: '',
headerArguments: {
Target: '1 face',
Trajectory: '',
},
highlightedHeaderArg: 'trajectory',
stage: 'arguments',
})
await clickOnSketch2()
await page.waitForTimeout(500)
await cmdBar.progressCmdBar()
await expect(
page.getByText('Unable to sweep with the provided selection')
).toBeVisible()
})
})
test(`Fillet point-and-click`, async ({ test(`Fillet point-and-click`, async ({
context, context,
page, page,

View File

@ -172,7 +172,7 @@ test(
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
await expect(page.getByText('broken-code')).toBeVisible() await expect(page.getByText('broken-code')).toBeVisible()
await expect(page.getByText('bracket')).toBeVisible() await expect(page.getByText('bracket')).toBeVisible()
await expect(page.getByText('Create project')).toBeVisible() await expect(page.getByText('New Project')).toBeVisible()
}) })
await test.step('opening broken code project should clear the scene and show the error', async () => { await test.step('opening broken code project should clear the scene and show the error', async () => {
// Go back home. // Go back home.
@ -253,7 +253,7 @@ test(
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
await expect(page.getByText('empty')).toBeVisible() await expect(page.getByText('empty')).toBeVisible()
await expect(page.getByText('bracket')).toBeVisible() await expect(page.getByText('bracket')).toBeVisible()
await expect(page.getByText('Create project')).toBeVisible() await expect(page.getByText('New Project')).toBeVisible()
}) })
await test.step('opening empty code project should clear the scene', async () => { await test.step('opening empty code project should clear the scene', async () => {
// Go back home. // Go back home.
@ -985,126 +985,6 @@ test.describe(`Project management commands`, () => {
}) })
} }
) )
test(`Create a new project with a colliding name`, async ({
context,
homePage,
toolbar,
cmdBar,
}) => {
const projectName = 'test-project'
await test.step(`Setup`, async () => {
await context.folderSetupFn(async (dir) => {
const projectDir = path.join(dir, projectName)
await Promise.all([fsp.mkdir(projectDir, { recursive: true })])
await Promise.all([
fsp.copyFile(
executorInputPath('router-template-slate.kcl'),
path.join(projectDir, 'main.kcl')
),
])
})
await homePage.expectState({
projectCards: [
{
title: projectName,
fileCount: 1,
},
],
sortBy: 'last-modified-desc',
})
})
await test.step('Create a new project with the same name', async () => {
await cmdBar.openCmdBar()
await cmdBar.chooseCommand('create project')
await cmdBar.expectState({
stage: 'arguments',
commandName: 'Create project',
currentArgKey: 'name',
currentArgValue: '',
headerArguments: {
Name: '',
},
highlightedHeaderArg: 'name',
})
await cmdBar.argumentInput.fill(projectName)
await cmdBar.progressCmdBar()
})
await test.step(`Check the project was created with a non-colliding name`, async () => {
await toolbar.logoLink.click()
await homePage.expectState({
projectCards: [
{
title: projectName + '-1',
fileCount: 1,
},
{
title: projectName,
fileCount: 1,
},
],
sortBy: 'last-modified-desc',
})
})
await test.step('Create another project with the same name', async () => {
await cmdBar.openCmdBar()
await cmdBar.chooseCommand('create project')
await cmdBar.expectState({
stage: 'arguments',
commandName: 'Create project',
currentArgKey: 'name',
currentArgValue: '',
headerArguments: {
Name: '',
},
highlightedHeaderArg: 'name',
})
await cmdBar.argumentInput.fill(projectName)
await cmdBar.progressCmdBar()
})
await test.step(`Check the second project was created with a non-colliding name`, async () => {
await toolbar.logoLink.click()
await homePage.expectState({
projectCards: [
{
title: projectName + '-2',
fileCount: 1,
},
{
title: projectName + '-1',
fileCount: 1,
},
{
title: projectName,
fileCount: 1,
},
],
sortBy: 'last-modified-desc',
})
})
})
})
test(`Create a few projects using the default project name`, async ({
homePage,
toolbar,
}) => {
for (let i = 0; i < 12; i++) {
await test.step(`Create project ${i}`, async () => {
await homePage.expectState({
projectCards: Array.from({ length: i }, (_, i) => ({
title: `project-${i.toString().padStart(3, '0')}`,
fileCount: 1,
})).toReversed(),
sortBy: 'last-modified-desc',
})
await homePage.createAndGoToProject()
await toolbar.logoLink.click()
})
}
}) })
test( test(
@ -1511,7 +1391,7 @@ extrude001 = extrude(200, sketch001)`)
await page.getByTestId('app-logo').click() await page.getByTestId('app-logo').click()
await expect( await expect(
page.getByRole('button', { name: 'Create project' }) page.getByRole('button', { name: 'New project' })
).toBeVisible() ).toBeVisible()
for (let i = 1; i <= 10; i++) { for (let i = 1; i <= 10; i++) {
@ -1585,7 +1465,7 @@ test(
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
await expect(page.getByText('router-template-slate')).toBeVisible() await expect(page.getByText('router-template-slate')).toBeVisible()
await expect(page.getByText('Create project')).toBeVisible() await expect(page.getByText('New Project')).toBeVisible()
}) })
await test.step('Opening the router-template project should load the stream', async () => { await test.step('Opening the router-template project should load the stream', async () => {
@ -1614,7 +1494,7 @@ test(
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
await expect(page.getByText('router-template-slate')).toBeVisible() await expect(page.getByText('router-template-slate')).toBeVisible()
await expect(page.getByText('Create project')).toBeVisible() await expect(page.getByText('New Project')).toBeVisible()
}) })
} }
) )

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

After

Width:  |  Height:  |  Size: 145 KiB

View File

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

View File

@ -113,9 +113,9 @@
"test:unit": "vitest run --mode development --exclude **/kclSamples.test.ts", "test:unit": "vitest run --mode development --exclude **/kclSamples.test.ts",
"test:unit:kcl-samples": "vitest run --mode development ./src/lang/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": "playwright test --config=playwright.electron.config.ts --grep-invert='@snapshot'",
"test:playwright:electron:windows": "playwright test --config=playwright.electron.config.ts --grep-invert=\"@skipWin|@snapshot\" --quiet", "test:playwright:electron: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' --quiet", "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' --quiet", "test:playwright:electron:ubuntu": "playwright test --config=playwright.electron.config.ts --grep-invert='@skipLinux|@snapshot'",
"test:playwright:electron:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@snapshot'", "test:playwright:electron: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: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'", "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", "ts-node": "^10.0.0",
"typescript": "^5.7.3", "typescript": "^5.7.3",
"typescript-eslint": "^8.19.1", "typescript-eslint": "^8.19.1",
"vite": "^5.4.12", "vite": "^5.4.6",
"vite-plugin-package-version": "^1.1.0", "vite-plugin-package-version": "^1.1.0",
"vite-tsconfig-paths": "^4.3.2", "vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.6.0", "vitest": "^1.6.0",

View File

@ -683,9 +683,9 @@ vite-tsconfig-paths@^4.3.2:
tsconfck "^3.0.3" tsconfck "^3.0.3"
vite@^5.0.0: vite@^5.0.0:
version "5.4.14" version "5.4.11"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.14.tgz#ff8255edb02134df180dcfca1916c37a6abe8408" resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.11.tgz#3b415cd4aed781a356c1de5a9ebafb837715f6e5"
integrity sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA== integrity sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==
dependencies: dependencies:
esbuild "^0.21.3" esbuild "^0.21.3"
postcss "^8.4.43" postcss "^8.4.43"

View File

@ -22,28 +22,13 @@ import Gizmo from 'components/Gizmo'
import { CoreDumpManager } from 'lib/coredump' import { CoreDumpManager } from 'lib/coredump'
import { UnitsMenu } from 'components/UnitsMenu' import { UnitsMenu } from 'components/UnitsMenu'
import { CameraProjectionToggle } from 'components/CameraProjectionToggle' import { CameraProjectionToggle } from 'components/CameraProjectionToggle'
import { useCreateFileLinkQuery } from 'hooks/useCreateFileLinkQueryWatcher'
import { maybeWriteToDisk } from 'lib/telemetry' import { maybeWriteToDisk } from 'lib/telemetry'
import { commandBarActor } from 'machines/commandBarMachine'
maybeWriteToDisk() maybeWriteToDisk()
.then(() => {}) .then(() => {})
.catch(() => {}) .catch(() => {})
export function App() { export function App() {
const { project, file } = useLoaderData() as IndexLoaderData const { project, file } = useLoaderData() as IndexLoaderData
// Keep a lookout for a URL query string that invokes the 'import file from URL' command
useCreateFileLinkQuery((argDefaultValues) => {
commandBarActor.send({
type: 'Find and select command',
data: {
groupId: 'projects',
name: 'Import file from URL',
argDefaultValues,
},
})
})
useRefreshSettings(PATHS.FILE + 'SETTINGS') useRefreshSettings(PATHS.FILE + 'SETTINGS')
const navigate = useNavigate() const navigate = useNavigate()
const filePath = useAbsoluteFilePath() const filePath = useAbsoluteFilePath()

View File

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

View File

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

View File

@ -46,8 +46,8 @@ import {
} from 'lang/modifyAst' } from 'lang/modifyAst'
import { ActionButton } from 'components/ActionButton' import { ActionButton } from 'components/ActionButton'
import { err, reportRejection, trap } from 'lib/trap' import { err, reportRejection, trap } from 'lib/trap'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
import { commandBarActor } from 'machines/commandBarMachine'
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } { function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
const [isCamMoving, setIsCamMoving] = useState(false) const [isCamMoving, setIsCamMoving] = useState(false)
@ -510,6 +510,7 @@ const ConstraintSymbol = ({
constrainInfo: ConstrainInfo constrainInfo: ConstrainInfo
verticalPosition: 'top' | 'bottom' verticalPosition: 'top' | 'bottom'
}) => { }) => {
const { commandBarSend } = useCommandsContext()
const { context } = useModelingContext() const { context } = useModelingContext()
const varNameMap: { const varNameMap: {
[key in ConstrainInfo['type']]: { [key in ConstrainInfo['type']]: {
@ -629,7 +630,7 @@ const ConstraintSymbol = ({
// disabled={implicitDesc} TODO why does this change styles that are hard to override? // disabled={implicitDesc} TODO why does this change styles that are hard to override?
onClick={toSync(async () => { onClick={toSync(async () => {
if (!isConstrained) { if (!isConstrained) {
commandBarActor.send({ commandBarSend({
type: 'Find and select command', type: 'Find and select command',
data: { data: {
name: 'Constrain with named value', name: 'Constrain with named value',
@ -755,6 +756,7 @@ export const CamDebugSettings = () => {
sceneInfra.camControls.reactCameraProperties sceneInfra.camControls.reactCameraProperties
) )
const [fov, setFov] = useState(12) const [fov, setFov] = useState(12)
const { commandBarSend } = useCommandsContext()
useEffect(() => { useEffect(() => {
sceneInfra.camControls.setReactCameraPropertiesCallback(setCamSettings) sceneInfra.camControls.setReactCameraPropertiesCallback(setCamSettings)
@ -773,7 +775,7 @@ export const CamDebugSettings = () => {
type="checkbox" type="checkbox"
checked={camSettings.type === 'perspective'} checked={camSettings.type === 'perspective'}
onChange={() => onChange={() =>
commandBarActor.send({ commandBarSend({
type: 'Find and select command', type: 'Find and select command',
data: { data: {
groupId: 'settings', groupId: 'settings',

View File

@ -69,8 +69,7 @@ import {
codeManager, codeManager,
editorManager, editorManager,
} from 'lib/singletons' } from 'lib/singletons'
import { getNodeFromPath } from 'lang/queryAst' import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { executeAst, ToolTip } from 'lang/langHelpers' import { executeAst, ToolTip } from 'lang/langHelpers'
import { import {
createProfileStartHandle, createProfileStartHandle,
@ -1399,23 +1398,23 @@ export class SceneEntities {
const arg0 = arg(kclCircle3PointArgs[0]) const arg0 = arg(kclCircle3PointArgs[0])
if (!arg0) return kclManager.ast if (!arg0) return kclManager.ast
arg0[0].value = { value: points[0].x, suffix: 'None' } arg0[0].value = points[0].x
arg0[0].raw = points[0].x.toString() arg0[0].raw = points[0].x.toString()
arg0[1].value = { value: points[0].y, suffix: 'None' } arg0[1].value = points[0].y
arg0[1].raw = points[0].y.toString() arg0[1].raw = points[0].y.toString()
const arg1 = arg(kclCircle3PointArgs[1]) const arg1 = arg(kclCircle3PointArgs[1])
if (!arg1) return kclManager.ast if (!arg1) return kclManager.ast
arg1[0].value = { value: points[1].x, suffix: 'None' } arg1[0].value = points[1].x
arg1[0].raw = points[1].x.toString() arg1[0].raw = points[1].x.toString()
arg1[1].value = { value: points[1].y, suffix: 'None' } arg1[1].value = points[1].y
arg1[1].raw = points[1].y.toString() arg1[1].raw = points[1].y.toString()
const arg2 = arg(kclCircle3PointArgs[2]) const arg2 = arg(kclCircle3PointArgs[2])
if (!arg2) return kclManager.ast if (!arg2) return kclManager.ast
arg2[0].value = { value: points[2].x, suffix: 'None' } arg2[0].value = points[2].x
arg2[0].raw = points[2].x.toString() arg2[0].raw = points[2].x.toString()
arg2[1].value = { value: points[2].y, suffix: 'None' } arg2[1].value = points[2].y
arg2[1].raw = points[2].y.toString() arg2[1].raw = points[2].y.toString()
const astSnapshot = structuredClone(kclManager.ast) const astSnapshot = structuredClone(kclManager.ast)
@ -2052,8 +2051,8 @@ export class SceneEntities {
) )
if (!(sk instanceof Reason)) { if (!(sk instanceof Reason)) {
sketch = sk sketch = sk
} else if (maybeSketch && (maybeSketch.value as Solid)?.sketch) { } else if ((maybeSketch as Solid).sketch) {
sketch = (maybeSketch.value as Solid).sketch sketch = (maybeSketch as Solid).sketch
} }
if (!sketch) return if (!sketch) return
@ -2542,7 +2541,7 @@ export function sketchFromPathToNode({
const varDec = _varDec.node const varDec = _varDec.node
const result = programMemory.get(varDec?.id?.name || '') const result = programMemory.get(varDec?.id?.name || '')
if (result?.type === 'Solid') { if (result?.type === 'Solid') {
return result.value.sketch return result.sketch
} }
const sg = sketchFromKclValue(result, varDec?.id?.name) const sg = sketchFromKclValue(result, varDec?.id?.name)
if (err(sg)) { if (err(sg)) {

View File

@ -61,7 +61,6 @@ import { SegmentInputs } from 'lang/std/stdTypes'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { editorManager, sceneInfra } from 'lib/singletons' import { editorManager, sceneInfra } from 'lib/singletons'
import { Selections } from 'lib/selections' import { Selections } from 'lib/selections'
import { commandBarActor } from 'machines/commandBarMachine'
interface CreateSegmentArgs { interface CreateSegmentArgs {
input: SegmentInputs input: SegmentInputs
@ -848,7 +847,7 @@ function createLengthIndicator({
}) })
// Command Bar // Command Bar
commandBarActor.send({ editorManager.commandBarSend({
type: 'Find and select command', type: 'Find and select command',
data: { data: {
name: 'Constrain length', name: 'Constrain length',

View File

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

View File

@ -1,7 +1,6 @@
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import { editorManager, engineCommandManager, kclManager } from 'lib/singletons' import { editorManager, engineCommandManager, kclManager } from 'lib/singletons'
import { getNodeFromPath } from 'lang/queryAst' import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { trap } from 'lib/trap' import { trap } from 'lib/trap'
import { codeToIdSelections } from 'lib/selections' import { codeToIdSelections } from 'lib/selections'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,11 @@
import { Combobox } from '@headlessui/react' import { Combobox } from '@headlessui/react'
import Fuse from 'fuse.js' import Fuse from 'fuse.js'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { Command } from 'lib/commandTypes' import { Command } from 'lib/commandTypes'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { CustomIcon } from './CustomIcon' import { CustomIcon } from './CustomIcon'
import { getActorNextEvents } from 'lib/utils' import { getActorNextEvents } from 'lib/utils'
import { sortCommands } from 'lib/commandUtils' import { sortCommands } from 'lib/commandUtils'
import { commandBarActor } from 'machines/commandBarMachine'
function CommandComboBox({ function CommandComboBox({
options, options,
@ -14,6 +14,7 @@ function CommandComboBox({
options: Command[] options: Command[]
placeholder?: string placeholder?: string
}) { }) {
const { commandBarSend } = useCommandsContext()
const [query, setQuery] = useState('') const [query, setQuery] = useState('')
const [filteredOptions, setFilteredOptions] = useState<typeof options>() const [filteredOptions, setFilteredOptions] = useState<typeof options>()
@ -40,7 +41,7 @@ function CommandComboBox({
}, [query]) }, [query])
function handleSelection(command: Command) { function handleSelection(command: Command) {
commandBarActor.send({ type: 'Select command', data: { command } }) commandBarSend({ type: 'Select command', data: { command } })
} }
return ( return (
@ -51,7 +52,6 @@ function CommandComboBox({
className="w-5 h-5 bg-primary/10 dark:bg-primary text-primary dark:text-inherit" className="w-5 h-5 bg-primary/10 dark:bg-primary text-primary dark:text-inherit"
/> />
<Combobox.Input <Combobox.Input
data-testid="cmd-bar-search"
onChange={(event) => setQuery(event.target.value)} onChange={(event) => setQuery(event.target.value)}
className="w-full bg-transparent focus:outline-none selection:bg-primary/20 dark:selection:bg-primary/40 dark:focus:outline-none" className="w-full bg-transparent focus:outline-none selection:bg-primary/20 dark:selection:bg-primary/40 dark:focus:outline-none"
onKeyDown={(event) => { onKeyDown={(event) => {
@ -60,7 +60,7 @@ function CommandComboBox({
(event.key === 'Backspace' && !event.currentTarget.value) (event.key === 'Backspace' && !event.currentTarget.value)
) { ) {
event.preventDefault() event.preventDefault()
commandBarActor.send({ type: 'Close' }) commandBarSend({ type: 'Close' })
} }
}} }}
placeholder={ placeholder={
@ -75,7 +75,6 @@ function CommandComboBox({
autoFocus autoFocus
/> />
</div> </div>
{filteredOptions?.length ? (
<Combobox.Options <Combobox.Options
static static
className="overflow-y-auto max-h-96 cursor-pointer" className="overflow-y-auto max-h-96 cursor-pointer"
@ -86,7 +85,6 @@ function CommandComboBox({
value={option} value={option}
className="flex items-center gap-4 px-4 py-1.5 first:mt-2 last:mb-2 ui-active:bg-primary/10 dark:ui-active:bg-chalkboard-90 ui-disabled:!text-chalkboard-50" className="flex items-center gap-4 px-4 py-1.5 first:mt-2 last:mb-2 ui-active:bg-primary/10 dark:ui-active:bg-chalkboard-90 ui-disabled:!text-chalkboard-50"
disabled={optionIsDisabled(option)} disabled={optionIsDisabled(option)}
data-testid={`cmd-bar-option`}
> >
{'icon' in option && option.icon && ( {'icon' in option && option.icon && (
<CustomIcon name={option.icon} className="w-5 h-5" /> <CustomIcon name={option.icon} className="w-5 h-5" />
@ -104,11 +102,6 @@ function CommandComboBox({
</Combobox.Option> </Combobox.Option>
))} ))}
</Combobox.Options> </Combobox.Options>
) : (
<p className="px-4 pt-2 text-chalkboard-60 dark:text-chalkboard-50">
No results found
</p>
)}
</Combobox> </Combobox>
) )
} }

View File

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

View File

@ -12,6 +12,7 @@ import {
StateFrom, StateFrom,
fromPromise, fromPromise,
} from 'xstate' } from 'xstate'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { fileMachine } from 'machines/fileMachine' import { fileMachine } from 'machines/fileMachine'
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
import { import {
@ -29,7 +30,6 @@ import {
} from 'lib/getKclSamplesManifest' } from 'lib/getKclSamplesManifest'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { markOnce } from 'lib/performance' import { markOnce } from 'lib/performance'
import { commandBarActor } from 'machines/commandBarMachine'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -47,9 +47,9 @@ export const FileMachineProvider = ({
children: React.ReactNode children: React.ReactNode
}) => { }) => {
const navigate = useNavigate() const navigate = useNavigate()
const { settings, auth } = useSettingsAuthContext() const { commandBarSend } = useCommandsContext()
const projectData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData const { settings } = useSettingsAuthContext()
const { project, file } = projectData const { project, file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const [kclSamples, setKclSamples] = React.useState<KclSamplesManifestItem[]>( const [kclSamples, setKclSamples] = React.useState<KclSamplesManifestItem[]>(
[] []
) )
@ -90,7 +90,7 @@ export const FileMachineProvider = ({
navigateToFile: ({ context, event }) => { navigateToFile: ({ context, event }) => {
if (event.type !== 'xstate.done.actor.create-and-open-file') return if (event.type !== 'xstate.done.actor.create-and-open-file') return
if (event.output && 'name' in event.output) { if (event.output && 'name' in event.output) {
commandBarActor.send({ type: 'Close' }) commandBarSend({ type: 'Close' })
navigate( navigate(
`..${PATHS.FILE}/${encodeURIComponent( `..${PATHS.FILE}/${encodeURIComponent(
context.selectedDirectory + context.selectedDirectory +
@ -296,14 +296,8 @@ export const FileMachineProvider = ({
const kclCommandMemo = useMemo( const kclCommandMemo = useMemo(
() => () =>
kclCommands({ kclCommands(
authToken: auth?.context?.token ?? '', async (data) => {
projectData,
settings: {
defaultUnit: settings?.context?.modeling.defaultUnit.current ?? 'mm',
},
specialPropsForSampleCommand: {
onSubmit: async (data) => {
if (data.method === 'overwrite') { if (data.method === 'overwrite') {
codeManager.updateCodeStateEditor(data.code) codeManager.updateCodeStateEditor(data.code)
await kclManager.executeCode(true) await kclManager.executeCode(true)
@ -331,30 +325,26 @@ export const FileMachineProvider = ({
}) })
} }
}, },
providedOptions: kclSamples.map((sample) => ({ kclSamples.map((sample) => ({
value: sample.pathFromProjectDirectoryToFirstFile, value: sample.pathFromProjectDirectoryToFirstFile,
name: sample.title, name: sample.title,
})), }))
}, ).filter(
}).filter(
(command) => kclSamples.length || command.name !== 'open-kcl-example' (command) => kclSamples.length || command.name !== 'open-kcl-example'
), ),
[codeManager, kclManager, send, kclSamples] [codeManager, kclManager, send, kclSamples]
) )
useEffect(() => { useEffect(() => {
commandBarActor.send({ commandBarSend({ type: 'Add commands', data: { commands: kclCommandMemo } })
type: 'Add commands',
data: { commands: kclCommandMemo },
})
return () => { return () => {
commandBarActor.send({ commandBarSend({
type: 'Remove commands', type: 'Remove commands',
data: { commands: kclCommandMemo }, data: { commands: kclCommandMemo },
}) })
} }
}, [commandBarActor.send, kclCommandMemo]) }, [commandBarSend, kclCommandMemo])
return ( return (
<FileContext.Provider <FileContext.Provider

View File

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

View File

@ -1,4 +1,4 @@
import { useMachine, useSelector } from '@xstate/react' import { useMachine } from '@xstate/react'
import React, { import React, {
createContext, createContext,
useEffect, useEffect,
@ -11,7 +11,6 @@ import {
AnyStateMachine, AnyStateMachine,
ContextFrom, ContextFrom,
Prop, Prop,
SnapshotFrom,
StateFrom, StateFrom,
assign, assign,
fromPromise, fromPromise,
@ -68,14 +67,18 @@ import {
startSketchOnDefault, startSketchOnDefault,
} from 'lang/modifyAst' } from 'lang/modifyAst'
import { PathToNode, Program, parse, recast, resultIsOk } from 'lang/wasm' import { PathToNode, Program, parse, recast, resultIsOk } from 'lang/wasm'
import { artifactIsPlaneWithPaths, isSingleCursorInPipe } from 'lang/queryAst' import {
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils' artifactIsPlaneWithPaths,
getNodePathFromSourceRange,
isSingleCursorInPipe,
} from 'lang/queryAst'
import { exportFromEngine } from 'lib/exportFromEngine' import { exportFromEngine } from 'lib/exportFromEngine'
import { Models } from '@kittycad/lib/dist/types/src' import { Models } from '@kittycad/lib/dist/types/src'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom' import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls' import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
import { err, reportRejection, trap } from 'lib/trap' import { err, reportRejection, trap } from 'lib/trap'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { import {
ExportIntent, ExportIntent,
EngineConnectionStateType, EngineConnectionStateType,
@ -88,7 +91,6 @@ import { IndexLoaderData } from 'lib/types'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
import { promptToEditFlow } from 'lib/promptToEdit' import { promptToEditFlow } from 'lib/promptToEdit'
import { kclEditorActor } from 'machines/kclEditorMachine' import { kclEditorActor } from 'machines/kclEditorMachine'
import { commandBarActor } from 'machines/commandBarMachine'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -100,10 +102,6 @@ export const ModelingMachineContext = createContext(
{} as MachineContext<typeof modelingMachine> {} as MachineContext<typeof modelingMachine>
) )
const commandBarIsClosedSelector = (
state: SnapshotFrom<typeof commandBarActor>
) => state.matches('Closed')
export const ModelingMachineProvider = ({ export const ModelingMachineProvider = ({
children, children,
}: { }: {
@ -134,10 +132,8 @@ export const ModelingMachineProvider = ({
let [searchParams] = useSearchParams() let [searchParams] = useSearchParams()
const pool = searchParams.get('pool') const pool = searchParams.get('pool')
const isCommandBarClosed = useSelector( const { commandBarState, commandBarSend } = useCommandsContext()
commandBarActor,
commandBarIsClosedSelector
)
// Settings machine setup // Settings machine setup
// const retrievedSettings = useRef( // const retrievedSettings = useRef(
// localStorage?.getItem(MODELING_PERSIST_KEY) || '{}' // localStorage?.getItem(MODELING_PERSIST_KEY) || '{}'
@ -392,16 +388,7 @@ export const ModelingMachineProvider = ({
} }
if (setSelections.selectionType === 'completeSelection') { if (setSelections.selectionType === 'completeSelection') {
const codeMirrorSelection = editorManager.createEditorSelection( editorManager.selectRange(setSelections.selection)
setSelections.selection
)
kclEditorActor.send({
type: 'setLastSelectionEvent',
data: {
codeMirrorSelection,
scrollIntoView: false,
},
})
if (!sketchDetails) if (!sketchDetails)
return { return {
selectionRanges: setSelections.selection, selectionRanges: setSelections.selection,
@ -542,6 +529,7 @@ export const ModelingMachineProvider = ({
trimmedPrompt, trimmedPrompt,
fileMachineSend, fileMachineSend,
navigate, navigate,
commandBarSend,
context, context,
token, token,
settings: { settings: {
@ -555,7 +543,7 @@ export const ModelingMachineProvider = ({
'has valid selection for deletion': ({ 'has valid selection for deletion': ({
context: { selectionRanges }, context: { selectionRanges },
}) => { }) => {
if (!isCommandBarClosed) return false if (!commandBarState.matches('Closed')) return false
if (selectionRanges.graphSelections.length <= 0) return false if (selectionRanges.graphSelections.length <= 0) return false
return true return true
}, },

View File

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

View File

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

View File

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

View File

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

View File

@ -15,12 +15,12 @@ import { ModelingPane } from './ModelingPane'
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import { CustomIconName } from 'components/CustomIcon' import { CustomIconName } from 'components/CustomIcon'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { IconDefinition } from '@fortawesome/free-solid-svg-icons' import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
import { useKclContext } from 'lang/KclProvider' import { useKclContext } from 'lang/KclProvider'
import { MachineManagerContext } from 'components/MachineManagerProvider' import { MachineManagerContext } from 'components/MachineManagerProvider'
import { onboardingPaths } from 'routes/Onboarding/paths' import { onboardingPaths } from 'routes/Onboarding/paths'
import { SIDEBAR_BUTTON_SUFFIX } from 'lib/constants' import { SIDEBAR_BUTTON_SUFFIX } from 'lib/constants'
import { commandBarActor } from 'machines/commandBarMachine'
interface ModelingSidebarProps { interface ModelingSidebarProps {
paneOpacity: '' | 'opacity-20' | 'opacity-40' paneOpacity: '' | 'opacity-20' | 'opacity-40'
@ -37,6 +37,7 @@ function getPlatformString(): 'web' | 'desktop' {
export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) { export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
const machineManager = useContext(MachineManagerContext) const machineManager = useContext(MachineManagerContext)
const { commandBarSend } = useCommandsContext()
const kclContext = useKclContext() const kclContext = useKclContext()
const { settings } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
const onboardingStatus = settings.context.app.onboardingStatus const onboardingStatus = settings.context.app.onboardingStatus
@ -65,7 +66,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
icon: 'floppyDiskArrow', icon: 'floppyDiskArrow',
keybinding: 'Ctrl + Shift + E', keybinding: 'Ctrl + Shift + E',
action: () => action: () =>
commandBarActor.send({ commandBarSend({
type: 'Find and select command', type: 'Find and select command',
data: { name: 'Export', groupId: 'modeling' }, data: { name: 'Export', groupId: 'modeling' },
}), }),
@ -78,7 +79,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
keybinding: 'Ctrl + Shift + M', keybinding: 'Ctrl + Shift + M',
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises
action: async () => { action: async () => {
commandBarActor.send({ commandBarSend({
type: 'Find and select command', type: 'Find and select command',
data: { name: 'Make', groupId: 'modeling' }, data: { name: 'Make', groupId: 'modeling' },
}) })
@ -297,7 +298,7 @@ function ModelingPaneButton({
}) })
return ( return (
<div id={paneConfig.id + '-button-holder'} className="relative"> <div id={paneConfig.id + '-button-holder'}>
<button <button
className="group pointer-events-auto flex items-center justify-center border-transparent dark:border-transparent disabled:!border-transparent p-0 m-0 rounded-sm !outline-0 focus-visible:border-primary" className="group pointer-events-auto flex items-center justify-center border-transparent dark:border-transparent disabled:!border-transparent p-0 m-0 rounded-sm !outline-0 focus-visible:border-primary"
onClick={onClick} onClick={onClick}
@ -339,7 +340,7 @@ function ModelingPaneButton({
<p <p
id={`${paneConfig.id}-badge`} id={`${paneConfig.id}-badge`}
className={ className={
'absolute m-0 p-0 bottom-4 left-4 w-3 h-3 flex items-center justify-center text-[10px] font-semibold text-white bg-primary hue-rotate-90 rounded-full border border-chalkboard-10 dark:border-chalkboard-80 z-50 hover:cursor-pointer hover:scale-[2] transition-transform duration-200' 'absolute m-0 p-0 top-1 right-0 w-3 h-3 flex items-center justify-center text-[10px] font-semibold text-white bg-primary hue-rotate-90 rounded-full border border-chalkboard-10 dark:border-chalkboard-80 z-50 hover:cursor-pointer hover:scale-[2] transition-transform duration-200'
} }
onClick={showBadge.onClick} onClick={showBadge.onClick}
title={`Click to view ${showBadge.value} notification${ title={`Click to view ${showBadge.value} notification${

View File

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

View File

@ -1,68 +0,0 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { BrowserRouter, Route, Routes } from 'react-router-dom'
import { OpenInDesktopAppHandler } from './OpenInDesktopAppHandler'
/**
* The behavior under test requires a router,
* so we wrap the component in a minimal router setup.
*/
function TestingMinimalRouterWrapper({
children,
location,
}: {
location?: string
children: React.ReactNode
}) {
return (
<Routes location={location}>
<Route
path="/"
element={<OpenInDesktopAppHandler>{children}</OpenInDesktopAppHandler>}
/>
</Routes>
)
}
describe('OpenInDesktopAppHandler tests', () => {
test(`does not render the modal if no query param is present`, () => {
render(
<BrowserRouter>
<TestingMinimalRouterWrapper>
<p>Dummy app contents</p>
</TestingMinimalRouterWrapper>
</BrowserRouter>
)
const dummyAppContents = screen.getByText('Dummy app contents')
const modalContents = screen.queryByText('Open in desktop app')
expect(dummyAppContents).toBeInTheDocument()
expect(modalContents).not.toBeInTheDocument()
})
test(`renders the modal if the query param is present`, () => {
render(
<BrowserRouter>
<TestingMinimalRouterWrapper location="/?ask-open-desktop">
<p>Dummy app contents</p>
</TestingMinimalRouterWrapper>
</BrowserRouter>
)
let dummyAppContents = screen.queryByText('Dummy app contents')
let modalButton = screen.queryByText('Continue to web app')
// Starts as disconnected
expect(dummyAppContents).not.toBeInTheDocument()
expect(modalButton).not.toBeFalsy()
expect(modalButton).toBeInTheDocument()
fireEvent.click(modalButton as Element)
// I don't like that you have to re-query the screen here
dummyAppContents = screen.queryByText('Dummy app contents')
modalButton = screen.queryByText('Continue to web app')
expect(dummyAppContents).toBeInTheDocument()
expect(modalButton).not.toBeInTheDocument()
})
})

View File

@ -1,125 +0,0 @@
import { getSystemTheme, Themes } from 'lib/theme'
import { ZOO_STUDIO_PROTOCOL } from 'lib/constants'
import { isDesktop } from 'lib/isDesktop'
import { useSearchParams } from 'react-router-dom'
import { ASK_TO_OPEN_QUERY_PARAM } from 'lib/constants'
import { VITE_KC_SITE_BASE_URL } from 'env'
import { ActionButton } from './ActionButton'
import { Transition } from '@headlessui/react'
/**
* This component is a handler that checks if a certain query parameter
* is present, and if so, it will show a modal asking the user if they
* want to open the current page in the desktop app.
*/
export const OpenInDesktopAppHandler = (props: React.PropsWithChildren) => {
const theme = getSystemTheme()
const buttonClasses =
'bg-transparent flex-0 hover:bg-primary/10 dark:hover:bg-primary/10'
const pathLogomarkSvg = `${isDesktop() ? '.' : ''}/zma-logomark${
theme === Themes.Light ? '-dark' : ''
}.svg`
const [searchParams, setSearchParams] = useSearchParams()
// We also ignore this param on desktop, as it is redundant
const hasAskToOpenParam =
!isDesktop() && searchParams.has(ASK_TO_OPEN_QUERY_PARAM)
/**
* This function removes the query param to ask to open in desktop app
* and then navigates to the same route but with our custom protocol
* `zoo-studio:` instead of `https://${BASE_URL}`, to trigger the user's
* desktop app to open.
*/
function onOpenInDesktopApp() {
const newSearchParams = new URLSearchParams(globalThis.location.search)
newSearchParams.delete(ASK_TO_OPEN_QUERY_PARAM)
const newURL = `${ZOO_STUDIO_PROTOCOL}${globalThis.location.pathname.replace(
'/',
''
)}${searchParams.size > 0 ? `?${newSearchParams.toString()}` : ''}`
globalThis.location.href = newURL
}
/**
* Just remove the query param to ask to open in desktop app
* and continue to the web app.
*/
function continueToWebApp() {
searchParams.delete(ASK_TO_OPEN_QUERY_PARAM)
setSearchParams(searchParams)
}
return hasAskToOpenParam ? (
<Transition
appear
show={true}
as="div"
className={
theme +
` fixed inset-0 grid p-4 place-content-center ${
theme === Themes.Dark ? '!bg-chalkboard-110 text-chalkboard-20' : ''
}`
}
>
<Transition.Child
as="div"
className={`max-w-3xl py-6 px-10 flex flex-col items-center gap-8
mx-auto border rounded-lg shadow-lg dark:bg-chalkboard-100`}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
style={{ zIndex: 10 }}
>
<div>
<h1 className="text-2xl">
Launching{' '}
<img
src={pathLogomarkSvg}
className="w-48"
alt="Zoo Modeling App"
/>
</h1>
</div>
<p className="text-primary flex items-center gap-2">
Choose where to open this link...
</p>
<div className="flex flex-col md:flex-row items-start justify-between gap-4 xl:gap-8">
<div className="flex flex-col gap-2">
<ActionButton
Element="button"
className={buttonClasses + ' !text-base'}
onClick={onOpenInDesktopApp}
iconEnd={{ icon: 'arrowRight' }}
>
Open in desktop app
</ActionButton>
<ActionButton
Element="externalLink"
className={
buttonClasses +
' text-sm border-transparent justify-center dark:bg-transparent'
}
to={`${VITE_KC_SITE_BASE_URL}/modeling-app/download`}
iconEnd={{ icon: 'link', bgClassName: '!bg-transparent' }}
>
Download desktop app
</ActionButton>
</div>
<ActionButton
Element="button"
className={buttonClasses + ' -order-1 !text-base'}
onClick={continueToWebApp}
iconStart={{ icon: 'arrowLeft' }}
>
Continue to web app
</ActionButton>
</div>
</Transition.Child>
</Transition>
) : (
props.children
)
}

View File

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

View File

@ -7,19 +7,14 @@ import { Link, useLocation, useNavigate } from 'react-router-dom'
import { Fragment, useMemo, useContext } from 'react' import { Fragment, useMemo, useContext } from 'react'
import { Logo } from './Logo' import { Logo } from './Logo'
import { APP_NAME } from 'lib/constants' import { APP_NAME } from 'lib/constants'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { CustomIcon } from './CustomIcon' import { CustomIcon } from './CustomIcon'
import { useLspContext } from './LspProvider' import { useLspContext } from './LspProvider'
import { codeManager, engineCommandManager, kclManager } from 'lib/singletons' import { engineCommandManager, kclManager } from 'lib/singletons'
import { MachineManagerContext } from 'components/MachineManagerProvider' import { MachineManagerContext } from 'components/MachineManagerProvider'
import usePlatform from 'hooks/usePlatform' import usePlatform from 'hooks/usePlatform'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath' import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import Tooltip from './Tooltip' import Tooltip from './Tooltip'
import { SnapshotFrom } from 'xstate'
import { commandBarActor } from 'machines/commandBarMachine'
import { useSelector } from '@xstate/react'
import { copyFileShareLink } from 'lib/links'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { DEV } from 'env'
const ProjectSidebarMenu = ({ const ProjectSidebarMenu = ({
project, project,
@ -89,9 +84,6 @@ function AppLogoLink({
) )
} }
const commandsSelector = (state: SnapshotFrom<typeof commandBarActor>) =>
state.context.commands
function ProjectMenuPopover({ function ProjectMenuPopover({
project, project,
file, file,
@ -103,16 +95,17 @@ function ProjectMenuPopover({
const location = useLocation() const location = useLocation()
const navigate = useNavigate() const navigate = useNavigate()
const filePath = useAbsoluteFilePath() const filePath = useAbsoluteFilePath()
const { settings, auth } = useSettingsAuthContext()
const machineManager = useContext(MachineManagerContext) const machineManager = useContext(MachineManagerContext)
const commands = useSelector(commandBarActor, commandsSelector)
const { commandBarState, commandBarSend } = useCommandsContext()
const { onProjectClose } = useLspContext() const { onProjectClose } = useLspContext()
const exportCommandInfo = { name: 'Export', groupId: 'modeling' } const exportCommandInfo = { name: 'Export', groupId: 'modeling' }
const makeCommandInfo = { name: 'Make', groupId: 'modeling' } const makeCommandInfo = { name: 'Make', groupId: 'modeling' }
const findCommand = (obj: { name: string; groupId: string }) => const findCommand = (obj: { name: string; groupId: string }) =>
Boolean( Boolean(
commands.find((c) => c.name === obj.name && c.groupId === obj.groupId) commandBarState.context.commands.find(
(c) => c.name === obj.name && c.groupId === obj.groupId
)
) )
const machineCount = machineManager.machines.length const machineCount = machineManager.machines.length
@ -157,11 +150,12 @@ function ProjectMenuPopover({
), ),
disabled: !findCommand(exportCommandInfo), disabled: !findCommand(exportCommandInfo),
onClick: () => onClick: () =>
commandBarActor.send({ commandBarSend({
type: 'Find and select command', type: 'Find and select command',
data: exportCommandInfo, data: exportCommandInfo,
}), }),
}, },
'break',
{ {
id: 'make', id: 'make',
Element: 'button', Element: 'button',
@ -181,26 +175,12 @@ function ProjectMenuPopover({
), ),
disabled: !findCommand(makeCommandInfo) || machineCount === 0, disabled: !findCommand(makeCommandInfo) || machineCount === 0,
onClick: () => { onClick: () => {
commandBarActor.send({ commandBarSend({
type: 'Find and select command', type: 'Find and select command',
data: makeCommandInfo, data: makeCommandInfo,
}) })
}, },
}, },
{
id: 'share-link',
Element: 'button',
children: 'Share link to file',
disabled: !DEV,
onClick: async () => {
await copyFileShareLink({
token: auth?.context.token || '',
code: codeManager.code,
name: project?.name || '',
units: settings.context.modeling.defaultUnit.current,
})
},
},
'break', 'break',
{ {
id: 'go-home', id: 'go-home',
@ -220,7 +200,7 @@ function ProjectMenuPopover({
[ [
platform, platform,
findCommand, findCommand,
commandBarActor.send, commandBarSend,
engineCommandManager, engineCommandManager,
onProjectClose, onProjectClose,
isDesktop, isDesktop,

View File

@ -1,12 +1,13 @@
import { useMachine } from '@xstate/react' import { useMachine } from '@xstate/react'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher' import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
import { useProjectsLoader } from 'hooks/useProjectsLoader' import { useProjectsLoader } from 'hooks/useProjectsLoader'
import { projectsMachine } from 'machines/projectsMachine' import { projectsMachine } from 'machines/projectsMachine'
import { createContext, useCallback, useEffect, useState } from 'react' import { createContext, useEffect, useState } from 'react'
import { Actor, AnyStateMachine, fromPromise, Prop, StateFrom } from 'xstate' import { Actor, AnyStateMachine, fromPromise, Prop, StateFrom } from 'xstate'
import { useLspContext } from './LspProvider' import { useLspContext } from './LspProvider'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import { PATHS } from 'lib/paths' import { PATHS } from 'lib/paths'
import { import {
createNewProjectDirectory, createNewProjectDirectory,
@ -17,29 +18,11 @@ import {
getNextProjectIndex, getNextProjectIndex,
interpolateProjectNameWithIndex, interpolateProjectNameWithIndex,
doesProjectNameNeedInterpolated, doesProjectNameNeedInterpolated,
getUniqueProjectName,
getNextFileName,
} from 'lib/desktopFS' } from 'lib/desktopFS'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import useStateMachineCommands from 'hooks/useStateMachineCommands' import useStateMachineCommands from 'hooks/useStateMachineCommands'
import { projectsCommandBarConfig } from 'lib/commandBarConfigs/projectsCommandConfig' import { projectsCommandBarConfig } from 'lib/commandBarConfigs/projectsCommandConfig'
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
import { commandBarActor } from 'machines/commandBarMachine'
import {
CREATE_FILE_URL_PARAM,
FILE_EXT,
PROJECT_ENTRYPOINT,
} from 'lib/constants'
import { DeepPartial } from 'lib/types'
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import { codeManager } from 'lib/singletons'
import {
loadAndValidateSettings,
projectConfigurationToSettingsPayload,
saveSettings,
setSettingsAtLevel,
} from 'lib/settings/settingsUtils'
import { Project } from 'lib/project'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state?: StateFrom<T> state?: StateFrom<T>
@ -69,110 +52,12 @@ export const ProjectsContextProvider = ({
) )
} }
/**
* We need some of the functionality of the ProjectsContextProvider in the web version
* but we can't perform file system operations in the browser,
* so most of the behavior of this machine is stubbed out.
*/
const ProjectsContextWeb = ({ children }: { children: React.ReactNode }) => { const ProjectsContextWeb = ({ children }: { children: React.ReactNode }) => {
const [searchParams, setSearchParams] = useSearchParams()
const clearImportSearchParams = useCallback(() => {
// Clear the search parameters related to the "Import file from URL" command
// or we'll never be able cancel or submit it.
searchParams.delete(CREATE_FILE_URL_PARAM)
searchParams.delete('code')
searchParams.delete('name')
searchParams.delete('units')
setSearchParams(searchParams)
}, [searchParams, setSearchParams])
const {
settings: { context: settings, send: settingsSend },
} = useSettingsAuthContext()
const [state, send, actor] = useMachine(
projectsMachine.provide({
actions: {
navigateToProject: () => {},
navigateToProjectIfNeeded: () => {},
navigateToFile: () => {},
toastSuccess: ({ event }) =>
toast.success(
('data' in event && typeof event.data === 'string' && event.data) ||
('output' in event &&
'message' in event.output &&
typeof event.output.message === 'string' &&
event.output.message) ||
''
),
toastError: ({ event }) =>
toast.error(
('data' in event && typeof event.data === 'string' && event.data) ||
('output' in event &&
typeof event.output === 'string' &&
event.output) ||
''
),
},
actors: {
readProjects: fromPromise(async () => [] as Project[]),
createProject: fromPromise(async () => ({
message: 'not implemented on web',
})),
renameProject: fromPromise(async () => ({
message: 'not implemented on web',
oldName: '',
newName: '',
})),
deleteProject: fromPromise(async () => ({
message: 'not implemented on web',
name: '',
})),
createFile: fromPromise(async ({ input }) => {
// Browser version doesn't navigate, just overwrites the current file
clearImportSearchParams()
codeManager.updateCodeStateEditor(input.code || '')
await codeManager.writeToFile()
settingsSend({
type: 'set.modeling.defaultUnit',
data: {
level: 'project',
value: input.units,
},
})
return {
message: 'File and units overwritten successfully',
fileName: input.name,
projectName: '',
}
}),
},
}),
{
input: {
projects: [],
defaultProjectName: settings.projects.defaultProjectName.current,
defaultDirectory: settings.app.projectDirectory.current,
},
}
)
// register all project-related command palette commands
useStateMachineCommands({
machineId: 'projects',
send,
state,
commandBarConfig: projectsCommandBarConfig,
actor,
onCancel: clearImportSearchParams,
})
return ( return (
<ProjectsMachineContext.Provider <ProjectsMachineContext.Provider
value={{ value={{
state, state: undefined,
send, send: () => {},
}} }}
> >
{children} {children}
@ -187,21 +72,19 @@ const ProjectsContextDesktop = ({
}) => { }) => {
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const [searchParams, setSearchParams] = useSearchParams() const { commandBarSend } = useCommandsContext()
const clearImportSearchParams = useCallback(() => {
// Clear the search parameters related to the "Import file from URL" command
// or we'll never be able cancel or submit it.
searchParams.delete(CREATE_FILE_URL_PARAM)
searchParams.delete('code')
searchParams.delete('name')
searchParams.delete('units')
setSearchParams(searchParams)
}, [searchParams, setSearchParams])
const { onProjectOpen } = useLspContext() const { onProjectOpen } = useLspContext()
const { const {
settings: { context: settings }, settings: { context: settings },
} = useSettingsAuthContext() } = useSettingsAuthContext()
useEffect(() => {
console.log(
'project directory changed',
settings.app.projectDirectory.current
)
}, [settings.app.projectDirectory.current])
const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0) const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0)
const { projectPaths, projectsDir } = useProjectsLoader([ const { projectPaths, projectsDir } = useProjectsLoader([
projectsLoaderTrigger, projectsLoaderTrigger,
@ -242,7 +125,7 @@ const ProjectsContextDesktop = ({
}, },
null null
) )
commandBarActor.send({ type: 'Close' }) commandBarSend({ type: 'Close' })
const newPathName = `${PATHS.FILE}/${encodeURIComponent( const newPathName = `${PATHS.FILE}/${encodeURIComponent(
projectPath projectPath
)}` )}`
@ -285,31 +168,6 @@ const ProjectsContextDesktop = ({
} }
} }
}, },
navigateToFile: ({ context, event }) => {
if (event.type !== 'xstate.done.actor.create-file') return
// For now, the browser version of create-file doesn't need to navigate
// since it just overwrites the current file.
if (!isDesktop()) return
let projectPath = window.electron.join(
context.defaultDirectory,
event.output.projectName
)
let filePath = window.electron.join(
projectPath,
event.output.fileName
)
onProjectOpen(
{
name: event.output.projectName,
path: projectPath,
},
null
)
const pathToNavigateTo = `${PATHS.FILE}/${encodeURIComponent(
filePath
)}`
navigate(pathToNavigateTo)
},
toastSuccess: ({ event }) => toastSuccess: ({ event }) =>
toast.success( toast.success(
('data' in event && typeof event.data === 'string' && event.data) || ('data' in event && typeof event.data === 'string' && event.data) ||
@ -337,12 +195,16 @@ const ProjectsContextDesktop = ({
: settings.projects.defaultProjectName.current : settings.projects.defaultProjectName.current
).trim() ).trim()
const uniqueName = getUniqueProjectName(name, input.projects) if (doesProjectNameNeedInterpolated(name)) {
await createNewProjectDirectory(uniqueName) const nextIndex = getNextProjectIndex(name, input.projects)
name = interpolateProjectNameWithIndex(name, nextIndex)
}
await createNewProjectDirectory(name)
return { return {
message: `Successfully created "${uniqueName}"`, message: `Successfully created "${name}"`,
name: uniqueName, name,
} }
}), }),
renameProject: fromPromise(async ({ input }) => { renameProject: fromPromise(async ({ input }) => {
@ -359,6 +221,8 @@ const ProjectsContextDesktop = ({
name = interpolateProjectNameWithIndex(name, nextIndex) name = interpolateProjectNameWithIndex(name, nextIndex)
} }
console.log('from Project')
await renameProjectDirectory( await renameProjectDirectory(
window.electron.path.join(defaultDirectory, oldName), window.electron.path.join(defaultDirectory, oldName),
name name
@ -381,83 +245,14 @@ const ProjectsContextDesktop = ({
name: input.name, name: input.name,
} }
}), }),
createFile: fromPromise(async ({ input }) => {
let projectName =
(input.method === 'newProject' ? input.name : input.projectName) ||
settings.projects.defaultProjectName.current
let fileName =
input.method === 'newProject'
? PROJECT_ENTRYPOINT
: input.name.endsWith(FILE_EXT)
? input.name
: input.name + FILE_EXT
let message = 'File created successfully'
const unitsConfiguration: DeepPartial<Configuration> = {
settings: {
project: {
directory: settings.app.projectDirectory.current,
}, },
modeling: { guards: {
base_unit: input.units, 'Has at least 1 project': ({ event }) => {
if (event.type !== 'xstate.done.actor.read-projects') return false
console.log(`from has at least 1 project: ${event.output.length}`)
return event.output.length ? event.output.length >= 1 : false
}, },
}, },
}
const needsInterpolated = doesProjectNameNeedInterpolated(projectName)
if (needsInterpolated) {
const nextIndex = getNextProjectIndex(projectName, input.projects)
projectName = interpolateProjectNameWithIndex(
projectName,
nextIndex
)
}
// Create the project around the file if newProject
if (input.method === 'newProject') {
await createNewProjectDirectory(
projectName,
input.code,
unitsConfiguration
)
message = `Project "${projectName}" created successfully with link contents`
} else {
let projectPath = window.electron.join(
settings.app.projectDirectory.current,
projectName
)
message = `File "${fileName}" created successfully`
const existingConfiguration = await loadAndValidateSettings(
projectPath
)
const settingsToSave = setSettingsAtLevel(
existingConfiguration.settings,
'project',
projectConfigurationToSettingsPayload(unitsConfiguration)
)
await saveSettings(settingsToSave, projectPath)
}
// Create the file
let baseDir = window.electron.join(
settings.app.projectDirectory.current,
projectName
)
const { name, path } = getNextFileName({
entryName: fileName,
baseDir,
})
fileName = name
await window.electron.writeFile(path, input.code || '')
return {
message,
fileName,
projectName,
}
}),
},
}), }),
{ {
input: { input: {
@ -479,7 +274,6 @@ const ProjectsContextDesktop = ({
state, state,
commandBarConfig: projectsCommandBarConfig, commandBarConfig: projectsCommandBarConfig,
actor, actor,
onCancel: clearImportSearchParams,
}) })
return ( return (

View File

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

View File

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

View File

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

View File

@ -1,8 +1,10 @@
import { toolTips } from 'lang/langHelpers' import { toolTips } from 'lang/langHelpers'
import { Selections } from 'lib/selections' import { Selections } from 'lib/selections'
import { Program, Expr, VariableDeclarator } from '../../lang/wasm' import { Program, Expr, VariableDeclarator } from '../../lang/wasm'
import { getNodeFromPath } from '../../lang/queryAst' import {
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils' getNodePathFromSourceRange,
getNodeFromPath,
} from '../../lang/queryAst'
import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints' import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints'
import { import {
transformSecondarySketchLinesTagFirst, transformSecondarySketchLinesTagFirst,

View File

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

View File

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

View File

@ -0,0 +1,10 @@
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,65 +0,0 @@
import { base64ToString } from 'lib/base64'
import { CREATE_FILE_URL_PARAM, DEFAULT_FILE_NAME } from 'lib/constants'
import { useEffect } from 'react'
import { useSearchParams } from 'react-router-dom'
import { useSettingsAuthContext } from './useSettingsAuthContext'
import { isDesktop } from 'lib/isDesktop'
import { FileLinkParams } from 'lib/links'
import { ProjectsCommandSchema } from 'lib/commandBarConfigs/projectsCommandConfig'
import { baseUnitsUnion } from 'lib/settings/settingsTypes'
// For initializing the command arguments, we actually want `method` to be undefined
// so that we don't skip it in the command palette.
export type CreateFileSchemaMethodOptional = Omit<
ProjectsCommandSchema['Import file from URL'],
'method'
> & {
method?: 'newProject' | 'existingProject'
}
/**
* companion to createFileLink. This hook runs an effect on mount that
* checks the URL for the CREATE_FILE_URL_PARAM and triggers the "Create file"
* command if it is present, loading the command's default values from the other
* URL parameters.
*/
export function useCreateFileLinkQuery(
callback: (args: CreateFileSchemaMethodOptional) => void
) {
const [searchParams] = useSearchParams()
const { settings } = useSettingsAuthContext()
useEffect(() => {
const createFileParam = searchParams.has(CREATE_FILE_URL_PARAM)
if (createFileParam) {
const params: FileLinkParams = {
code: base64ToString(
decodeURIComponent(searchParams.get('code') ?? '')
),
name: searchParams.get('name') ?? DEFAULT_FILE_NAME,
units:
(baseUnitsUnion.find((unit) => searchParams.get('units') === unit) ||
settings.context.modeling.defaultUnit.default) ??
settings.context.modeling.defaultUnit.current,
}
const argDefaultValues: CreateFileSchemaMethodOptional = {
name: params.name
? isDesktop()
? params.name.replace('.kcl', '')
: params.name
: isDesktop()
? settings.context.projects.defaultProjectName.current
: DEFAULT_FILE_NAME,
code: params.code || '',
units: params.units,
method: isDesktop() ? undefined : 'existingProject',
}
callback(argDefaultValues)
}
}, [searchParams])
}

View File

@ -17,8 +17,7 @@ import {
} from 'lang/std/artifactGraph' } from 'lang/std/artifactGraph'
import { err, reportRejection } from 'lib/trap' import { err, reportRejection } from 'lib/trap'
import { DefaultPlaneStr, getFaceDetails } from 'clientSideScene/sceneEntities' import { DefaultPlaneStr, getFaceDetails } from 'clientSideScene/sceneEntities'
import { getNodeFromPath } from 'lang/queryAst' import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { CallExpression, defaultSourceRange } from 'lang/wasm' import { CallExpression, defaultSourceRange } from 'lang/wasm'
import { EdgeCutInfo, ExtrudeFacePlane } from 'machines/modelingMachine' import { EdgeCutInfo, ExtrudeFacePlane } from 'machines/modelingMachine'

View File

@ -14,7 +14,7 @@ export const useProjectsLoader = (deps?: [number]) => {
useEffect(() => { useEffect(() => {
// Useless on web, until we get fake filesystems over there. // Useless on web, until we get fake filesystems over there.
if (!isDesktop()) return if (!isDesktop) return
if (deps && deps[0] === lastTs) return if (deps && deps[0] === lastTs) return

View File

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

View File

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

View File

@ -54,9 +54,6 @@ const mySketch001 = startSketchOn('XY')
}, },
], ],
id: expect.any(String), id: expect.any(String),
units: {
type: 'Mm',
},
__meta: [{ sourceRange: [46, 71, 0] }], __meta: [{ sourceRange: [46, 71, 0] }],
}, },
}) })
@ -74,8 +71,6 @@ const mySketch001 = startSketchOn('XY')
// @ts-ignore // @ts-ignore
const sketch001 = execState.memory.get('mySketch001') const sketch001 = execState.memory.get('mySketch001')
expect(sketch001).toEqual({ expect(sketch001).toEqual({
type: 'Solid',
value: {
type: 'Solid', type: 'Solid',
id: expect.any(String), id: expect.any(String),
value: [ value: [
@ -96,9 +91,6 @@ const mySketch001 = startSketchOn('XY')
], ],
sketch: { sketch: {
id: expect.any(String), id: expect.any(String),
units: {
type: 'Mm',
},
__meta: expect.any(Array), __meta: expect.any(Array),
on: expect.any(Object), on: expect.any(Object),
start: expect.any(Object), start: expect.any(Object),
@ -129,11 +121,7 @@ const mySketch001 = startSketchOn('XY')
height: 2, height: 2,
startCapId: expect.any(String), startCapId: expect.any(String),
endCapId: expect.any(String), endCapId: expect.any(String),
units: {
type: 'Mm',
},
__meta: [{ sourceRange: [46, 71, 0] }], __meta: [{ sourceRange: [46, 71, 0] }],
},
}) })
}) })
test('sketch extrude and sketch on one of the faces', async () => { test('sketch extrude and sketch on one of the faces', async () => {
@ -165,8 +153,6 @@ const sk2 = startSketchOn('XY')
const geos = [programMemory.get('theExtrude'), programMemory.get('sk2')] const geos = [programMemory.get('theExtrude'), programMemory.get('sk2')]
expect(geos).toEqual([ expect(geos).toEqual([
{ {
type: 'Solid',
value: {
type: 'Solid', type: 'Solid',
id: expect.any(String), id: expect.any(String),
value: [ value: [
@ -203,9 +189,6 @@ const sk2 = startSketchOn('XY')
on: expect.any(Object), on: expect.any(Object),
start: expect.any(Object), start: expect.any(Object),
type: 'Sketch', type: 'Sketch',
units: {
type: 'Mm',
},
tags: { tags: {
p: { p: {
__meta: [ __meta: [
@ -259,15 +242,9 @@ const sk2 = startSketchOn('XY')
height: 2, height: 2,
startCapId: expect.any(String), startCapId: expect.any(String),
endCapId: expect.any(String), endCapId: expect.any(String),
units: {
type: 'Mm',
},
__meta: [{ sourceRange: [38, 63, 0] }], __meta: [{ sourceRange: [38, 63, 0] }],
}, },
},
{ {
type: 'Solid',
value: {
type: 'Solid', type: 'Solid',
id: expect.any(String), id: expect.any(String),
value: [ value: [
@ -300,9 +277,6 @@ const sk2 = startSketchOn('XY')
], ],
sketch: { sketch: {
id: expect.any(String), id: expect.any(String),
units: {
type: 'Mm',
},
__meta: expect.any(Array), __meta: expect.any(Array),
on: expect.any(Object), on: expect.any(Object),
start: expect.any(Object), start: expect.any(Object),
@ -361,10 +335,6 @@ const sk2 = startSketchOn('XY')
startCapId: expect.any(String), startCapId: expect.any(String),
endCapId: expect.any(String), endCapId: expect.any(String),
__meta: [{ sourceRange: [342, 367, 0] }], __meta: [{ sourceRange: [342, 367, 0] }],
units: {
type: 'Mm',
},
},
}, },
]) ])
}) })

View File

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

View File

@ -1,5 +1,4 @@
import { getNodeFromPath } from './queryAst' import { getNodePathFromSourceRange, getNodeFromPath } from './queryAst'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { import {
Identifier, Identifier,
assertParse, assertParse,

View File

@ -25,8 +25,7 @@ import {
deleteFromSelection, deleteFromSelection,
} from './modifyAst' } from './modifyAst'
import { enginelessExecutor } from '../lib/testHelpers' import { enginelessExecutor } from '../lib/testHelpers'
import { findUsesOfTagInPipe } from './queryAst' import { findUsesOfTagInPipe, getNodePathFromSourceRange } from './queryAst'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { SimplifiedArgDetails } from './std/stdTypes' import { SimplifiedArgDetails } from './std/stdTypes'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
@ -40,7 +39,7 @@ describe('Testing createLiteral', () => {
it('should create a literal', () => { it('should create a literal', () => {
const result = createLiteral(5) const result = createLiteral(5)
expect(result.type).toBe('Literal') expect(result.type).toBe('Literal')
expect((result as any).value.value).toBe(5) expect(result.value).toBe(5)
}) })
}) })
describe('Testing createIdentifier', () => { describe('Testing createIdentifier', () => {
@ -57,7 +56,7 @@ describe('Testing createCallExpression', () => {
expect(result.callee.type).toBe('Identifier') expect(result.callee.type).toBe('Identifier')
expect(result.callee.name).toBe('myFunc') expect(result.callee.name).toBe('myFunc')
expect(result.arguments[0].type).toBe('Literal') expect(result.arguments[0].type).toBe('Literal')
expect((result.arguments[0] as any).value.value).toBe(5) expect((result.arguments[0] as any).value).toBe(5)
}) })
}) })
describe('Testing createObjectExpression', () => { describe('Testing createObjectExpression', () => {
@ -69,7 +68,7 @@ describe('Testing createObjectExpression', () => {
expect(result.properties[0].type).toBe('ObjectProperty') expect(result.properties[0].type).toBe('ObjectProperty')
expect(result.properties[0].key.name).toBe('myProp') expect(result.properties[0].key.name).toBe('myProp')
expect(result.properties[0].value.type).toBe('Literal') expect(result.properties[0].value.type).toBe('Literal')
expect((result.properties[0].value as any).value.value).toBe(5) expect((result.properties[0].value as any).value).toBe(5)
}) })
}) })
describe('Testing createArrayExpression', () => { describe('Testing createArrayExpression', () => {
@ -77,7 +76,7 @@ describe('Testing createArrayExpression', () => {
const result = createArrayExpression([createLiteral(5)]) const result = createArrayExpression([createLiteral(5)])
expect(result.type).toBe('ArrayExpression') expect(result.type).toBe('ArrayExpression')
expect(result.elements[0].type).toBe('Literal') expect(result.elements[0].type).toBe('Literal')
expect((result.elements[0] as any).value.value).toBe(5) expect((result.elements[0] as any).value).toBe(5)
}) })
}) })
describe('Testing createPipeSubstitution', () => { describe('Testing createPipeSubstitution', () => {
@ -94,7 +93,7 @@ describe('Testing createVariableDeclaration', () => {
expect(result.declaration.id.type).toBe('Identifier') expect(result.declaration.id.type).toBe('Identifier')
expect(result.declaration.id.name).toBe('myVar') expect(result.declaration.id.name).toBe('myVar')
expect(result.declaration.init.type).toBe('Literal') expect(result.declaration.init.type).toBe('Literal')
expect((result.declaration.init as any).value.value).toBe(5) expect((result.declaration.init as any).value).toBe(5)
}) })
}) })
describe('Testing createPipeExpression', () => { describe('Testing createPipeExpression', () => {
@ -102,7 +101,7 @@ describe('Testing createPipeExpression', () => {
const result = createPipeExpression([createLiteral(5)]) const result = createPipeExpression([createLiteral(5)])
expect(result.type).toBe('PipeExpression') expect(result.type).toBe('PipeExpression')
expect(result.body[0].type).toBe('Literal') expect(result.body[0].type).toBe('Literal')
expect((result.body[0] as any).value.value).toBe(5) expect((result.body[0] as any).value).toBe(5)
}) })
}) })

View File

@ -26,10 +26,10 @@ import {
findAllPreviousVariables, findAllPreviousVariables,
findAllPreviousVariablesPath, findAllPreviousVariablesPath,
getNodeFromPath, getNodeFromPath,
getNodePathFromSourceRange,
isNodeSafeToReplace, isNodeSafeToReplace,
traverse, traverse,
} from './queryAst' } from './queryAst'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { addTagForSketchOnFace, getConstraintInfo } from './std/sketch' import { addTagForSketchOnFace, getConstraintInfo } from './std/sketch'
import { import {
PathToNodeMap, PathToNodeMap,
@ -743,18 +743,14 @@ export function splitPathAtPipeExpression(pathToNode: PathToNode): {
return splitPathAtPipeExpression(pathToNode.slice(0, -1)) return splitPathAtPipeExpression(pathToNode.slice(0, -1))
} }
export function createLiteral(value: LiteralValue | number): Node<Literal> { export function createLiteral(value: LiteralValue): Node<Literal> {
const raw = `${value}`
if (typeof value === 'number') {
value = { value, suffix: 'None' }
}
return { return {
type: 'Literal', type: 'Literal',
start: 0, start: 0,
end: 0, end: 0,
moduleId: 0, moduleId: 0,
value, value,
raw, raw: `${value}`,
} }
} }

View File

@ -21,8 +21,7 @@ import {
ChamferParameters, ChamferParameters,
EdgeTreatmentParameters, EdgeTreatmentParameters,
} from './addEdgeTreatment' } from './addEdgeTreatment'
import { getNodeFromPath } from '../queryAst' import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { createLiteral } from 'lang/modifyAst' import { createLiteral } from 'lang/modifyAst'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { Selection, Selections } from 'lib/selections' import { Selection, Selections } from 'lib/selections'

View File

@ -20,10 +20,10 @@ import {
} from '../modifyAst' } from '../modifyAst'
import { import {
getNodeFromPath, getNodeFromPath,
getNodePathFromSourceRange,
hasSketchPipeBeenExtruded, hasSketchPipeBeenExtruded,
traverse, traverse,
} from '../queryAst' } from '../queryAst'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { import {
addTagForSketchOnFace, addTagForSketchOnFace,
getTagFromCallExpression, getTagFromCallExpression,

View File

@ -19,8 +19,7 @@ import {
findUniqueName, findUniqueName,
createVariableDeclaration, createVariableDeclaration,
} from 'lang/modifyAst' } from 'lang/modifyAst'
import { getNodeFromPath } from 'lang/queryAst' import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { import {
mutateAstWithTagForSketchSegment, mutateAstWithTagForSketchSegment,
getEdgeTagCall, getEdgeTagCall,

View File

@ -10,6 +10,7 @@ import {
findAllPreviousVariables, findAllPreviousVariables,
isNodeSafeToReplace, isNodeSafeToReplace,
isTypeInValue, isTypeInValue,
getNodePathFromSourceRange,
hasExtrudeSketch, hasExtrudeSketch,
findUsesOfTagInPipe, findUsesOfTagInPipe,
hasSketchPipeBeenExtruded, hasSketchPipeBeenExtruded,
@ -18,7 +19,6 @@ import {
getNodeFromPath, getNodeFromPath,
doesSceneHaveExtrudedSketch, doesSceneHaveExtrudedSketch,
} from './queryAst' } from './queryAst'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { enginelessExecutor } from '../lib/testHelpers' import { enginelessExecutor } from '../lib/testHelpers'
import { import {
createArrayExpression, createArrayExpression,
@ -660,7 +660,7 @@ myNestedVar = [
enter: (node, path) => { enter: (node, path) => {
if ( if (
node.type === 'Literal' && node.type === 'Literal' &&
String((node as any).value.value) === literalOfInterest String(node.value) === literalOfInterest
) { ) {
pathToNode = path pathToNode = path
} else if ( } else if (

View File

@ -22,7 +22,6 @@ import {
VariableDeclaration, VariableDeclaration,
VariableDeclarator, VariableDeclarator,
} from './wasm' } from './wasm'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { createIdentifier, splitPathAtLastIndex } from './modifyAst' import { createIdentifier, splitPathAtLastIndex } from './modifyAst'
import { getSketchSegmentFromSourceRange } from './std/sketchConstraints' import { getSketchSegmentFromSourceRange } from './std/sketchConstraints'
import { getAngle } from '../lib/utils' import { getAngle } from '../lib/utils'
@ -126,6 +125,311 @@ export function getNodeFromPathCurry(
} }
} }
function moreNodePathFromSourceRange(
node: Node<
| Expr
| ImportStatement
| ExpressionStatement
| VariableDeclaration
| ReturnStatement
>,
sourceRange: SourceRange,
previousPath: PathToNode = [['body', '']]
): PathToNode {
const [start, end] = sourceRange
let path: PathToNode = [...previousPath]
const _node = { ...node }
if (start < _node.start || end > _node.end) return path
const isInRange = _node.start <= start && _node.end >= end
if (
(_node.type === 'Identifier' ||
_node.type === 'Literal' ||
_node.type === 'TagDeclarator') &&
isInRange
) {
return path
}
if (_node.type === 'CallExpression' && isInRange) {
const { callee, arguments: args } = _node
if (
callee.type === 'Identifier' &&
callee.start <= start &&
callee.end >= end
) {
path.push(['callee', 'CallExpression'])
return path
}
if (args.length > 0) {
for (let argIndex = 0; argIndex < args.length; argIndex++) {
const arg = args[argIndex]
if (arg.start <= start && arg.end >= end) {
path.push(['arguments', 'CallExpression'])
path.push([argIndex, 'index'])
return moreNodePathFromSourceRange(arg, sourceRange, path)
}
}
}
return path
}
if (_node.type === 'CallExpressionKw' && isInRange) {
const { callee, arguments: args } = _node
if (
callee.type === 'Identifier' &&
callee.start <= start &&
callee.end >= end
) {
path.push(['callee', 'CallExpressionKw'])
return path
}
if (args.length > 0) {
for (let argIndex = 0; argIndex < args.length; argIndex++) {
const arg = args[argIndex].arg
if (arg.start <= start && arg.end >= end) {
path.push(['arguments', 'CallExpressionKw'])
path.push([argIndex, 'index'])
return moreNodePathFromSourceRange(arg, sourceRange, path)
}
}
}
return path
}
if (_node.type === 'BinaryExpression' && isInRange) {
const { left, right } = _node
if (left.start <= start && left.end >= end) {
path.push(['left', 'BinaryExpression'])
return moreNodePathFromSourceRange(left, sourceRange, path)
}
if (right.start <= start && right.end >= end) {
path.push(['right', 'BinaryExpression'])
return moreNodePathFromSourceRange(right, sourceRange, path)
}
return path
}
if (_node.type === 'PipeExpression' && isInRange) {
const { body } = _node
for (let i = 0; i < body.length; i++) {
const pipe = body[i]
if (pipe.start <= start && pipe.end >= end) {
path.push(['body', 'PipeExpression'])
path.push([i, 'index'])
return moreNodePathFromSourceRange(pipe, sourceRange, path)
}
}
return path
}
if (_node.type === 'ArrayExpression' && isInRange) {
const { elements } = _node
for (let elIndex = 0; elIndex < elements.length; elIndex++) {
const element = elements[elIndex]
if (element.start <= start && element.end >= end) {
path.push(['elements', 'ArrayExpression'])
path.push([elIndex, 'index'])
return moreNodePathFromSourceRange(element, sourceRange, path)
}
}
return path
}
if (_node.type === 'ObjectExpression' && isInRange) {
const { properties } = _node
for (let propIndex = 0; propIndex < properties.length; propIndex++) {
const property = properties[propIndex]
if (property.start <= start && property.end >= end) {
path.push(['properties', 'ObjectExpression'])
path.push([propIndex, 'index'])
if (property.key.start <= start && property.key.end >= end) {
path.push(['key', 'Property'])
return moreNodePathFromSourceRange(property.key, sourceRange, path)
}
if (property.value.start <= start && property.value.end >= end) {
path.push(['value', 'Property'])
return moreNodePathFromSourceRange(property.value, sourceRange, path)
}
}
}
return path
}
if (_node.type === 'ExpressionStatement' && isInRange) {
const { expression } = _node
path.push(['expression', 'ExpressionStatement'])
return moreNodePathFromSourceRange(expression, sourceRange, path)
}
if (_node.type === 'VariableDeclaration' && isInRange) {
const declaration = _node.declaration
if (declaration.start <= start && declaration.end >= end) {
path.push(['declaration', 'VariableDeclaration'])
const init = declaration.init
if (init.start <= start && init.end >= end) {
path.push(['init', ''])
return moreNodePathFromSourceRange(init, sourceRange, path)
}
}
}
if (_node.type === 'VariableDeclaration' && isInRange) {
const declaration = _node.declaration
if (declaration.start <= start && declaration.end >= end) {
const init = declaration.init
if (init.start <= start && init.end >= end) {
path.push(['declaration', 'VariableDeclaration'])
path.push(['init', ''])
return moreNodePathFromSourceRange(init, sourceRange, path)
}
}
return path
}
if (_node.type === 'UnaryExpression' && isInRange) {
const { argument } = _node
if (argument.start <= start && argument.end >= end) {
path.push(['argument', 'UnaryExpression'])
return moreNodePathFromSourceRange(argument, sourceRange, path)
}
return path
}
if (_node.type === 'FunctionExpression' && isInRange) {
for (let i = 0; i < _node.params.length; i++) {
const param = _node.params[i]
if (param.identifier.start <= start && param.identifier.end >= end) {
path.push(['params', 'FunctionExpression'])
path.push([i, 'index'])
return moreNodePathFromSourceRange(param.identifier, sourceRange, path)
}
}
if (_node.body.start <= start && _node.body.end >= end) {
path.push(['body', 'FunctionExpression'])
const fnBody = _node.body.body
for (let i = 0; i < fnBody.length; i++) {
const statement = fnBody[i]
if (statement.start <= start && statement.end >= end) {
path.push(['body', 'FunctionExpression'])
path.push([i, 'index'])
return moreNodePathFromSourceRange(statement, sourceRange, path)
}
}
}
return path
}
if (_node.type === 'ReturnStatement' && isInRange) {
const { argument } = _node
if (argument.start <= start && argument.end >= end) {
path.push(['argument', 'ReturnStatement'])
return moreNodePathFromSourceRange(argument, sourceRange, path)
}
return path
}
if (_node.type === 'MemberExpression' && isInRange) {
const { object, property } = _node
if (object.start <= start && object.end >= end) {
path.push(['object', 'MemberExpression'])
return moreNodePathFromSourceRange(object, sourceRange, path)
}
if (property.start <= start && property.end >= end) {
path.push(['property', 'MemberExpression'])
return moreNodePathFromSourceRange(property, sourceRange, path)
}
return path
}
if (_node.type === 'PipeSubstitution' && isInRange) return path
if (_node.type === 'IfExpression' && isInRange) {
const { cond, then_val, else_ifs, final_else } = _node
if (cond.start <= start && cond.end >= end) {
path.push(['cond', 'IfExpression'])
return moreNodePathFromSourceRange(cond, sourceRange, path)
}
if (then_val.start <= start && then_val.end >= end) {
path.push(['then_val', 'IfExpression'])
path.push(['body', 'IfExpression'])
return getNodePathFromSourceRange(then_val, sourceRange, path)
}
for (let i = 0; i < else_ifs.length; i++) {
const else_if = else_ifs[i]
if (else_if.start <= start && else_if.end >= end) {
path.push(['else_ifs', 'IfExpression'])
path.push([i, 'index'])
const { cond, then_val } = else_if
if (cond.start <= start && cond.end >= end) {
path.push(['cond', 'IfExpression'])
return moreNodePathFromSourceRange(cond, sourceRange, path)
}
path.push(['then_val', 'IfExpression'])
path.push(['body', 'IfExpression'])
return getNodePathFromSourceRange(then_val, sourceRange, path)
}
}
if (final_else.start <= start && final_else.end >= end) {
path.push(['final_else', 'IfExpression'])
path.push(['body', 'IfExpression'])
return getNodePathFromSourceRange(final_else, sourceRange, path)
}
return path
}
if (_node.type === 'ImportStatement' && isInRange) {
if (_node.selector && _node.selector.type === 'List') {
path.push(['selector', 'ImportStatement'])
const { items } = _node.selector
for (let i = 0; i < items.length; i++) {
const item = items[i]
if (item.start <= start && item.end >= end) {
path.push(['items', 'ImportSelector'])
path.push([i, 'index'])
if (item.name.start <= start && item.name.end >= end) {
path.push(['name', 'ImportItem'])
return path
}
if (
item.alias &&
item.alias.start <= start &&
item.alias.end >= end
) {
path.push(['alias', 'ImportItem'])
return path
}
return path
}
}
return path
}
return path
}
console.error('not implemented: ' + node.type)
return path
}
export function getNodePathFromSourceRange(
node: Program,
sourceRange: SourceRange,
previousPath: PathToNode = [['body', '']]
): PathToNode {
const [start, end] = sourceRange || []
let path: PathToNode = [...previousPath]
const _node = { ...node }
// loop over each statement in body getting the index with a for loop
for (
let statementIndex = 0;
statementIndex < _node.body.length;
statementIndex++
) {
const statement = _node.body[statementIndex]
if (statement.start <= start && statement.end >= end) {
path.push([statementIndex, 'index'])
return moreNodePathFromSourceRange(statement, sourceRange, path)
}
}
return path
}
type KCLNode = Node< type KCLNode = Node<
| Expr | Expr
| ExpressionStatement | ExpressionStatement
@ -413,6 +717,16 @@ function isTypeInArrayExp(
return node.elements.some((el) => isTypeInValue(el, syntaxType)) 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( export function isLinesParallelAndConstrained(
ast: Program, ast: Program,
artifactGraph: ArtifactGraph, artifactGraph: ArtifactGraph,

View File

@ -1,316 +0,0 @@
import {
Expr,
ExpressionStatement,
VariableDeclaration,
ReturnStatement,
SourceRange,
PathToNode,
Program,
} from './wasm'
import { ImportStatement } from 'wasm-lib/kcl/bindings/ImportStatement'
import { Node } from 'wasm-lib/kcl/bindings/Node'
function moreNodePathFromSourceRange(
node: Node<
| Expr
| ImportStatement
| ExpressionStatement
| VariableDeclaration
| ReturnStatement
>,
sourceRange: SourceRange,
previousPath: PathToNode = [['body', '']]
): PathToNode {
const [start, end] = sourceRange
let path: PathToNode = [...previousPath]
const _node = { ...node }
if (start < _node.start || end > _node.end) return path
const isInRange = _node.start <= start && _node.end >= end
if (
(_node.type === 'Identifier' ||
_node.type === 'Literal' ||
_node.type === 'TagDeclarator') &&
isInRange
) {
return path
}
if (_node.type === 'CallExpression' && isInRange) {
const { callee, arguments: args } = _node
if (
callee.type === 'Identifier' &&
callee.start <= start &&
callee.end >= end
) {
path.push(['callee', 'CallExpression'])
return path
}
if (args.length > 0) {
for (let argIndex = 0; argIndex < args.length; argIndex++) {
const arg = args[argIndex]
if (arg.start <= start && arg.end >= end) {
path.push(['arguments', 'CallExpression'])
path.push([argIndex, 'index'])
return moreNodePathFromSourceRange(arg, sourceRange, path)
}
}
}
return path
}
if (_node.type === 'CallExpressionKw' && isInRange) {
const { callee, arguments: args } = _node
if (
callee.type === 'Identifier' &&
callee.start <= start &&
callee.end >= end
) {
path.push(['callee', 'CallExpressionKw'])
return path
}
if (args.length > 0) {
for (let argIndex = 0; argIndex < args.length; argIndex++) {
const arg = args[argIndex].arg
if (arg.start <= start && arg.end >= end) {
path.push(['arguments', 'CallExpressionKw'])
path.push([argIndex, 'index'])
return moreNodePathFromSourceRange(arg, sourceRange, path)
}
}
}
return path
}
if (_node.type === 'BinaryExpression' && isInRange) {
const { left, right } = _node
if (left.start <= start && left.end >= end) {
path.push(['left', 'BinaryExpression'])
return moreNodePathFromSourceRange(left, sourceRange, path)
}
if (right.start <= start && right.end >= end) {
path.push(['right', 'BinaryExpression'])
return moreNodePathFromSourceRange(right, sourceRange, path)
}
return path
}
if (_node.type === 'PipeExpression' && isInRange) {
const { body } = _node
for (let i = 0; i < body.length; i++) {
const pipe = body[i]
if (pipe.start <= start && pipe.end >= end) {
path.push(['body', 'PipeExpression'])
path.push([i, 'index'])
return moreNodePathFromSourceRange(pipe, sourceRange, path)
}
}
return path
}
if (_node.type === 'ArrayExpression' && isInRange) {
const { elements } = _node
for (let elIndex = 0; elIndex < elements.length; elIndex++) {
const element = elements[elIndex]
if (element.start <= start && element.end >= end) {
path.push(['elements', 'ArrayExpression'])
path.push([elIndex, 'index'])
return moreNodePathFromSourceRange(element, sourceRange, path)
}
}
return path
}
if (_node.type === 'ObjectExpression' && isInRange) {
const { properties } = _node
for (let propIndex = 0; propIndex < properties.length; propIndex++) {
const property = properties[propIndex]
if (property.start <= start && property.end >= end) {
path.push(['properties', 'ObjectExpression'])
path.push([propIndex, 'index'])
if (property.key.start <= start && property.key.end >= end) {
path.push(['key', 'Property'])
return moreNodePathFromSourceRange(property.key, sourceRange, path)
}
if (property.value.start <= start && property.value.end >= end) {
path.push(['value', 'Property'])
return moreNodePathFromSourceRange(property.value, sourceRange, path)
}
}
}
return path
}
if (_node.type === 'ExpressionStatement' && isInRange) {
const { expression } = _node
path.push(['expression', 'ExpressionStatement'])
return moreNodePathFromSourceRange(expression, sourceRange, path)
}
if (_node.type === 'VariableDeclaration' && isInRange) {
const declaration = _node.declaration
if (declaration.start <= start && declaration.end >= end) {
path.push(['declaration', 'VariableDeclaration'])
const init = declaration.init
if (init.start <= start && init.end >= end) {
path.push(['init', ''])
return moreNodePathFromSourceRange(init, sourceRange, path)
}
}
}
if (_node.type === 'VariableDeclaration' && isInRange) {
const declaration = _node.declaration
if (declaration.start <= start && declaration.end >= end) {
const init = declaration.init
if (init.start <= start && init.end >= end) {
path.push(['declaration', 'VariableDeclaration'])
path.push(['init', ''])
return moreNodePathFromSourceRange(init, sourceRange, path)
}
}
return path
}
if (_node.type === 'UnaryExpression' && isInRange) {
const { argument } = _node
if (argument.start <= start && argument.end >= end) {
path.push(['argument', 'UnaryExpression'])
return moreNodePathFromSourceRange(argument, sourceRange, path)
}
return path
}
if (_node.type === 'FunctionExpression' && isInRange) {
for (let i = 0; i < _node.params.length; i++) {
const param = _node.params[i]
if (param.identifier.start <= start && param.identifier.end >= end) {
path.push(['params', 'FunctionExpression'])
path.push([i, 'index'])
return moreNodePathFromSourceRange(param.identifier, sourceRange, path)
}
}
if (_node.body.start <= start && _node.body.end >= end) {
path.push(['body', 'FunctionExpression'])
const fnBody = _node.body.body
for (let i = 0; i < fnBody.length; i++) {
const statement = fnBody[i]
if (statement.start <= start && statement.end >= end) {
path.push(['body', 'FunctionExpression'])
path.push([i, 'index'])
return moreNodePathFromSourceRange(statement, sourceRange, path)
}
}
}
return path
}
if (_node.type === 'ReturnStatement' && isInRange) {
const { argument } = _node
if (argument.start <= start && argument.end >= end) {
path.push(['argument', 'ReturnStatement'])
return moreNodePathFromSourceRange(argument, sourceRange, path)
}
return path
}
if (_node.type === 'MemberExpression' && isInRange) {
const { object, property } = _node
if (object.start <= start && object.end >= end) {
path.push(['object', 'MemberExpression'])
return moreNodePathFromSourceRange(object, sourceRange, path)
}
if (property.start <= start && property.end >= end) {
path.push(['property', 'MemberExpression'])
return moreNodePathFromSourceRange(property, sourceRange, path)
}
return path
}
if (_node.type === 'PipeSubstitution' && isInRange) return path
if (_node.type === 'IfExpression' && isInRange) {
const { cond, then_val, else_ifs, final_else } = _node
if (cond.start <= start && cond.end >= end) {
path.push(['cond', 'IfExpression'])
return moreNodePathFromSourceRange(cond, sourceRange, path)
}
if (then_val.start <= start && then_val.end >= end) {
path.push(['then_val', 'IfExpression'])
path.push(['body', 'IfExpression'])
return getNodePathFromSourceRange(then_val, sourceRange, path)
}
for (let i = 0; i < else_ifs.length; i++) {
const else_if = else_ifs[i]
if (else_if.start <= start && else_if.end >= end) {
path.push(['else_ifs', 'IfExpression'])
path.push([i, 'index'])
const { cond, then_val } = else_if
if (cond.start <= start && cond.end >= end) {
path.push(['cond', 'IfExpression'])
return moreNodePathFromSourceRange(cond, sourceRange, path)
}
path.push(['then_val', 'IfExpression'])
path.push(['body', 'IfExpression'])
return getNodePathFromSourceRange(then_val, sourceRange, path)
}
}
if (final_else.start <= start && final_else.end >= end) {
path.push(['final_else', 'IfExpression'])
path.push(['body', 'IfExpression'])
return getNodePathFromSourceRange(final_else, sourceRange, path)
}
return path
}
if (_node.type === 'ImportStatement' && isInRange) {
if (_node.selector && _node.selector.type === 'List') {
path.push(['selector', 'ImportStatement'])
const { items } = _node.selector
for (let i = 0; i < items.length; i++) {
const item = items[i]
if (item.start <= start && item.end >= end) {
path.push(['items', 'ImportSelector'])
path.push([i, 'index'])
if (item.name.start <= start && item.name.end >= end) {
path.push(['name', 'ImportItem'])
return path
}
if (
item.alias &&
item.alias.start <= start &&
item.alias.end >= end
) {
path.push(['alias', 'ImportItem'])
return path
}
return path
}
}
return path
}
return path
}
console.error('not implemented: ' + node.type)
return path
}
export function getNodePathFromSourceRange(
node: Program,
sourceRange: SourceRange,
previousPath: PathToNode = [['body', '']]
): PathToNode {
const [start, end] = sourceRange || []
let path: PathToNode = [...previousPath]
const _node = { ...node }
// loop over each statement in body getting the index with a for loop
for (
let statementIndex = 0;
statementIndex < _node.body.length;
statementIndex++
) {
const statement = _node.body[statementIndex]
if (statement.start <= start && statement.end >= end) {
path.push([statementIndex, 'index'])
return moreNodePathFromSourceRange(statement, sourceRange, path)
}
}
return path
}

View File

@ -16,7 +16,7 @@ import {
EdgeCut, EdgeCut,
} from 'lang/wasm' } from 'lang/wasm'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils' import { getNodePathFromSourceRange } from 'lang/queryAst'
import { err } from 'lib/trap' import { err } from 'lib/trap'
export type { Artifact, ArtifactId, SegmentArtifact } from 'lang/wasm' export type { Artifact, ArtifactId, SegmentArtifact } from 'lang/wasm'

View File

@ -1014,11 +1014,6 @@ class EngineConnection extends EventTarget {
this.pingPongSpan.pong = new Date() this.pingPongSpan.pong = new Date()
break 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. // Only fires on successful authentication.
case 'ice_server_info': case 'ice_server_info':
let ice_servers = resp.data?.ice_servers let ice_servers = resp.data?.ice_servers

View File

@ -14,8 +14,7 @@ import {
CallExpression, CallExpression,
topLevelRange, topLevelRange,
} from '../wasm' } from '../wasm'
import { getNodeFromPath } from '../queryAst' import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { enginelessExecutor } from '../../lib/testHelpers' import { enginelessExecutor } from '../../lib/testHelpers'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'

View File

@ -17,9 +17,9 @@ import {
import { import {
getNodeFromPath, getNodeFromPath,
getNodeFromPathCurry, getNodeFromPathCurry,
getNodePathFromSourceRange,
getObjExprProperty, getObjExprProperty,
} from 'lang/queryAst' } from 'lang/queryAst'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { import {
isLiteralArrayOrStatic, isLiteralArrayOrStatic,
isNotLiteralArrayOrStatic, isNotLiteralArrayOrStatic,

View File

@ -20,10 +20,13 @@ import {
sketchFromKclValue, sketchFromKclValue,
Literal, Literal,
SourceRange, SourceRange,
LiteralValue,
} from '../wasm' } from '../wasm'
import { getNodeFromPath, getNodeFromPathCurry } from '../queryAst' import {
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils' getNodeFromPath,
getNodeFromPathCurry,
getNodePathFromSourceRange,
isValueZero,
} from '../queryAst'
import { import {
createArrayExpression, createArrayExpression,
createBinaryExpression, createBinaryExpression,
@ -76,32 +79,11 @@ export type ConstraintType =
| 'setAngleBetween' | 'setAngleBetween'
const REF_NUM_ERR = new Error('Referenced segment does not have a to value') 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 { function isUndef(val: any): val is undefined {
return typeof val === 'undefined' return typeof val === 'undefined'
} }
function isNum(val: any): val is number {
function isValueZero(val?: Expr): boolean { return typeof val === 'number'
return (
(val?.type === 'Literal' && forceNum(val) === 0) ||
(val?.type === 'UnaryExpression' &&
val.operator === '-' &&
val.argument.type === 'Literal' &&
Number(val.argument.value) === 0)
)
} }
function createCallWrapper( function createCallWrapper(
@ -208,7 +190,7 @@ const xyLineSetLength =
: referenceSeg : referenceSeg
? segRef ? segRef
: args[0].expr : args[0].expr
const literalARg = asNum(args[0].expr.value) const literalARg = getArgLiteralVal(args[0].expr)
if (err(literalARg)) return literalARg if (err(literalARg)) return literalARg
return createCallWrapper(xOrY, lineVal, tag, literalARg) return createCallWrapper(xOrY, lineVal, tag, literalARg)
} }
@ -229,14 +211,13 @@ const basicAngledLineCreateNode =
referencedSegment: path, referencedSegment: path,
}) => { }) => {
const refAng = path ? getAngle(path?.from, path?.to) : 0 const refAng = path ? getAngle(path?.from, path?.to) : 0
const argValue = asNum(args[0].expr.value) if (!isNum(args[0].expr.value)) return REF_NUM_ERR
if (err(argValue)) return argValue
const nonForcedAng = const nonForcedAng =
varValToUse === 'ang' varValToUse === 'ang'
? inputs[0].expr ? inputs[0].expr
: referenceSeg === 'ang' : referenceSeg === 'ang'
? getClosesAngleDirection( ? getClosesAngleDirection(
argValue, args[0].expr.value,
refAng, refAng,
createSegAngle(referenceSegName) createSegAngle(referenceSegName)
) )
@ -249,8 +230,8 @@ const basicAngledLineCreateNode =
: args[1].expr : args[1].expr
const shouldForceAng = valToForce === 'ang' && forceValueUsedInTransform const shouldForceAng = valToForce === 'ang' && forceValueUsedInTransform
const shouldForceLen = valToForce === 'len' && forceValueUsedInTransform const shouldForceLen = valToForce === 'len' && forceValueUsedInTransform
const literalArg = asNum( const literalArg = getArgLiteralVal(
valToForce === 'ang' ? args[0].expr.value : args[1].expr.value valToForce === 'ang' ? args[0].expr : args[1].expr
) )
if (err(literalArg)) return literalArg if (err(literalArg)) return literalArg
return createCallWrapper( return createCallWrapper(
@ -302,7 +283,7 @@ const getMinAndSegAngVals = (
} }
const getSignedLeg = (arg: Literal, legLenVal: BinaryPart) => const getSignedLeg = (arg: Literal, legLenVal: BinaryPart) =>
forceNum(arg) < 0 ? createUnaryExpression(legLenVal) : legLenVal Number(arg.value) < 0 ? createUnaryExpression(legLenVal) : legLenVal
const getLegAng = (ang: number, legAngleVal: BinaryPart) => { const getLegAng = (ang: number, legAngleVal: BinaryPart) => {
const normalisedAngle = ((ang % 360) + 360) % 360 // between 0 and 360 const normalisedAngle = ((ang % 360) + 360) % 360 // between 0 and 360
@ -341,7 +322,8 @@ const setHorzVertDistanceCreateNode =
referencedSegment, referencedSegment,
}) => { }) => {
const refNum = referencedSegment?.to?.[index] const refNum = referencedSegment?.to?.[index]
const literalArg = asNum(args?.[index].expr.value) const literalArg = getArgLiteralVal(args?.[index].expr)
if (err(literalArg)) return literalArg
if (isUndef(refNum) || err(literalArg)) return REF_NUM_ERR if (isUndef(refNum) || err(literalArg)) return REF_NUM_ERR
const valueUsedInTransform = roundOff(literalArg - refNum, 2) const valueUsedInTransform = roundOff(literalArg - refNum, 2)
@ -370,7 +352,7 @@ const setHorzVertDistanceForAngleLineCreateNode =
referencedSegment, referencedSegment,
}) => { }) => {
const refNum = referencedSegment?.to?.[index] const refNum = referencedSegment?.to?.[index]
const literalArg = asNum(args?.[1].expr.value) const literalArg = getArgLiteralVal(args?.[1].expr)
if (isUndef(refNum) || err(literalArg)) return REF_NUM_ERR if (isUndef(refNum) || err(literalArg)) return REF_NUM_ERR
const valueUsedInTransform = roundOff(literalArg - refNum, 2) const valueUsedInTransform = roundOff(literalArg - refNum, 2)
const binExp = createBinaryExpressionWithUnary([ const binExp = createBinaryExpressionWithUnary([
@ -392,8 +374,8 @@ const setAbsDistanceCreateNode =
index = xOrY === 'x' ? 0 : 1 index = xOrY === 'x' ? 0 : 1
): CreateStdLibSketchCallExpr => ): CreateStdLibSketchCallExpr =>
({ tag, forceValueUsedInTransform, rawArgs: args }) => { ({ tag, forceValueUsedInTransform, rawArgs: args }) => {
const literalArg = asNum(args?.[index].expr.value) const literalArg = getArgLiteralVal(args?.[index].expr)
if (err(literalArg)) return literalArg if (err(literalArg)) return REF_NUM_ERR
const valueUsedInTransform = roundOff(literalArg, 2) const valueUsedInTransform = roundOff(literalArg, 2)
const val = forceValueUsedInTransform || createLiteral(valueUsedInTransform) const val = forceValueUsedInTransform || createLiteral(valueUsedInTransform)
if (isXOrYLine) { if (isXOrYLine) {
@ -414,8 +396,8 @@ const setAbsDistanceCreateNode =
const setAbsDistanceForAngleLineCreateNode = const setAbsDistanceForAngleLineCreateNode =
(xOrY: 'x' | 'y'): CreateStdLibSketchCallExpr => (xOrY: 'x' | 'y'): CreateStdLibSketchCallExpr =>
({ tag, forceValueUsedInTransform, inputs, rawArgs: args }) => { ({ tag, forceValueUsedInTransform, inputs, rawArgs: args }) => {
const literalArg = asNum(args?.[1].expr.value) const literalArg = getArgLiteralVal(args?.[1].expr)
if (err(literalArg)) return literalArg if (err(literalArg)) return REF_NUM_ERR
const valueUsedInTransform = roundOff(literalArg, 2) const valueUsedInTransform = roundOff(literalArg, 2)
const val = forceValueUsedInTransform || createLiteral(valueUsedInTransform) const val = forceValueUsedInTransform || createLiteral(valueUsedInTransform)
return createCallWrapper( return createCallWrapper(
@ -437,7 +419,7 @@ const setHorVertDistanceForXYLines =
}) => { }) => {
const index = xOrY === 'x' ? 0 : 1 const index = xOrY === 'x' ? 0 : 1
const refNum = referencedSegment?.to?.[index] const refNum = referencedSegment?.to?.[index]
const literalArg = asNum(args?.[index].expr.value) const literalArg = getArgLiteralVal(args?.[index].expr)
if (isUndef(refNum) || err(literalArg)) return REF_NUM_ERR if (isUndef(refNum) || err(literalArg)) return REF_NUM_ERR
const valueUsedInTransform = roundOff(literalArg - refNum, 2) const valueUsedInTransform = roundOff(literalArg - refNum, 2)
const makeBinExp = createBinaryExpressionWithUnary([ const makeBinExp = createBinaryExpressionWithUnary([
@ -463,9 +445,9 @@ const setHorzVertDistanceConstraintLineCreateNode =
]) ])
const makeBinExp = (index: 0 | 1) => { const makeBinExp = (index: 0 | 1) => {
const arg = asNum(args?.[index].expr.value) const arg = getArgLiteralVal(args?.[index].expr)
const refNum = referencedSegment?.to?.[index] const refNum = referencedSegment?.to?.[index]
if (err(arg) || isUndef(refNum)) return REF_NUM_ERR if (err(arg) || !isNum(refNum)) return REF_NUM_ERR
return createBinaryExpressionWithUnary([ return createBinaryExpressionWithUnary([
createSegEnd(referenceSegName, isX), createSegEnd(referenceSegName, isX),
createLiteral(roundOff(arg - refNum, 2)), createLiteral(roundOff(arg - refNum, 2)),
@ -486,9 +468,9 @@ const setAngledIntersectLineForLines: CreateStdLibSketchCallExpr = ({
forceValueUsedInTransform, forceValueUsedInTransform,
rawArgs: args, rawArgs: args,
}) => { }) => {
const val = asNum(args[1].expr.value), const val = args[1].expr.value,
angle = asNum(args[0].expr.value) angle = args[0].expr.value
if (err(val) || err(angle)) return REF_NUM_ERR if (!isNum(val) || !isNum(angle)) return REF_NUM_ERR
const valueUsedInTransform = roundOff(val, 2) const valueUsedInTransform = roundOff(val, 2)
const varNamMap: { [key: number]: string } = { const varNamMap: { [key: number]: string } = {
0: 'ZERO', 0: 'ZERO',
@ -516,8 +498,8 @@ const setAngledIntersectForAngledLines: CreateStdLibSketchCallExpr = ({
inputs, inputs,
rawArgs: args, rawArgs: args,
}) => { }) => {
const val = asNum(args[1].expr.value) const val = args[1].expr.value
if (err(val)) return val if (!isNum(val)) return REF_NUM_ERR
const valueUsedInTransform = roundOff(val, 2) const valueUsedInTransform = roundOff(val, 2)
return intersectCallWrapper({ return intersectCallWrapper({
fnName: 'angledLineThatIntersects', fnName: 'angledLineThatIntersects',
@ -542,8 +524,8 @@ const setAngleBetweenCreateNode =
const refAngle = referencedSegment const refAngle = referencedSegment
? getAngle(referencedSegment?.from, referencedSegment?.to) ? getAngle(referencedSegment?.from, referencedSegment?.to)
: 0 : 0
const val = asNum(args[0].expr.value) const val = args[0].expr.value
if (err(val)) return val if (!isNum(val)) return REF_NUM_ERR
let valueUsedInTransform = roundOff(normaliseAngle(val - refAngle)) let valueUsedInTransform = roundOff(normaliseAngle(val - refAngle))
let firstHalfValue = createSegAngle(referenceSegName) let firstHalfValue = createSegAngle(referenceSegName)
if (Math.abs(valueUsedInTransform) > 90) { if (Math.abs(valueUsedInTransform) > 90) {
@ -724,11 +706,13 @@ const transformMap: TransformMap = {
createPipeSubstitution(), createPipeSubstitution(),
] ]
) )
const val = asNum(args[0].expr.value) if (!isNum(args[0].expr.value)) return REF_NUM_ERR
if (err(val)) return val
return createCallWrapper( return createCallWrapper(
'angledLineToX', 'angledLineToX',
[getAngleLengthSign(val, angleToMatchLengthXCall), inputs[0].expr], [
getAngleLengthSign(args[0].expr.value, angleToMatchLengthXCall),
inputs[0].expr,
],
tag tag
) )
}, },
@ -755,11 +739,13 @@ const transformMap: TransformMap = {
createPipeSubstitution(), createPipeSubstitution(),
] ]
) )
const val = asNum(args[0].expr.value) if (!isNum(args[0].expr.value)) return REF_NUM_ERR
if (err(val)) return val
return createCallWrapper( return createCallWrapper(
'angledLineToY', 'angledLineToY',
[getAngleLengthSign(val, angleToMatchLengthYCall), inputs[1].expr], [
getAngleLengthSign(args[0].expr.value, angleToMatchLengthYCall),
inputs[1].expr,
],
tag tag
) )
}, },
@ -777,7 +763,7 @@ const transformMap: TransformMap = {
forceValueUsedInTransform, forceValueUsedInTransform,
rawArgs: args, rawArgs: args,
}) => { }) => {
const val = asNum(args[0].expr.value) const val = getArgLiteralVal(args[0].expr)
if (err(val)) return val if (err(val)) return val
return createCallWrapper( return createCallWrapper(
'angledLineToY', 'angledLineToY',
@ -858,7 +844,7 @@ const transformMap: TransformMap = {
tooltip: 'yLine', tooltip: 'yLine',
createNode: ({ inputs, tag, rawArgs: args }) => { createNode: ({ inputs, tag, rawArgs: args }) => {
const expr = inputs[1].expr const expr = inputs[1].expr
if (forceNum(args[0].expr) >= 0) if (Number(args[0].expr.value) >= 0)
return createCallWrapper('yLine', expr, tag) return createCallWrapper('yLine', expr, tag)
if (isExprBinaryPart(expr)) if (isExprBinaryPart(expr))
return createCallWrapper('yLine', createUnaryExpression(expr), tag) return createCallWrapper('yLine', createUnaryExpression(expr), tag)
@ -870,7 +856,7 @@ const transformMap: TransformMap = {
tooltip: 'xLine', tooltip: 'xLine',
createNode: ({ inputs, tag, rawArgs: args }) => { createNode: ({ inputs, tag, rawArgs: args }) => {
const expr = inputs[1].expr const expr = inputs[1].expr
if (forceNum(args[0].expr) >= 0) if (Number(args[0].expr.value) >= 0)
return createCallWrapper('xLine', expr, tag) return createCallWrapper('xLine', expr, tag)
if (isExprBinaryPart(expr)) if (isExprBinaryPart(expr))
return createCallWrapper('xLine', createUnaryExpression(expr), tag) return createCallWrapper('xLine', createUnaryExpression(expr), tag)
@ -914,11 +900,10 @@ const transformMap: TransformMap = {
referenceSegName, referenceSegName,
getInputOfType(inputs, 'xRelative').expr getInputOfType(inputs, 'xRelative').expr
) )
const val = asNum(args[0].expr.value) if (!isNum(args[0].expr.value)) return REF_NUM_ERR
if (err(val)) return val
return createCallWrapper( return createCallWrapper(
'angledLineOfXLength', 'angledLineOfXLength',
[getLegAng(val, legAngle), minVal], [getLegAng(args[0].expr.value, legAngle), minVal],
tag tag
) )
}, },
@ -927,7 +912,7 @@ const transformMap: TransformMap = {
tooltip: 'xLine', tooltip: 'xLine',
createNode: ({ inputs, tag, rawArgs: args }) => { createNode: ({ inputs, tag, rawArgs: args }) => {
const expr = inputs[1].expr const expr = inputs[1].expr
if (forceNum(args[0].expr) >= 0) if (Number(args[0].expr.value) >= 0)
return createCallWrapper('xLine', expr, tag) return createCallWrapper('xLine', expr, tag)
if (isExprBinaryPart(expr)) if (isExprBinaryPart(expr))
return createCallWrapper('xLine', createUnaryExpression(expr), tag) return createCallWrapper('xLine', createUnaryExpression(expr), tag)
@ -968,11 +953,10 @@ const transformMap: TransformMap = {
inputs[1].expr, inputs[1].expr,
'legAngY' 'legAngY'
) )
const val = asNum(args[0].expr.value) if (!isNum(args[0].expr.value)) return REF_NUM_ERR
if (err(val)) return val
return createCallWrapper( return createCallWrapper(
'angledLineOfXLength', 'angledLineOfXLength',
[getLegAng(val, legAngle), minVal], [getLegAng(args[0].expr.value, legAngle), minVal],
tag tag
) )
}, },
@ -981,7 +965,7 @@ const transformMap: TransformMap = {
tooltip: 'yLine', tooltip: 'yLine',
createNode: ({ inputs, tag, rawArgs: args }) => { createNode: ({ inputs, tag, rawArgs: args }) => {
const expr = inputs[1].expr const expr = inputs[1].expr
if (forceNum(args[0].expr) >= 0) if (Number(args[0].expr.value) >= 0)
return createCallWrapper('yLine', expr, tag) return createCallWrapper('yLine', expr, tag)
if (isExprBinaryPart(expr)) if (isExprBinaryPart(expr))
return createCallWrapper('yLine', createUnaryExpression(expr), tag) return createCallWrapper('yLine', createUnaryExpression(expr), tag)
@ -1021,11 +1005,13 @@ const transformMap: TransformMap = {
createPipeSubstitution(), createPipeSubstitution(),
] ]
) )
const val = asNum(args[0].expr.value) if (!isNum(args[0].expr.value)) return REF_NUM_ERR
if (err(val)) return val
return createCallWrapper( return createCallWrapper(
'angledLineToX', 'angledLineToX',
[getAngleLengthSign(val, angleToMatchLengthXCall), inputs[1].expr], [
getAngleLengthSign(args[0].expr.value, angleToMatchLengthXCall),
inputs[1].expr,
],
tag tag
) )
}, },
@ -1071,11 +1057,13 @@ const transformMap: TransformMap = {
createPipeSubstitution(), createPipeSubstitution(),
] ]
) )
const val = asNum(args[0].expr.value) if (!isNum(args[0].expr.value)) return REF_NUM_ERR
if (err(val)) return val
return createCallWrapper( return createCallWrapper(
'angledLineToY', 'angledLineToY',
[getAngleLengthSign(val, angleToMatchLengthXCall), inputs[1].expr], [
getAngleLengthSign(args[0].expr.value, angleToMatchLengthXCall),
inputs[1].expr,
],
tag tag
) )
}, },
@ -1092,7 +1080,7 @@ const transformMap: TransformMap = {
equalLength: { equalLength: {
tooltip: 'xLine', tooltip: 'xLine',
createNode: ({ referenceSegName, tag, rawArgs: args }) => { createNode: ({ referenceSegName, tag, rawArgs: args }) => {
const argVal = asNum(args[0].expr.value) const argVal = getArgLiteralVal(args[0].expr)
if (err(argVal)) return argVal if (err(argVal)) return argVal
const segLen = createSegLen(referenceSegName) const segLen = createSegLen(referenceSegName)
if (argVal > 0) return createCallWrapper('xLine', segLen, tag, argVal) if (argVal > 0) return createCallWrapper('xLine', segLen, tag, argVal)
@ -1130,7 +1118,7 @@ const transformMap: TransformMap = {
equalLength: { equalLength: {
tooltip: 'yLine', tooltip: 'yLine',
createNode: ({ referenceSegName, tag, rawArgs: args }) => { createNode: ({ referenceSegName, tag, rawArgs: args }) => {
const argVal = asNum(args[0].expr.value) const argVal = getArgLiteralVal(args[0].expr)
if (err(argVal)) return argVal if (err(argVal)) return argVal
let segLen = createSegLen(referenceSegName) let segLen = createSegLen(referenceSegName)
if (argVal < 0) segLen = createUnaryExpression(segLen) if (argVal < 0) segLen = createUnaryExpression(segLen)
@ -1726,7 +1714,7 @@ export function transformAstSketchLines({
let kclVal = programMemory.get(varName) let kclVal = programMemory.get(varName)
let sketch let sketch
if (kclVal?.type === 'Solid') { if (kclVal?.type === 'Solid') {
sketch = kclVal.value.sketch sketch = kclVal.sketch
} else { } else {
sketch = sketchFromKclValue(kclVal, varName) sketch = sketchFromKclValue(kclVal, varName)
if (err(sketch)) { if (err(sketch)) {
@ -1835,6 +1823,11 @@ 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 type ConstraintLevel = 'free' | 'partial' | 'full'
export function getConstraintLevelFromSourceRange( export function getConstraintLevelFromSourceRange(

View File

@ -53,7 +53,7 @@ import { ArtifactId } from 'wasm-lib/kcl/bindings/Artifact'
import { ArtifactCommand } from 'wasm-lib/kcl/bindings/Artifact' import { ArtifactCommand } from 'wasm-lib/kcl/bindings/Artifact'
import { ArtifactGraph as RustArtifactGraph } from 'wasm-lib/kcl/bindings/Artifact' import { ArtifactGraph as RustArtifactGraph } from 'wasm-lib/kcl/bindings/Artifact'
import { Artifact } from './std/artifactGraph' import { Artifact } from './std/artifactGraph'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils' import { getNodePathFromSourceRange } from './queryAst'
export type { Artifact } from 'wasm-lib/kcl/bindings/Artifact' export type { Artifact } from 'wasm-lib/kcl/bindings/Artifact'
export type { ArtifactCommand } from 'wasm-lib/kcl/bindings/Artifact' export type { ArtifactCommand } from 'wasm-lib/kcl/bindings/Artifact'
@ -539,8 +539,7 @@ export function sketchFromKclValueOptional(
): Sketch | Reason { ): Sketch | Reason {
if (obj?.value?.type === 'Sketch') return obj.value if (obj?.value?.type === 'Sketch') return obj.value
if (obj?.value?.type === 'Solid') return obj.value.sketch if (obj?.value?.type === 'Solid') return obj.value.sketch
if (obj?.type === 'Sketch') return obj.value if (obj?.type === 'Solid') return obj.sketch
if (obj?.type === 'Solid') return obj.value.sketch
if (!varName) { if (!varName) {
varName = 'a KCL value' varName = 'a KCL value'
} }

View File

@ -1,40 +0,0 @@
import { expect } from 'vitest'
import { base64ToString, stringToBase64 } from './base64'
describe('base64 encoding', () => {
test('to base64, simple code', async () => {
const code = `extrusionDistance = 12`
// Generated by online tool
const expectedBase64 = `ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg==`
const base64 = stringToBase64(code)
expect(base64).toBe(expectedBase64)
})
test(`to base64, code with UTF-8 characters`, async () => {
// example adapted from MDN docs: https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
const code = `a = 5; Ā = 12.in(); 𐀀 = Ā; 文 = 12.0; 🦄 = -5;`
// Generated by online tool
const expectedBase64 = `YSA9IDU7IMSAID0gMTIuaW4oKTsg8JCAgCA9IMSAOyDmlocgPSAxMi4wOyDwn6aEID0gLTU7`
const base64 = stringToBase64(code)
expect(base64).toBe(expectedBase64)
})
// The following are simply the reverse of the above tests
test('from base64, simple code', async () => {
const base64 = `ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg==`
const expectedCode = `extrusionDistance = 12`
const code = base64ToString(base64)
expect(code).toBe(expectedCode)
})
test(`from base64, code with UTF-8 characters`, async () => {
const base64 = `YSA9IDU7IMSAID0gMTIuaW4oKTsg8JCAgCA9IMSAOyDmlocgPSAxMi4wOyDwn6aEID0gLTU7`
const expectedCode = `a = 5; Ā = 12.in(); 𐀀 = Ā; 文 = 12.0; 🦄 = -5;`
const code = base64ToString(base64)
expect(code).toBe(expectedCode)
})
})

View File

@ -1,29 +0,0 @@
/**
* Converts a string to a base64 string, preserving the UTF-8 encoding
*/
export function stringToBase64(str: string) {
return bytesToBase64(new TextEncoder().encode(str))
}
/**
* Converts a base64 string to a string, preserving the UTF-8 encoding
*/
export function base64ToString(base64: string) {
return new TextDecoder().decode(base64ToBytes(base64))
}
/**
* From the MDN Web Docs
* https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
*/
function base64ToBytes(base64: string) {
const binString = atob(base64)
return Uint8Array.from(binString, (m) => m.codePointAt(0)!)
}
function bytesToBase64(bytes: Uint8Array) {
const binString = Array.from(bytes, (byte) =>
String.fromCodePoint(byte)
).join('')
return btoa(binString)
}

View File

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

View File

@ -1,8 +1,5 @@
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarning' import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarning'
import { StateMachineCommandSetConfig } from 'lib/commandTypes' import { StateMachineCommandSetConfig } from 'lib/commandTypes'
import { isDesktop } from 'lib/isDesktop'
import { baseUnitLabels, baseUnitsUnion } from 'lib/settings/settingsTypes'
import { projectsMachine } from 'machines/projectsMachine' import { projectsMachine } from 'machines/projectsMachine'
export type ProjectsCommandSchema = { export type ProjectsCommandSchema = {
@ -20,13 +17,6 @@ export type ProjectsCommandSchema = {
oldName: string oldName: string
newName: string newName: string
} }
'Import file from URL': {
name: string
code?: string
units: UnitLength_type
method: 'newProject' | 'existingProject'
projectName?: string
}
} }
export const projectsCommandBarConfig: StateMachineCommandSetConfig< export const projectsCommandBarConfig: StateMachineCommandSetConfig<
@ -36,7 +26,6 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
'Open project': { 'Open project': {
icon: 'arrowRight', icon: 'arrowRight',
description: 'Open a project', description: 'Open a project',
status: isDesktop() ? 'active' : 'inactive',
args: { args: {
name: { name: {
inputType: 'options', inputType: 'options',
@ -53,7 +42,6 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
'Create project': { 'Create project': {
icon: 'folderPlus', icon: 'folderPlus',
description: 'Create a project', description: 'Create a project',
status: isDesktop() ? 'active' : 'inactive',
args: { args: {
name: { name: {
inputType: 'string', inputType: 'string',
@ -65,7 +53,6 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
'Delete project': { 'Delete project': {
icon: 'close', icon: 'close',
description: 'Delete a project', description: 'Delete a project',
status: isDesktop() ? 'active' : 'inactive',
needsReview: true, needsReview: true,
reviewMessage: ({ argumentsToSubmit }) => reviewMessage: ({ argumentsToSubmit }) =>
CommandBarOverwriteWarning({ CommandBarOverwriteWarning({
@ -88,7 +75,6 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
icon: 'folder', icon: 'folder',
description: 'Rename a project', description: 'Rename a project',
needsReview: true, needsReview: true,
status: isDesktop() ? 'active' : 'inactive',
args: { args: {
oldName: { oldName: {
inputType: 'options', inputType: 'options',
@ -106,80 +92,4 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
}, },
}, },
}, },
'Import file from URL': {
icon: 'file',
description: 'Create a file',
needsReview: true,
status: 'active',
args: {
method: {
inputType: 'options',
required: true,
skip: true,
options: isDesktop()
? [
{ name: 'New project', value: 'newProject' },
{ name: 'Existing project', value: 'existingProject' },
]
: [{ name: 'Overwrite', value: 'existingProject' }],
valueSummary(value) {
return isDesktop()
? value === 'newProject'
? 'New project'
: 'Existing project'
: 'Overwrite'
},
},
// TODO: We can't get the currently-opened project to auto-populate here because
// it's not available on projectMachine, but lower in fileMachine. Unify these.
projectName: {
inputType: 'options',
required: (commandsContext) =>
isDesktop() &&
commandsContext.argumentsToSubmit.method === 'existingProject',
skip: true,
options: (_, context) =>
context?.projects.map((p) => ({
name: p.name!,
value: p.name!,
})) || [],
},
name: {
inputType: 'string',
required: isDesktop(),
skip: true,
},
code: {
inputType: 'text',
required: true,
skip: true,
valueSummary(value) {
const lineCount = value?.trim().split('\n').length
return `${lineCount} line${lineCount === 1 ? '' : 's'}`
},
},
units: {
inputType: 'options',
required: false,
skip: true,
options: baseUnitsUnion.map((unit) => ({
name: baseUnitLabels[unit],
value: unit,
})),
},
},
reviewMessage(commandBarContext) {
return isDesktop()
? `Will add the contents from URL to a new ${
commandBarContext.argumentsToSubmit.method === 'newProject'
? 'project with file main.kcl'
: `file within the project "${commandBarContext.argumentsToSubmit.projectName}"`
} named "${
commandBarContext.argumentsToSubmit.name
}", and set default units to "${
commandBarContext.argumentsToSubmit.units
}".`
: `Will overwrite the contents of the current file with the contents from the URL.`
},
},
} }

View File

@ -207,64 +207,3 @@ export const shellValidator = async ({
return 'Unable to shell with the provided selection' 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'
}

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