Compare commits
30 Commits
revert-510
...
nightly-v2
Author | SHA1 | Date | |
---|---|---|---|
f321ecdff0 | |||
d114ab798c | |||
69fec37107 | |||
8ca8c49cc3 | |||
b25fc302fd | |||
648b37c1dd | |||
18e5da5ca4 | |||
46524cda10 | |||
4585671a5d | |||
e7203b9e7a | |||
ab375f4b92 | |||
04ed6f52ee | |||
2332338ca1 | |||
41b97de3d1 | |||
8ef31a0be1 | |||
3adb42b5f2 | |||
20016b101e | |||
8d9dbf36c3 | |||
440704ed9f | |||
2261217a5d | |||
10da986649 | |||
10789d9c3c | |||
67cc4f5835 | |||
2692f2b73a | |||
965cb18059 | |||
a022b8ef6c | |||
4d24bf7c94 | |||
9a537da183 | |||
df81b76b8b | |||
ac3f7ab712 |
44
.github/workflows/cargo-bench.yml
vendored
@ -1,44 +0,0 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- '**.rs'
|
||||
- '**/Cargo.toml'
|
||||
- '**/Cargo.lock'
|
||||
- '**/rust-toolchain.toml'
|
||||
- .github/workflows/cargo-bench.yml
|
||||
pull_request:
|
||||
paths:
|
||||
- '**.rs'
|
||||
- '**/Cargo.toml'
|
||||
- '**/Cargo.lock'
|
||||
- '**/rust-toolchain.toml'
|
||||
- .github/workflows/cargo-bench.yml
|
||||
workflow_dispatch:
|
||||
permissions: read-all
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
name: cargo bench
|
||||
jobs:
|
||||
cargo-bench:
|
||||
name: Benchmark with iai
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cargo install cargo-criterion
|
||||
sudo apt update
|
||||
sudo apt install -y valgrind
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2.6.1
|
||||
- name: Benchmark kcl library
|
||||
shell: bash
|
||||
run: |-
|
||||
cd src/wasm-lib/kcl; cargo bench --all-features -- iai
|
||||
env:
|
||||
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}
|
||||
|
@ -4,8 +4,12 @@ excerpt: "Import a CAD file."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
**WARNING:** This function is deprecated.
|
||||
|
||||
Import a CAD file.
|
||||
|
||||
**DEPRECATED** Prefer to use import statements.
|
||||
|
||||
For formats lacking unit data (such as STL, OBJ, or PLY files), the default unit of measurement is millimeters. Alternatively you may specify the unit by passing your desired measurement unit in the options parameter. When importing a GLTF file, the bin file will be imported as well. Import paths are relative to the current project directory.
|
||||
|
||||
Note: The import command currently only works when using the native Modeling App.
|
||||
|
@ -51,7 +51,6 @@ layout: manual
|
||||
* [`helixRevolutions`](kcl/helixRevolutions)
|
||||
* [`hole`](kcl/hole)
|
||||
* [`hollow`](kcl/hollow)
|
||||
* [`import`](kcl/import)
|
||||
* [`inch`](kcl/inch)
|
||||
* [`lastSegX`](kcl/lastSegX)
|
||||
* [`lastSegY`](kcl/lastSegY)
|
||||
|
20429
docs/kcl/std.json
28
docs/kcl/types/Face.md
Normal file
@ -0,0 +1,28 @@
|
||||
---
|
||||
title: "Face"
|
||||
excerpt: "A face."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
A face.
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `id` |`string`| The id of the face. | No |
|
||||
| `value` |`string`| The tag of the face. | No |
|
||||
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face’s X axis be? | No |
|
||||
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face’s 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 |
|
||||
|
||||
|
@ -20,6 +20,7 @@ A helix.
|
||||
| `revolutions` |`number`| Number of revolutions. | No |
|
||||
| `angleStart` |`number`| Start angle (in degrees). | No |
|
||||
| `ccw` |`boolean`| Is the helix rotation counter clockwise? | No |
|
||||
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A helix. | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||
|
||||
|
||||
|
@ -20,6 +20,7 @@ A helix.
|
||||
| `revolutions` |`number`| Number of revolutions. | No |
|
||||
| `angleStart` |`number`| Start angle (in degrees). | No |
|
||||
| `ccw` |`boolean`| Is the helix rotation counter clockwise? | No |
|
||||
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A helix. | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||
|
||||
|
||||
|
@ -168,7 +168,6 @@ Any KCL value.
|
||||
|
||||
|
||||
----
|
||||
A plane.
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
@ -181,17 +180,10 @@ A plane.
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: [`Plane`](/docs/kcl/types/Plane)| | No |
|
||||
| `id` |`string`| The id of the plane. | No |
|
||||
| `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| Any KCL value. | No |
|
||||
| `origin` |[`Point3d`](/docs/kcl/types/Point3d)| Origin of the plane. | No |
|
||||
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s X axis be? | No |
|
||||
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s Y axis be? | No |
|
||||
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||
| `value` |[`Plane`](/docs/kcl/types/Plane)| Any KCL value. | No |
|
||||
|
||||
|
||||
----
|
||||
A face.
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
@ -203,14 +195,8 @@ A face.
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `Face`| | No |
|
||||
| `id` |`string`| The id of the face. | No |
|
||||
| `value` |`string`| The tag of the face. | No |
|
||||
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face’s X axis be? | No |
|
||||
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face’s Y axis be? | No |
|
||||
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
|
||||
| `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||
| `type` |enum: [`Face`](/docs/kcl/types/Face)| | No |
|
||||
| `value` |[`Face`](/docs/kcl/types/Face)| Any KCL value. | No |
|
||||
|
||||
|
||||
----
|
||||
@ -246,7 +232,6 @@ A face.
|
||||
|
||||
|
||||
----
|
||||
An solid is a collection of extrude surfaces.
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
@ -259,14 +244,7 @@ An solid is a collection of extrude surfaces.
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: [`Solid`](/docs/kcl/types/Solid)| | No |
|
||||
| `id` |`string`| The id of the solid. | No |
|
||||
| `value` |`[` [`ExtrudeSurface`](/docs/kcl/types/ExtrudeSurface) `]`| The extrude surfaces. | No |
|
||||
| `sketch` |[`Sketch`](/docs/kcl/types/Sketch)| The sketch. | No |
|
||||
| `height` |`number`| The height of the solid. | No |
|
||||
| `startCapId` |`string`| The id of the extrusion start cap | No |
|
||||
| `endCapId` |`string`| The id of the extrusion end cap | No |
|
||||
| `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |
|
||||
| `value` |[`Solid`](/docs/kcl/types/Solid)| Any KCL value. | No |
|
||||
|
||||
|
||||
----
|
||||
@ -286,7 +264,6 @@ An solid is a collection of extrude surfaces.
|
||||
|
||||
|
||||
----
|
||||
A helix.
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
@ -299,11 +276,7 @@ A helix.
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: [`Helix`](/docs/kcl/types/Helix)| | No |
|
||||
| `value` |`string`| The id of the helix. | No |
|
||||
| `revolutions` |`number`| Number of revolutions. | No |
|
||||
| `angleStart` |`number`| Start angle (in degrees). | No |
|
||||
| `ccw` |`boolean`| Is the helix rotation counter clockwise? | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||
| `value` |[`Helix`](/docs/kcl/types/Helix)| Any KCL value. | No |
|
||||
|
||||
|
||||
----
|
||||
|
@ -22,6 +22,7 @@ A plane.
|
||||
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s X axis be? | No |
|
||||
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s Y axis be? | No |
|
||||
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
|
||||
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A plane. | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||
|
||||
|
||||
|
@ -21,6 +21,7 @@ A sketch is a collection of paths.
|
||||
| `on` |[`SketchSurface`](/docs/kcl/types/SketchSurface)| What the sketch is on (can be a plane or a face). | No |
|
||||
| `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No |
|
||||
| `tags` |`object`| Tag identifiers that have been declared in this sketch. | No |
|
||||
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch is a collection of paths. | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |
|
||||
|
||||
|
||||
|
@ -30,6 +30,7 @@ A sketch is a collection of paths.
|
||||
| `on` |[`SketchSurface`](/docs/kcl/types/SketchSurface)| What the sketch is on (can be a plane or a face). | No |
|
||||
| `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No |
|
||||
| `tags` |`object`| Tag identifiers that have been declared in this sketch. | No |
|
||||
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch or a group of sketches. | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |
|
||||
|
||||
|
||||
|
@ -31,6 +31,7 @@ A plane.
|
||||
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s X axis be? | No |
|
||||
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s Y axis be? | No |
|
||||
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
|
||||
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch type. | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||
|
||||
|
||||
@ -54,6 +55,7 @@ A face.
|
||||
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face’s Y axis be? | No |
|
||||
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
|
||||
| `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No |
|
||||
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch type. | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||
|
||||
|
||||
|
@ -23,6 +23,7 @@ An solid is a collection of extrude surfaces.
|
||||
| `startCapId` |`string`| The id of the extrusion start cap | No |
|
||||
| `endCapId` |`string`| The id of the extrusion end cap | No |
|
||||
| `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No |
|
||||
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| An solid is a collection of extrude surfaces. | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |
|
||||
|
||||
|
||||
|
@ -32,6 +32,7 @@ An solid is a collection of extrude surfaces.
|
||||
| `startCapId` |`string`| The id of the extrusion start cap | No |
|
||||
| `endCapId` |`string`| The id of the extrusion end cap | No |
|
||||
| `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No |
|
||||
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A solid or a group of solids. | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |
|
||||
|
||||
|
||||
|
107
docs/kcl/types/UnitLen.md
Normal file
@ -0,0 +1,107 @@
|
||||
---
|
||||
title: "UnitLen"
|
||||
excerpt: ""
|
||||
layout: manual
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
**This schema accepts exactly one of the following:**
|
||||
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `Mm`| | No |
|
||||
|
||||
|
||||
----
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `Cm`| | No |
|
||||
|
||||
|
||||
----
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `M`| | No |
|
||||
|
||||
|
||||
----
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `Inches`| | No |
|
||||
|
||||
|
||||
----
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `Feet`| | No |
|
||||
|
||||
|
||||
----
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `Yards`| | No |
|
||||
|
||||
|
||||
----
|
||||
|
||||
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { test, expect } from './zoo-test'
|
||||
|
||||
import { getUtils } from './test-utils'
|
||||
import * as fsp from 'fs/promises'
|
||||
import { executorInputPath, getUtils } from './test-utils'
|
||||
import { KCL_DEFAULT_LENGTH } from 'lib/constants'
|
||||
import path from 'path'
|
||||
|
||||
test.describe('Command bar tests', () => {
|
||||
test('Extrude from command bar selects extrude line after', async ({
|
||||
@ -305,4 +306,132 @@ test.describe('Command bar tests', () => {
|
||||
await arcToolCommand.click()
|
||||
await expect(arcToolButton).toHaveAttribute('aria-pressed', 'true')
|
||||
})
|
||||
|
||||
test(`Reacts to query param to open "import from URL" command`, async ({
|
||||
page,
|
||||
cmdBar,
|
||||
editor,
|
||||
homePage,
|
||||
}) => {
|
||||
await test.step(`Prepare and navigate to home page with query params`, async () => {
|
||||
const targetURL = `?create-file&name=test&units=mm&code=ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg%3D%3D&ask-open-desktop`
|
||||
await homePage.expectState({
|
||||
projectCards: [],
|
||||
sortBy: 'last-modified-desc',
|
||||
})
|
||||
await page.goto(page.url() + targetURL)
|
||||
expect(page.url()).toContain(targetURL)
|
||||
})
|
||||
|
||||
await test.step(`Submit the command`, async () => {
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
commandName: 'Import file from URL',
|
||||
currentArgKey: 'method',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Method: '',
|
||||
Name: 'test',
|
||||
Code: '1 line',
|
||||
},
|
||||
highlightedHeaderArg: 'method',
|
||||
})
|
||||
await cmdBar.selectOption({ name: 'New Project' }).click()
|
||||
await cmdBar.expectState({
|
||||
stage: 'review',
|
||||
commandName: 'Import file from URL',
|
||||
headerArguments: {
|
||||
Method: 'New project',
|
||||
Name: 'test',
|
||||
Code: '1 line',
|
||||
},
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
})
|
||||
|
||||
await test.step(`Ensure we created the project and are in the modeling scene`, async () => {
|
||||
await editor.expectEditor.toContain('extrusionDistance = 12')
|
||||
})
|
||||
})
|
||||
|
||||
test(`"import from URL" can add to existing project`, async ({
|
||||
page,
|
||||
cmdBar,
|
||||
editor,
|
||||
homePage,
|
||||
toolbar,
|
||||
context,
|
||||
}) => {
|
||||
await context.folderSetupFn(async (dir) => {
|
||||
const testProjectDir = path.join(dir, 'testProjectDir')
|
||||
await Promise.all([fsp.mkdir(testProjectDir, { recursive: true })])
|
||||
await Promise.all([
|
||||
fsp.copyFile(
|
||||
executorInputPath('cylinder.kcl'),
|
||||
path.join(testProjectDir, 'main.kcl')
|
||||
),
|
||||
])
|
||||
})
|
||||
await test.step(`Prepare and navigate to home page with query params`, async () => {
|
||||
const targetURL = `?create-file&name=test&units=mm&code=ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg%3D%3D&ask-open-desktop`
|
||||
await homePage.expectState({
|
||||
projectCards: [
|
||||
{
|
||||
fileCount: 1,
|
||||
title: 'testProjectDir',
|
||||
},
|
||||
],
|
||||
sortBy: 'last-modified-desc',
|
||||
})
|
||||
await page.goto(page.url() + targetURL)
|
||||
expect(page.url()).toContain(targetURL)
|
||||
})
|
||||
|
||||
await test.step(`Submit the command`, async () => {
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
commandName: 'Import file from URL',
|
||||
currentArgKey: 'method',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Method: '',
|
||||
Name: 'test',
|
||||
Code: '1 line',
|
||||
},
|
||||
highlightedHeaderArg: 'method',
|
||||
})
|
||||
await cmdBar.selectOption({ name: 'Existing Project' }).click()
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
commandName: 'Import file from URL',
|
||||
currentArgKey: 'projectName',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Method: 'Existing project',
|
||||
Name: 'test',
|
||||
ProjectName: '',
|
||||
Code: '1 line',
|
||||
},
|
||||
highlightedHeaderArg: 'projectName',
|
||||
})
|
||||
await cmdBar.selectOption({ name: 'testProjectDir' }).click()
|
||||
await cmdBar.expectState({
|
||||
stage: 'review',
|
||||
commandName: 'Import file from URL',
|
||||
headerArguments: {
|
||||
Method: 'Existing project',
|
||||
ProjectName: 'testProjectDir',
|
||||
Name: 'test',
|
||||
Code: '1 line',
|
||||
},
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
})
|
||||
|
||||
await test.step(`Ensure we created the project and are in the modeling scene`, async () => {
|
||||
await editor.expectEditor.toContain('extrusionDistance = 12')
|
||||
await toolbar.openPane('files')
|
||||
await toolbar.expectFileTreeState(['main.kcl', 'test.kcl'])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -38,14 +38,14 @@ test.describe('Debug pane', () => {
|
||||
// Set the code in the code editor.
|
||||
await u.codeLocator.click()
|
||||
await page.keyboard.type(code, { delay: 0 })
|
||||
// Scroll to the feature tree.
|
||||
// Scroll to the artifact graph.
|
||||
await tree.scrollIntoViewIfNeeded()
|
||||
// Expand the feature tree.
|
||||
await tree.getByText('Feature Tree').click()
|
||||
// Expand the artifact graph.
|
||||
await tree.getByText('Artifact Graph').click()
|
||||
// Just expanded the details, making the element taller, so scroll again.
|
||||
await tree.getByText('Plane').first().scrollIntoViewIfNeeded()
|
||||
})
|
||||
// Extract the artifact IDs from the debug feature tree.
|
||||
// Extract the artifact IDs from the debug artifact graph.
|
||||
const initialSegmentIds = await segment.innerText({ timeout: 5_000 })
|
||||
// The artifact ID should include a UUID.
|
||||
expect(initialSegmentIds).toMatch(
|
||||
|
@ -151,4 +151,11 @@ export class CmdBarFixture {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -103,7 +103,7 @@ export class HomePageFixture {
|
||||
.toEqual(expectedState)
|
||||
}
|
||||
|
||||
createAndGoToProject = async (projectTitle: string) => {
|
||||
createAndGoToProject = async (projectTitle = 'project-$nnn') => {
|
||||
await expect(this.projectSection).not.toHaveText('Loading your Projects...')
|
||||
await this.projectButtonNew.click()
|
||||
await this.projectTextName.click()
|
||||
|
@ -963,37 +963,31 @@ sketch002 = startSketchOn('XZ')
|
||||
await toolbar.sweepButton.click()
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Sweep',
|
||||
currentArgKey: 'profile',
|
||||
currentArgKey: 'target',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Path: '',
|
||||
Profile: '',
|
||||
Target: '',
|
||||
Trajectory: '',
|
||||
},
|
||||
highlightedHeaderArg: 'profile',
|
||||
highlightedHeaderArg: 'target',
|
||||
stage: 'arguments',
|
||||
})
|
||||
await clickOnSketch1()
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Sweep',
|
||||
currentArgKey: 'path',
|
||||
currentArgKey: 'trajectory',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Path: '',
|
||||
Profile: '1 face',
|
||||
Target: '1 face',
|
||||
Trajectory: '',
|
||||
},
|
||||
highlightedHeaderArg: 'path',
|
||||
highlightedHeaderArg: 'trajectory',
|
||||
stage: 'arguments',
|
||||
})
|
||||
await clickOnSketch2()
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Sweep',
|
||||
headerArguments: {
|
||||
Path: '1 face',
|
||||
Profile: '1 face',
|
||||
},
|
||||
stage: 'review',
|
||||
})
|
||||
await page.waitForTimeout(500)
|
||||
await cmdBar.progressCmdBar()
|
||||
await page.waitForTimeout(500)
|
||||
})
|
||||
|
||||
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
|
||||
@ -1020,6 +1014,75 @@ sketch002 = startSketchOn('XZ')
|
||||
})
|
||||
})
|
||||
|
||||
test(`Sweep point-and-click failing validation`, async ({
|
||||
context,
|
||||
page,
|
||||
homePage,
|
||||
scene,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
const initialCode = `sketch001 = startSketchOn('YZ')
|
||||
|> circle({
|
||||
center = [0, 0],
|
||||
radius = 500
|
||||
}, %)
|
||||
sketch002 = startSketchOn('XZ')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> xLine(-500, %)
|
||||
|> lineTo([-2000, 500], %)
|
||||
`
|
||||
await context.addInitScript((initialCode) => {
|
||||
localStorage.setItem('persistCode', initialCode)
|
||||
}, initialCode)
|
||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||
await homePage.goToModelingScene()
|
||||
await scene.waitForExecutionDone()
|
||||
|
||||
// One dumb hardcoded screen pixel value
|
||||
const testPoint = { x: 700, y: 250 }
|
||||
const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
|
||||
const [clickOnSketch2] = scene.makeMouseHelpers(testPoint.x - 50, testPoint.y)
|
||||
|
||||
await test.step(`Look for sketch001`, async () => {
|
||||
await toolbar.closePane('code')
|
||||
await scene.expectPixelColor([53, 53, 53], testPoint, 15)
|
||||
})
|
||||
|
||||
await test.step(`Go through the command bar flow and fail validation with a toast`, async () => {
|
||||
await toolbar.sweepButton.click()
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Sweep',
|
||||
currentArgKey: 'target',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Target: '',
|
||||
Trajectory: '',
|
||||
},
|
||||
highlightedHeaderArg: 'target',
|
||||
stage: 'arguments',
|
||||
})
|
||||
await clickOnSketch1()
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Sweep',
|
||||
currentArgKey: 'trajectory',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Target: '1 face',
|
||||
Trajectory: '',
|
||||
},
|
||||
highlightedHeaderArg: 'trajectory',
|
||||
stage: 'arguments',
|
||||
})
|
||||
await clickOnSketch2()
|
||||
await page.waitForTimeout(500)
|
||||
await cmdBar.progressCmdBar()
|
||||
await expect(
|
||||
page.getByText('Unable to sweep with the provided selection')
|
||||
).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test(`Fillet point-and-click`, async ({
|
||||
context,
|
||||
page,
|
||||
|
@ -1088,6 +1088,25 @@ test.describe(`Project management commands`, () => {
|
||||
})
|
||||
})
|
||||
|
||||
test(`Create a few projects using the default project name`, async ({
|
||||
homePage,
|
||||
toolbar,
|
||||
}) => {
|
||||
for (let i = 0; i < 12; i++) {
|
||||
await test.step(`Create project ${i}`, async () => {
|
||||
await homePage.expectState({
|
||||
projectCards: Array.from({ length: i }, (_, i) => ({
|
||||
title: `project-${i.toString().padStart(3, '0')}`,
|
||||
fileCount: 1,
|
||||
})).toReversed(),
|
||||
sortBy: 'last-modified-desc',
|
||||
})
|
||||
await homePage.createAndGoToProject()
|
||||
await toolbar.logoLink.click()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
test(
|
||||
'File in the file pane should open with a single click',
|
||||
{ tag: '@electron' },
|
||||
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 145 KiB |
@ -113,9 +113,9 @@
|
||||
"test:unit": "vitest run --mode development --exclude **/kclSamples.test.ts",
|
||||
"test:unit:kcl-samples": "vitest run --mode development ./src/lang/kclSamples.test.ts",
|
||||
"test:playwright:electron": "playwright test --config=playwright.electron.config.ts --grep-invert='@snapshot'",
|
||||
"test:playwright:electron:windows": "playwright test --config=playwright.electron.config.ts --grep-invert=\"@skipWin|@snapshot\"",
|
||||
"test:playwright:electron:macos": "playwright test --config=playwright.electron.config.ts --grep-invert='@skipMacos|@snapshot'",
|
||||
"test:playwright:electron:ubuntu": "playwright test --config=playwright.electron.config.ts --grep-invert='@skipLinux|@snapshot'",
|
||||
"test:playwright:electron:windows": "playwright test --config=playwright.electron.config.ts --grep-invert=\"@skipWin|@snapshot\" --quiet",
|
||||
"test:playwright:electron:macos": "playwright test --config=playwright.electron.config.ts --grep-invert='@skipMacos|@snapshot' --quiet",
|
||||
"test:playwright:electron:ubuntu": "playwright test --config=playwright.electron.config.ts --grep-invert='@skipLinux|@snapshot' --quiet",
|
||||
"test:playwright:electron:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@snapshot'",
|
||||
"test:playwright:electron:windows:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert=\"@skipWin|@snapshot\"",
|
||||
"test:playwright:electron:macos:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@skipMacos|@snapshot'",
|
||||
@ -201,7 +201,7 @@
|
||||
"ts-node": "^10.0.0",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.19.1",
|
||||
"vite": "^5.4.6",
|
||||
"vite": "^5.4.12",
|
||||
"vite-plugin-package-version": "^1.1.0",
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
"vitest": "^1.6.0",
|
||||
|
@ -683,9 +683,9 @@ vite-tsconfig-paths@^4.3.2:
|
||||
tsconfck "^3.0.3"
|
||||
|
||||
vite@^5.0.0:
|
||||
version "5.4.11"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.11.tgz#3b415cd4aed781a356c1de5a9ebafb837715f6e5"
|
||||
integrity sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==
|
||||
version "5.4.14"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.14.tgz#ff8255edb02134df180dcfca1916c37a6abe8408"
|
||||
integrity sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==
|
||||
dependencies:
|
||||
esbuild "^0.21.3"
|
||||
postcss "^8.4.43"
|
||||
|
15
src/App.tsx
@ -22,13 +22,28 @@ import Gizmo from 'components/Gizmo'
|
||||
import { CoreDumpManager } from 'lib/coredump'
|
||||
import { UnitsMenu } from 'components/UnitsMenu'
|
||||
import { CameraProjectionToggle } from 'components/CameraProjectionToggle'
|
||||
import { useCreateFileLinkQuery } from 'hooks/useCreateFileLinkQueryWatcher'
|
||||
import { maybeWriteToDisk } from 'lib/telemetry'
|
||||
import { commandBarActor } from 'machines/commandBarMachine'
|
||||
maybeWriteToDisk()
|
||||
.then(() => {})
|
||||
.catch(() => {})
|
||||
|
||||
export function App() {
|
||||
const { project, file } = useLoaderData() as IndexLoaderData
|
||||
|
||||
// Keep a lookout for a URL query string that invokes the 'import file from URL' command
|
||||
useCreateFileLinkQuery((argDefaultValues) => {
|
||||
commandBarActor.send({
|
||||
type: 'Find and select command',
|
||||
data: {
|
||||
groupId: 'projects',
|
||||
name: 'Import file from URL',
|
||||
argDefaultValues,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
useRefreshSettings(PATHS.FILE + 'SETTINGS')
|
||||
const navigate = useNavigate()
|
||||
const filePath = useAbsoluteFilePath()
|
||||
|
@ -31,11 +31,10 @@ import {
|
||||
settingsLoader,
|
||||
telemetryLoader,
|
||||
} from 'lib/routeLoaders'
|
||||
import { CommandBarProvider } from 'components/CommandBar/CommandBarProvider'
|
||||
import SettingsAuthProvider from 'components/SettingsAuthProvider'
|
||||
import LspProvider from 'components/LspProvider'
|
||||
import { KclContextProvider } from 'lang/KclProvider'
|
||||
import { BROWSER_PROJECT_NAME } from 'lib/constants'
|
||||
import { ASK_TO_OPEN_QUERY_PARAM, BROWSER_PROJECT_NAME } from 'lib/constants'
|
||||
import { CoreDumpManager } from 'lib/coredump'
|
||||
import { codeManager, engineCommandManager } from 'lib/singletons'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
@ -47,6 +46,7 @@ import { AppStateProvider } from 'AppState'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { RouteProvider } from 'components/RouteProvider'
|
||||
import { ProjectsContextProvider } from 'components/ProjectsContextProvider'
|
||||
import { OpenInDesktopAppHandler } from 'components/OpenInDesktopAppHandler'
|
||||
|
||||
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
|
||||
|
||||
@ -58,7 +58,7 @@ const router = createRouter([
|
||||
/* Make sure auth is the outermost provider or else we will have
|
||||
* inefficient re-renders, use the react profiler to see. */
|
||||
element: (
|
||||
<CommandBarProvider>
|
||||
<OpenInDesktopAppHandler>
|
||||
<RouteProvider>
|
||||
<SettingsAuthProvider>
|
||||
<LspProvider>
|
||||
@ -74,17 +74,26 @@ const router = createRouter([
|
||||
</LspProvider>
|
||||
</SettingsAuthProvider>
|
||||
</RouteProvider>
|
||||
</CommandBarProvider>
|
||||
</OpenInDesktopAppHandler>
|
||||
),
|
||||
errorElement: <ErrorPage />,
|
||||
children: [
|
||||
{
|
||||
path: PATHS.INDEX,
|
||||
loader: async () => {
|
||||
loader: async ({ request }) => {
|
||||
const onDesktop = isDesktop()
|
||||
return onDesktop
|
||||
? redirect(PATHS.HOME)
|
||||
: redirect(PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME)
|
||||
const url = new URL(request.url)
|
||||
if (onDesktop) {
|
||||
return redirect(PATHS.HOME + (url.search || ''))
|
||||
} else {
|
||||
const searchParams = new URLSearchParams(url.search)
|
||||
if (!searchParams.has(ASK_TO_OPEN_QUERY_PARAM)) {
|
||||
return redirect(
|
||||
PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME + (url.search || '')
|
||||
)
|
||||
}
|
||||
}
|
||||
return null
|
||||
},
|
||||
},
|
||||
{
|
||||
|
221
src/Toolbar.tsx
@ -1,8 +1,7 @@
|
||||
import { useRef, useMemo, memo } from 'react'
|
||||
import { useRef, useMemo, memo, useCallback, useState } from 'react'
|
||||
import { isCursorInSketchCommandRange } from 'lang/util'
|
||||
import { engineCommandManager, kclManager } from 'lib/singletons'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||
import { ActionButton } from 'components/ActionButton'
|
||||
@ -22,20 +21,19 @@ import {
|
||||
} from 'lib/toolbar'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||
import { commandBarActor } from 'machines/commandBarMachine'
|
||||
|
||||
export function Toolbar({
|
||||
className = '',
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLElement>) {
|
||||
const { state, send, context } = useModelingContext()
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const iconClassName =
|
||||
'group-disabled:text-chalkboard-50 !text-inherit dark:group-enabled:group-hover:!text-inherit'
|
||||
const bgClassName = '!bg-transparent'
|
||||
const buttonBgClassName =
|
||||
'bg-chalkboard-transparent dark:bg-transparent disabled:bg-transparent dark:disabled:bg-transparent enabled:hover:bg-chalkboard-10 dark:enabled:hover:bg-chalkboard-100 pressed:!bg-primary pressed:enabled:hover:!text-chalkboard-10'
|
||||
const buttonBorderClassName =
|
||||
'!border-transparent hover:!border-chalkboard-20 dark:enabled:hover:!border-primary pressed:!border-primary ui-open:!border-primary'
|
||||
const buttonBorderClassName = '!border-transparent'
|
||||
|
||||
const sketchPathId = useMemo(() => {
|
||||
if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast))
|
||||
@ -50,6 +48,7 @@ export function Toolbar({
|
||||
const { overallState } = useNetworkContext()
|
||||
const { isExecuting } = useKclContext()
|
||||
const { isStreamReady } = useAppState()
|
||||
const [showRichContent, setShowRichContent] = useState(false)
|
||||
|
||||
const disableAllButtons =
|
||||
(overallState !== NetworkHealthState.Ok &&
|
||||
@ -71,12 +70,45 @@ export function Toolbar({
|
||||
() => ({
|
||||
modelingState: state,
|
||||
modelingSend: send,
|
||||
commandBarSend,
|
||||
sketchPathId,
|
||||
}),
|
||||
[state, send, commandBarSend, sketchPathId]
|
||||
[state, send, commandBarActor.send, sketchPathId]
|
||||
)
|
||||
|
||||
const tooltipContentClassName = !showRichContent
|
||||
? ''
|
||||
: '!text-left text-wrap !text-xs !p-0 !pb-2 flex gap-2 !max-w-none !w-72 flex-col items-stretch'
|
||||
const richContentTimeout = useRef<number | null>(null)
|
||||
const richContentClearTimeout = useRef<number | null>(null)
|
||||
// On mouse enter, show rich content after a 1s delay
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
// Cancel the clear timeout if it's already set
|
||||
if (richContentClearTimeout.current) {
|
||||
clearTimeout(richContentClearTimeout.current)
|
||||
}
|
||||
// Start our own timeout to show the rich content
|
||||
richContentTimeout.current = window.setTimeout(() => {
|
||||
setShowRichContent(true)
|
||||
if (richContentClearTimeout.current) {
|
||||
clearTimeout(richContentClearTimeout.current)
|
||||
}
|
||||
}, 1000)
|
||||
}, [setShowRichContent])
|
||||
// On mouse leave, clear the timeout and hide rich content
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
// Clear the timeout to show rich content
|
||||
if (richContentTimeout.current) {
|
||||
clearTimeout(richContentTimeout.current)
|
||||
}
|
||||
// Start a timeout to hide the rich content
|
||||
richContentClearTimeout.current = window.setTimeout(() => {
|
||||
setShowRichContent(false)
|
||||
if (richContentClearTimeout.current) {
|
||||
clearTimeout(richContentClearTimeout.current)
|
||||
}
|
||||
}, 500)
|
||||
}, [setShowRichContent])
|
||||
|
||||
/**
|
||||
* Resolve all the callbacks and values for the current mode,
|
||||
* so we don't need to worry about the other modes
|
||||
@ -174,43 +206,64 @@ export function Toolbar({
|
||||
status: itemConfig.status,
|
||||
}))}
|
||||
>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
id={maybeIconConfig[0].id}
|
||||
data-testid={maybeIconConfig[0].id}
|
||||
iconStart={{
|
||||
icon: maybeIconConfig[0].icon,
|
||||
className: iconClassName,
|
||||
bgClassName: bgClassName,
|
||||
}}
|
||||
className={
|
||||
'!border-transparent !px-0 pressed:!text-chalkboard-10 pressed:enabled:hovered:!text-chalkboard-10 ' +
|
||||
buttonBgClassName
|
||||
}
|
||||
aria-pressed={maybeIconConfig[0].isActive}
|
||||
disabled={
|
||||
disableAllButtons ||
|
||||
maybeIconConfig[0].status !== 'available' ||
|
||||
maybeIconConfig[0].disabled
|
||||
}
|
||||
name={maybeIconConfig[0].title}
|
||||
// aria-description is still in ARIA 1.3 draft.
|
||||
// eslint-disable-next-line jsx-a11y/aria-props
|
||||
aria-description={maybeIconConfig[0].description}
|
||||
onClick={() =>
|
||||
maybeIconConfig[0].onClick(configCallbackProps)
|
||||
}
|
||||
<div
|
||||
className="contents"
|
||||
// Mouse events do not fire on disabled buttons
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<span
|
||||
className={!maybeIconConfig[0].showTitle ? 'sr-only' : ''}
|
||||
<ActionButton
|
||||
Element="button"
|
||||
id={maybeIconConfig[0].id}
|
||||
data-testid={maybeIconConfig[0].id}
|
||||
iconStart={{
|
||||
icon: maybeIconConfig[0].icon,
|
||||
className: iconClassName,
|
||||
bgClassName: bgClassName,
|
||||
}}
|
||||
className={
|
||||
'!border-transparent !px-0 pressed:!text-chalkboard-10 pressed:enabled:hovered:!text-chalkboard-10 ' +
|
||||
buttonBgClassName
|
||||
}
|
||||
aria-pressed={maybeIconConfig[0].isActive}
|
||||
disabled={
|
||||
disableAllButtons ||
|
||||
maybeIconConfig[0].status !== 'available' ||
|
||||
maybeIconConfig[0].disabled
|
||||
}
|
||||
name={maybeIconConfig[0].title}
|
||||
// aria-description is still in ARIA 1.3 draft.
|
||||
// eslint-disable-next-line jsx-a11y/aria-props
|
||||
aria-description={maybeIconConfig[0].description}
|
||||
onClick={() =>
|
||||
maybeIconConfig[0].onClick(configCallbackProps)
|
||||
}
|
||||
>
|
||||
{maybeIconConfig[0].title}
|
||||
</span>
|
||||
</ActionButton>
|
||||
<ToolbarItemTooltip
|
||||
itemConfig={maybeIconConfig[0]}
|
||||
configCallbackProps={configCallbackProps}
|
||||
/>
|
||||
<span
|
||||
className={!maybeIconConfig[0].showTitle ? 'sr-only' : ''}
|
||||
>
|
||||
{maybeIconConfig[0].title}
|
||||
</span>
|
||||
<ToolbarItemTooltip
|
||||
itemConfig={maybeIconConfig[0]}
|
||||
configCallbackProps={configCallbackProps}
|
||||
wrapperClassName="ui-open:!hidden"
|
||||
contentClassName={tooltipContentClassName}
|
||||
>
|
||||
{showRichContent ? (
|
||||
<ToolbarItemTooltipRichContent
|
||||
itemConfig={maybeIconConfig[0]}
|
||||
/>
|
||||
) : (
|
||||
<ToolbarItemTooltipShortContent
|
||||
status={maybeIconConfig[0].status}
|
||||
title={maybeIconConfig[0].title}
|
||||
hotkey={maybeIconConfig[0].hotkey}
|
||||
/>
|
||||
)}
|
||||
</ToolbarItemTooltip>
|
||||
</ActionButton>
|
||||
</div>
|
||||
</ActionButtonDropdown>
|
||||
)
|
||||
}
|
||||
@ -218,7 +271,13 @@ export function Toolbar({
|
||||
|
||||
// A single button
|
||||
return (
|
||||
<div className="relative" key={itemConfig.id}>
|
||||
<div
|
||||
className="relative"
|
||||
key={itemConfig.id}
|
||||
// Mouse events do not fire on disabled buttons
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
key={itemConfig.id}
|
||||
@ -255,7 +314,18 @@ export function Toolbar({
|
||||
<ToolbarItemTooltip
|
||||
itemConfig={itemConfig}
|
||||
configCallbackProps={configCallbackProps}
|
||||
/>
|
||||
contentClassName={tooltipContentClassName}
|
||||
>
|
||||
{showRichContent ? (
|
||||
<ToolbarItemTooltipRichContent itemConfig={itemConfig} />
|
||||
) : (
|
||||
<ToolbarItemTooltipShortContent
|
||||
status={itemConfig.status}
|
||||
title={itemConfig.title}
|
||||
hotkey={itemConfig.hotkey}
|
||||
/>
|
||||
)}
|
||||
</ToolbarItemTooltip>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
@ -269,6 +339,12 @@ export function Toolbar({
|
||||
)
|
||||
}
|
||||
|
||||
interface ToolbarItemContentsProps extends React.PropsWithChildren {
|
||||
itemConfig: ToolbarItemResolved
|
||||
configCallbackProps: ToolbarItemCallbackProps
|
||||
wrapperClassName?: string
|
||||
contentClassName?: string
|
||||
}
|
||||
/**
|
||||
* The single button and dropdown button share content, so we extract it here
|
||||
* It contains a tooltip with the title, description, and links
|
||||
@ -277,12 +353,10 @@ export function Toolbar({
|
||||
const ToolbarItemTooltip = memo(function ToolbarItemContents({
|
||||
itemConfig,
|
||||
configCallbackProps,
|
||||
}: {
|
||||
itemConfig: ToolbarItemResolved
|
||||
configCallbackProps: ToolbarItemCallbackProps
|
||||
}) {
|
||||
const { state } = useModelingContext()
|
||||
|
||||
wrapperClassName = '',
|
||||
contentClassName = '',
|
||||
children,
|
||||
}: ToolbarItemContentsProps) {
|
||||
useHotkeys(
|
||||
itemConfig.hotkey || '',
|
||||
() => {
|
||||
@ -305,11 +379,50 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
|
||||
? ({ '-webkit-app-region': 'no-drag' } as React.CSSProperties)
|
||||
: {}
|
||||
}
|
||||
hoverOnly
|
||||
position="bottom"
|
||||
wrapperClassName="!p-4 !pointer-events-auto"
|
||||
contentClassName="!text-left text-wrap !text-xs !p-0 !pb-2 flex gap-2 !max-w-none !w-72 flex-col items-stretch"
|
||||
wrapperClassName={'!p-4 !pointer-events-auto ' + wrapperClassName}
|
||||
contentClassName={contentClassName}
|
||||
delay={0}
|
||||
>
|
||||
{children}
|
||||
</Tooltip>
|
||||
)
|
||||
})
|
||||
|
||||
const ToolbarItemTooltipShortContent = ({
|
||||
status,
|
||||
title,
|
||||
hotkey,
|
||||
}: {
|
||||
status: string
|
||||
title: string
|
||||
hotkey?: string | string[]
|
||||
}) => (
|
||||
<span
|
||||
className={`text-sm ${
|
||||
status !== 'available' ? 'text-chalkboard-70 dark:text-chalkboard-40' : ''
|
||||
}`}
|
||||
>
|
||||
{title}
|
||||
{hotkey && (
|
||||
<kbd className="inline-block ml-2 flex-none hotkey">{hotkey}</kbd>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
|
||||
const ToolbarItemTooltipRichContent = ({
|
||||
itemConfig,
|
||||
}: {
|
||||
itemConfig: ToolbarItemResolved
|
||||
}) => {
|
||||
const { state } = useModelingContext()
|
||||
return (
|
||||
<>
|
||||
<div className="rounded-top flex items-center gap-2 pt-3 pb-2 px-2 bg-chalkboard-20/50 dark:bg-chalkboard-80/50">
|
||||
{itemConfig.icon && (
|
||||
<CustomIcon className="w-5 h-5" name={itemConfig.icon} />
|
||||
)}
|
||||
<span
|
||||
className={`text-sm flex-1 ${
|
||||
itemConfig.status !== 'available'
|
||||
@ -378,6 +491,6 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</Tooltip>
|
||||
</>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
@ -46,8 +46,8 @@ import {
|
||||
} from 'lang/modifyAst'
|
||||
import { ActionButton } from 'components/ActionButton'
|
||||
import { err, reportRejection, trap } from 'lib/trap'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
import { commandBarActor } from 'machines/commandBarMachine'
|
||||
|
||||
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
|
||||
const [isCamMoving, setIsCamMoving] = useState(false)
|
||||
@ -510,7 +510,6 @@ const ConstraintSymbol = ({
|
||||
constrainInfo: ConstrainInfo
|
||||
verticalPosition: 'top' | 'bottom'
|
||||
}) => {
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const { context } = useModelingContext()
|
||||
const varNameMap: {
|
||||
[key in ConstrainInfo['type']]: {
|
||||
@ -630,7 +629,7 @@ const ConstraintSymbol = ({
|
||||
// disabled={implicitDesc} TODO why does this change styles that are hard to override?
|
||||
onClick={toSync(async () => {
|
||||
if (!isConstrained) {
|
||||
commandBarSend({
|
||||
commandBarActor.send({
|
||||
type: 'Find and select command',
|
||||
data: {
|
||||
name: 'Constrain with named value',
|
||||
@ -756,7 +755,6 @@ export const CamDebugSettings = () => {
|
||||
sceneInfra.camControls.reactCameraProperties
|
||||
)
|
||||
const [fov, setFov] = useState(12)
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
|
||||
useEffect(() => {
|
||||
sceneInfra.camControls.setReactCameraPropertiesCallback(setCamSettings)
|
||||
@ -775,7 +773,7 @@ export const CamDebugSettings = () => {
|
||||
type="checkbox"
|
||||
checked={camSettings.type === 'perspective'}
|
||||
onChange={() =>
|
||||
commandBarSend({
|
||||
commandBarActor.send({
|
||||
type: 'Find and select command',
|
||||
data: {
|
||||
groupId: 'settings',
|
||||
|
@ -69,7 +69,8 @@ import {
|
||||
codeManager,
|
||||
editorManager,
|
||||
} from 'lib/singletons'
|
||||
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
||||
import { getNodeFromPath } from 'lang/queryAst'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
import { executeAst, ToolTip } from 'lang/langHelpers'
|
||||
import {
|
||||
createProfileStartHandle,
|
||||
@ -1398,23 +1399,23 @@ export class SceneEntities {
|
||||
|
||||
const arg0 = arg(kclCircle3PointArgs[0])
|
||||
if (!arg0) return kclManager.ast
|
||||
arg0[0].value = points[0].x
|
||||
arg0[0].value = { value: points[0].x, suffix: 'None' }
|
||||
arg0[0].raw = points[0].x.toString()
|
||||
arg0[1].value = points[0].y
|
||||
arg0[1].value = { value: points[0].y, suffix: 'None' }
|
||||
arg0[1].raw = points[0].y.toString()
|
||||
|
||||
const arg1 = arg(kclCircle3PointArgs[1])
|
||||
if (!arg1) return kclManager.ast
|
||||
arg1[0].value = points[1].x
|
||||
arg1[0].value = { value: points[1].x, suffix: 'None' }
|
||||
arg1[0].raw = points[1].x.toString()
|
||||
arg1[1].value = points[1].y
|
||||
arg1[1].value = { value: points[1].y, suffix: 'None' }
|
||||
arg1[1].raw = points[1].y.toString()
|
||||
|
||||
const arg2 = arg(kclCircle3PointArgs[2])
|
||||
if (!arg2) return kclManager.ast
|
||||
arg2[0].value = points[2].x
|
||||
arg2[0].value = { value: points[2].x, suffix: 'None' }
|
||||
arg2[0].raw = points[2].x.toString()
|
||||
arg2[1].value = points[2].y
|
||||
arg2[1].value = { value: points[2].y, suffix: 'None' }
|
||||
arg2[1].raw = points[2].y.toString()
|
||||
|
||||
const astSnapshot = structuredClone(kclManager.ast)
|
||||
@ -2051,8 +2052,8 @@ export class SceneEntities {
|
||||
)
|
||||
if (!(sk instanceof Reason)) {
|
||||
sketch = sk
|
||||
} else if ((maybeSketch as Solid).sketch) {
|
||||
sketch = (maybeSketch as Solid).sketch
|
||||
} else if (maybeSketch && (maybeSketch.value as Solid)?.sketch) {
|
||||
sketch = (maybeSketch.value as Solid).sketch
|
||||
}
|
||||
if (!sketch) return
|
||||
|
||||
@ -2541,7 +2542,7 @@ export function sketchFromPathToNode({
|
||||
const varDec = _varDec.node
|
||||
const result = programMemory.get(varDec?.id?.name || '')
|
||||
if (result?.type === 'Solid') {
|
||||
return result.sketch
|
||||
return result.value.sketch
|
||||
}
|
||||
const sg = sketchFromKclValue(result, varDec?.id?.name)
|
||||
if (err(sg)) {
|
||||
|
@ -61,6 +61,7 @@ import { SegmentInputs } from 'lang/std/stdTypes'
|
||||
import { err } from 'lib/trap'
|
||||
import { editorManager, sceneInfra } from 'lib/singletons'
|
||||
import { Selections } from 'lib/selections'
|
||||
import { commandBarActor } from 'machines/commandBarMachine'
|
||||
|
||||
interface CreateSegmentArgs {
|
||||
input: SegmentInputs
|
||||
@ -847,7 +848,7 @@ function createLengthIndicator({
|
||||
})
|
||||
|
||||
// Command Bar
|
||||
editorManager.commandBarSend({
|
||||
commandBarActor.send({
|
||||
type: 'Find and select command',
|
||||
data: {
|
||||
name: 'Constrain length',
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { Popover } from '@headlessui/react'
|
||||
import { ActionButtonProps } from './ActionButton'
|
||||
import { CustomIcon } from './CustomIcon'
|
||||
import Tooltip from './Tooltip'
|
||||
|
||||
type ActionButtonSplitProps = ActionButtonProps & { Element: 'button' } & {
|
||||
name?: string
|
||||
dropdownTooltipText?: string
|
||||
splitMenuItems: {
|
||||
id: string
|
||||
label: string
|
||||
@ -17,6 +19,7 @@ type ActionButtonSplitProps = ActionButtonProps & { Element: 'button' } & {
|
||||
export function ActionButtonDropdown({
|
||||
splitMenuItems,
|
||||
className,
|
||||
dropdownTooltipText = 'More tools',
|
||||
children,
|
||||
...props
|
||||
}: ActionButtonSplitProps) {
|
||||
@ -26,7 +29,14 @@ export function ActionButtonDropdown({
|
||||
{({ close }) => (
|
||||
<>
|
||||
{children}
|
||||
<Popover.Button className="border-transparent dark:border-transparent p-0 m-0 rounded-none !outline-none ui-open:border-primary ui-open:bg-primary">
|
||||
<Popover.Button
|
||||
className={
|
||||
'!border-transparent dark:!border-transparent ' +
|
||||
'bg-chalkboard-transparent dark:bg-transparent disabled:bg-transparent dark:disabled:bg-transparent ' +
|
||||
'enabled:hover:bg-chalkboard-10 dark:enabled:hover:bg-chalkboard-100 ' +
|
||||
'pressed:!bg-primary pressed:enabled:hover:!text-chalkboard-10 p-0 m-0 rounded-none !outline-none ui-open:border-primary ui-open:bg-primary'
|
||||
}
|
||||
>
|
||||
<CustomIcon
|
||||
name="caretDown"
|
||||
className={
|
||||
@ -37,6 +47,14 @@ export function ActionButtonDropdown({
|
||||
<span className="sr-only">
|
||||
{props.name ? props.name + ': ' : ''}open menu
|
||||
</span>
|
||||
<Tooltip
|
||||
delay={0}
|
||||
position="bottom"
|
||||
hoverOnly
|
||||
wrapperClassName="ui-open:!hidden"
|
||||
>
|
||||
{dropdownTooltipText}
|
||||
</Tooltip>
|
||||
</Popover.Button>
|
||||
<Popover.Panel
|
||||
as="ul"
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { editorManager, engineCommandManager, kclManager } from 'lib/singletons'
|
||||
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
||||
import { getNodeFromPath } from 'lang/queryAst'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { trap } from 'lib/trap'
|
||||
import { codeToIdSelections } from 'lib/selections'
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Combobox } from '@headlessui/react'
|
||||
import { useSelector } from '@xstate/react'
|
||||
import Fuse from 'fuse.js'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { CommandArgument, CommandArgumentOption } from 'lib/commandTypes'
|
||||
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { AnyStateMachine, StateFrom } from 'xstate'
|
||||
|
||||
@ -23,7 +23,7 @@ function CommandArgOptionInput({
|
||||
placeholder?: string
|
||||
}) {
|
||||
const actorContext = useSelector(arg.machineActor, contextSelector)
|
||||
const { commandBarSend, commandBarState } = useCommandsContext()
|
||||
const commandBarState = useCommandBarState()
|
||||
const resolvedOptions = useMemo(
|
||||
() =>
|
||||
typeof arg.options === 'function'
|
||||
@ -129,6 +129,7 @@ function CommandArgOptionInput({
|
||||
<label
|
||||
htmlFor="option-input"
|
||||
className="capitalize px-2 py-1 rounded-l bg-chalkboard-100 dark:bg-chalkboard-80 text-chalkboard-10 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80"
|
||||
data-testid="cmd-bar-arg-name"
|
||||
>
|
||||
{argName}
|
||||
</label>
|
||||
@ -142,7 +143,7 @@ function CommandArgOptionInput({
|
||||
className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none"
|
||||
onKeyDown={(event) => {
|
||||
if (event.metaKey && event.key === 'k')
|
||||
commandBarSend({ type: 'Close' })
|
||||
commandBarActor.send({ type: 'Close' })
|
||||
if (event.key === 'Backspace' && !event.currentTarget.value) {
|
||||
stepBack()
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { Dialog, Popover, Transition } from '@headlessui/react'
|
||||
import { Fragment, useEffect } from 'react'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import CommandBarArgument from './CommandBarArgument'
|
||||
import CommandComboBox from '../CommandComboBox'
|
||||
import CommandBarReview from './CommandBarReview'
|
||||
@ -8,12 +7,13 @@ import { useLocation } from 'react-router-dom'
|
||||
import useHotkeyWrapper from 'lib/hotkeyWrapper'
|
||||
import { CustomIcon } from 'components/CustomIcon'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||
|
||||
export const COMMAND_PALETTE_HOTKEY = 'mod+k'
|
||||
|
||||
export const CommandBar = () => {
|
||||
const { pathname } = useLocation()
|
||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||
const commandBarState = useCommandBarState()
|
||||
const {
|
||||
context: { selectedCommand, currentArgument, commands },
|
||||
} = commandBarState
|
||||
@ -23,16 +23,16 @@ export const CommandBar = () => {
|
||||
// Close the command bar when navigating
|
||||
useEffect(() => {
|
||||
if (commandBarState.matches('Closed')) return
|
||||
commandBarSend({ type: 'Close' })
|
||||
commandBarActor.send({ type: 'Close' })
|
||||
}, [pathname])
|
||||
|
||||
// Hook up keyboard shortcuts
|
||||
useHotkeyWrapper([COMMAND_PALETTE_HOTKEY], () => {
|
||||
if (commandBarState.context.commands.length === 0) return
|
||||
if (commandBarState.matches('Closed')) {
|
||||
commandBarSend({ type: 'Open' })
|
||||
commandBarActor.send({ type: 'Open' })
|
||||
} else {
|
||||
commandBarSend({ type: 'Close' })
|
||||
commandBarActor.send({ type: 'Close' })
|
||||
}
|
||||
})
|
||||
|
||||
@ -52,14 +52,14 @@ export const CommandBar = () => {
|
||||
...entries[entries.length - 1][1],
|
||||
}
|
||||
|
||||
commandBarSend({
|
||||
commandBarActor.send({
|
||||
type: 'Edit argument',
|
||||
data: {
|
||||
arg: currentArg,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
commandBarSend({ type: 'Deselect command' })
|
||||
commandBarActor.send({ type: 'Deselect command' })
|
||||
}
|
||||
} else {
|
||||
const entries = Object.entries(selectedCommand?.args || {})
|
||||
@ -68,9 +68,9 @@ export const CommandBar = () => {
|
||||
)
|
||||
|
||||
if (index === 0) {
|
||||
commandBarSend({ type: 'Deselect command' })
|
||||
commandBarActor.send({ type: 'Deselect command' })
|
||||
} else {
|
||||
commandBarSend({
|
||||
commandBarActor.send({
|
||||
type: 'Change current argument',
|
||||
data: {
|
||||
arg: { name: entries[index - 1][0], ...entries[index - 1][1] },
|
||||
@ -85,14 +85,14 @@ export const CommandBar = () => {
|
||||
show={!commandBarState.matches('Closed') || false}
|
||||
afterLeave={() => {
|
||||
if (selectedCommand?.onCancel) selectedCommand.onCancel()
|
||||
commandBarSend({ type: 'Clear' })
|
||||
commandBarActor.send({ type: 'Clear' })
|
||||
}}
|
||||
as={Fragment}
|
||||
>
|
||||
<WrapperComponent
|
||||
open={!commandBarState.matches('Closed') || isSelectionArgument}
|
||||
onClose={() => {
|
||||
commandBarSend({ type: 'Close' })
|
||||
commandBarActor.send({ type: 'Close' })
|
||||
}}
|
||||
className={
|
||||
'fixed inset-0 z-50 overflow-y-auto pb-4 pt-1 ' +
|
||||
@ -122,7 +122,7 @@ export const CommandBar = () => {
|
||||
)
|
||||
)}
|
||||
<button
|
||||
onClick={() => commandBarSend({ type: 'Close' })}
|
||||
onClick={() => commandBarActor.send({ type: 'Close' })}
|
||||
className="group block !absolute left-auto right-full top-[-3px] m-2.5 p-0 border-none bg-transparent hover:bg-transparent"
|
||||
>
|
||||
<CustomIcon
|
||||
|
@ -2,13 +2,13 @@ import CommandArgOptionInput from './CommandArgOptionInput'
|
||||
import CommandBarBasicInput from './CommandBarBasicInput'
|
||||
import CommandBarSelectionInput from './CommandBarSelectionInput'
|
||||
import { CommandArgument } from 'lib/commandTypes'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import CommandBarHeader from './CommandBarHeader'
|
||||
import CommandBarKclInput from './CommandBarKclInput'
|
||||
import CommandBarTextareaInput from './CommandBarTextareaInput'
|
||||
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||
|
||||
function CommandBarArgument({ stepBack }: { stepBack: () => void }) {
|
||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||
const commandBarState = useCommandBarState()
|
||||
const {
|
||||
context: { currentArgument },
|
||||
} = commandBarState
|
||||
@ -16,7 +16,7 @@ function CommandBarArgument({ stepBack }: { stepBack: () => void }) {
|
||||
function onSubmit(data: unknown) {
|
||||
if (!currentArgument) return
|
||||
|
||||
commandBarSend({
|
||||
commandBarActor.send({
|
||||
type: 'Submit argument',
|
||||
data: {
|
||||
[currentArgument.name]: data,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { CommandArgument } from 'lib/commandTypes'
|
||||
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
|
||||
@ -15,8 +15,8 @@ function CommandBarBasicInput({
|
||||
stepBack: () => void
|
||||
onSubmit: (event: unknown) => void
|
||||
}) {
|
||||
const { commandBarSend, commandBarState } = useCommandsContext()
|
||||
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
|
||||
const commandBarState = useCommandBarState()
|
||||
useHotkeys('mod + k, mod + /', () => commandBarActor.send({ type: 'Close' }))
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { CustomIcon } from '../CustomIcon'
|
||||
import React, { useState } from 'react'
|
||||
import { ActionButton } from '../ActionButton'
|
||||
@ -7,9 +6,10 @@ import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { KclCommandValue, KclExpressionWithVariable } from 'lib/commandTypes'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import { roundOff } from 'lib/utils'
|
||||
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||
|
||||
function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
|
||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||
const commandBarState = useCommandBarState()
|
||||
const {
|
||||
context: { selectedCommand, currentArgument, argumentsToSubmit },
|
||||
} = commandBarState
|
||||
@ -49,7 +49,7 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
|
||||
]
|
||||
const arg = selectedCommand?.args[argName]
|
||||
if (!argName || !arg) return
|
||||
commandBarSend({
|
||||
commandBarActor.send({
|
||||
type: 'Change current argument',
|
||||
data: { arg: { ...arg, name: argName } },
|
||||
})
|
||||
@ -100,7 +100,7 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
|
||||
}
|
||||
disabled={!isReviewing && currentArgument?.name === argName}
|
||||
onClick={() => {
|
||||
commandBarSend({
|
||||
commandBarActor.send({
|
||||
type: isReviewing
|
||||
? 'Edit argument'
|
||||
: 'Change current argument',
|
||||
|
@ -7,7 +7,6 @@ import {
|
||||
} from '@codemirror/autocomplete'
|
||||
import { EditorView, keymap, ViewUpdate } from '@codemirror/view'
|
||||
import { CustomIcon } from 'components/CustomIcon'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { CommandArgument, KclCommandValue } from 'lib/commandTypes'
|
||||
import { getSystemTheme } from 'lib/theme'
|
||||
@ -20,6 +19,7 @@ import styles from './CommandBarKclInput.module.css'
|
||||
import { createIdentifier, createVariableDeclaration } from 'lang/modifyAst'
|
||||
import { useCodeMirror } from 'components/ModelingSidebar/ModelingPanes/CodeEditor'
|
||||
import { useSelector } from '@xstate/react'
|
||||
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||
|
||||
const machineContextSelector = (snapshot?: {
|
||||
context: Record<string, unknown>
|
||||
@ -37,7 +37,7 @@ function CommandBarKclInput({
|
||||
stepBack: () => void
|
||||
onSubmit: (event: unknown) => void
|
||||
}) {
|
||||
const { commandBarSend, commandBarState } = useCommandsContext()
|
||||
const commandBarState = useCommandBarState()
|
||||
const previouslySetValue = commandBarState.context.argumentsToSubmit[
|
||||
arg.name
|
||||
] as KclCommandValue | undefined
|
||||
@ -82,7 +82,7 @@ function CommandBarKclInput({
|
||||
false
|
||||
)
|
||||
const [canSubmit, setCanSubmit] = useState(true)
|
||||
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
|
||||
useHotkeys('mod + k, mod + /', () => commandBarActor.send({ type: 'Close' }))
|
||||
const editorRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const {
|
||||
|
@ -1,43 +0,0 @@
|
||||
import { createActorContext } from '@xstate/react'
|
||||
import { editorManager } from 'lib/singletons'
|
||||
import { commandBarMachine } from 'machines/commandBarMachine'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export const CommandsContext = createActorContext(
|
||||
commandBarMachine.provide({
|
||||
guards: {
|
||||
'Command has no arguments': ({ context }) => {
|
||||
return (
|
||||
!context.selectedCommand?.args ||
|
||||
Object.keys(context.selectedCommand?.args).length === 0
|
||||
)
|
||||
},
|
||||
'All arguments are skippable': ({ context }) => {
|
||||
return Object.values(context.selectedCommand!.args!).every(
|
||||
(argConfig) => argConfig.skip
|
||||
)
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
export const CommandBarProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
return (
|
||||
<CommandsContext.Provider>
|
||||
<CommandBarProviderInner>{children}</CommandBarProviderInner>
|
||||
</CommandsContext.Provider>
|
||||
)
|
||||
}
|
||||
function CommandBarProviderInner({ children }: { children: React.ReactNode }) {
|
||||
const commandBarActor = CommandsContext.useActorRef()
|
||||
|
||||
useEffect(() => {
|
||||
editorManager.setCommandBarSend(commandBarActor.send)
|
||||
})
|
||||
|
||||
return children
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||
import CommandBarHeader from './CommandBarHeader'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
|
||||
function CommandBarReview({ stepBack }: { stepBack: () => void }) {
|
||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||
const commandBarState = useCommandBarState()
|
||||
const {
|
||||
context: { argumentsToSubmit, selectedCommand },
|
||||
} = commandBarState
|
||||
@ -33,7 +33,7 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) {
|
||||
parseInt(b.keys[0], 10) - 1
|
||||
]
|
||||
const arg = selectedCommand?.args[argName]
|
||||
commandBarSend({
|
||||
commandBarActor.send({
|
||||
type: 'Edit argument',
|
||||
data: { arg: { ...arg, name: argName } },
|
||||
})
|
||||
@ -50,7 +50,7 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) {
|
||||
|
||||
function submitCommand(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault()
|
||||
commandBarSend({
|
||||
commandBarActor.send({
|
||||
type: 'Submit command',
|
||||
output: argumentsToSubmit,
|
||||
})
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { useSelector } from '@xstate/react'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { Artifact } from 'lang/std/artifactGraph'
|
||||
import { CommandArgument } from 'lib/commandTypes'
|
||||
import {
|
||||
@ -10,6 +9,7 @@ import {
|
||||
import { kclManager } from 'lib/singletons'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { toSync } from 'lib/utils'
|
||||
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||
import { modelingMachine } from 'machines/modelingMachine'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { StateFrom } from 'xstate'
|
||||
@ -49,7 +49,7 @@ function CommandBarSelectionInput({
|
||||
onSubmit: (data: unknown) => void
|
||||
}) {
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||
const commandBarState = useCommandBarState()
|
||||
const [hasSubmitted, setHasSubmitted] = useState(false)
|
||||
const selection = useSelector(arg.machineActor, selectionSelector)
|
||||
const selectionsByType = useMemo(() => {
|
||||
@ -145,7 +145,7 @@ function CommandBarSelectionInput({
|
||||
if (event.key === 'Backspace') {
|
||||
stepBack()
|
||||
} else if (event.key === 'Escape') {
|
||||
commandBarSend({ type: 'Close' })
|
||||
commandBarActor.send({ type: 'Close' })
|
||||
}
|
||||
}}
|
||||
onChange={handleChange}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { CommandArgument } from 'lib/commandTypes'
|
||||
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||
import { RefObject, useEffect, useRef } from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
|
||||
@ -15,8 +15,8 @@ function CommandBarTextareaInput({
|
||||
stepBack: () => void
|
||||
onSubmit: (event: unknown) => void
|
||||
}) {
|
||||
const { commandBarSend, commandBarState } = useCommandsContext()
|
||||
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
|
||||
const commandBarState = useCommandBarState()
|
||||
useHotkeys('mod + k, mod + /', () => commandBarActor.send({ type: 'Close' }))
|
||||
const formRef = useRef<HTMLFormElement>(null)
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null)
|
||||
useTextareaAutoGrow(inputRef)
|
||||
|
@ -1,16 +1,15 @@
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import usePlatform from 'hooks/usePlatform'
|
||||
import { hotkeyDisplay } from 'lib/hotkeyWrapper'
|
||||
import { COMMAND_PALETTE_HOTKEY } from './CommandBar/CommandBar'
|
||||
import { commandBarActor } from 'machines/commandBarMachine'
|
||||
|
||||
export function CommandBarOpenButton() {
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const platform = usePlatform()
|
||||
|
||||
return (
|
||||
<button
|
||||
className="group rounded-full flex items-center justify-center gap-2 px-2 py-1 bg-primary/10 dark:bg-chalkboard-90 dark:backdrop-blur-sm border-primary hover:border-primary dark:border-chalkboard-50 dark:hover:border-inherit text-primary dark:text-inherit"
|
||||
onClick={() => commandBarSend({ type: 'Open' })}
|
||||
onClick={() => commandBarActor.send({ type: 'Open' })}
|
||||
data-testid="command-bar-open-button"
|
||||
>
|
||||
<span>Commands</span>
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { Combobox } from '@headlessui/react'
|
||||
import Fuse from 'fuse.js'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { Command } from 'lib/commandTypes'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { CustomIcon } from './CustomIcon'
|
||||
import { getActorNextEvents } from 'lib/utils'
|
||||
import { sortCommands } from 'lib/commandUtils'
|
||||
import { commandBarActor } from 'machines/commandBarMachine'
|
||||
|
||||
function CommandComboBox({
|
||||
options,
|
||||
@ -14,7 +14,6 @@ function CommandComboBox({
|
||||
options: Command[]
|
||||
placeholder?: string
|
||||
}) {
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const [query, setQuery] = useState('')
|
||||
const [filteredOptions, setFilteredOptions] = useState<typeof options>()
|
||||
|
||||
@ -41,7 +40,7 @@ function CommandComboBox({
|
||||
}, [query])
|
||||
|
||||
function handleSelection(command: Command) {
|
||||
commandBarSend({ type: 'Select command', data: { command } })
|
||||
commandBarActor.send({ type: 'Select command', data: { command } })
|
||||
}
|
||||
|
||||
return (
|
||||
@ -61,7 +60,7 @@ function CommandComboBox({
|
||||
(event.key === 'Backspace' && !event.currentTarget.value)
|
||||
) {
|
||||
event.preventDefault()
|
||||
commandBarSend({ type: 'Close' })
|
||||
commandBarActor.send({ type: 'Close' })
|
||||
}
|
||||
}}
|
||||
placeholder={
|
||||
@ -76,34 +75,40 @@ function CommandComboBox({
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<Combobox.Options
|
||||
static
|
||||
className="overflow-y-auto max-h-96 cursor-pointer"
|
||||
>
|
||||
{filteredOptions?.map((option) => (
|
||||
<Combobox.Option
|
||||
key={option.groupId + option.name + (option.displayName || '')}
|
||||
value={option}
|
||||
className="flex items-center gap-4 px-4 py-1.5 first:mt-2 last:mb-2 ui-active:bg-primary/10 dark:ui-active:bg-chalkboard-90 ui-disabled:!text-chalkboard-50"
|
||||
disabled={optionIsDisabled(option)}
|
||||
data-testid={`cmd-bar-option`}
|
||||
>
|
||||
{'icon' in option && option.icon && (
|
||||
<CustomIcon name={option.icon} className="w-5 h-5" />
|
||||
)}
|
||||
<div className="flex-grow flex flex-col">
|
||||
<p className="my-0 leading-tight">
|
||||
{option.displayName || option.name}{' '}
|
||||
</p>
|
||||
{option.description && (
|
||||
<p className="my-0 text-xs text-chalkboard-60 dark:text-chalkboard-50">
|
||||
{option.description}
|
||||
</p>
|
||||
{filteredOptions?.length ? (
|
||||
<Combobox.Options
|
||||
static
|
||||
className="overflow-y-auto max-h-96 cursor-pointer"
|
||||
>
|
||||
{filteredOptions?.map((option) => (
|
||||
<Combobox.Option
|
||||
key={option.groupId + option.name + (option.displayName || '')}
|
||||
value={option}
|
||||
className="flex items-center gap-4 px-4 py-1.5 first:mt-2 last:mb-2 ui-active:bg-primary/10 dark:ui-active:bg-chalkboard-90 ui-disabled:!text-chalkboard-50"
|
||||
disabled={optionIsDisabled(option)}
|
||||
data-testid={`cmd-bar-option`}
|
||||
>
|
||||
{'icon' in option && option.icon && (
|
||||
<CustomIcon name={option.icon} className="w-5 h-5" />
|
||||
)}
|
||||
</div>
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
<div className="flex-grow flex flex-col">
|
||||
<p className="my-0 leading-tight">
|
||||
{option.displayName || option.name}{' '}
|
||||
</p>
|
||||
{option.description && (
|
||||
<p className="my-0 text-xs text-chalkboard-60 dark:text-chalkboard-50">
|
||||
{option.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
) : (
|
||||
<p className="px-4 pt-2 text-chalkboard-60 dark:text-chalkboard-50">
|
||||
No results found
|
||||
</p>
|
||||
)}
|
||||
</Combobox>
|
||||
)
|
||||
}
|
||||
|
@ -4,18 +4,18 @@ import { expandPlane, PlaneArtifactRich } from 'lang/std/artifactGraph'
|
||||
import { ArtifactGraph } from 'lang/wasm'
|
||||
import { DebugDisplayArray, GenericObj } from './DebugDisplayObj'
|
||||
|
||||
export function DebugFeatureTree() {
|
||||
const featureTree = useMemo(() => {
|
||||
export function DebugArtifactGraph() {
|
||||
const artifactGraphTree = useMemo(() => {
|
||||
return computeTree(engineCommandManager.artifactGraph)
|
||||
}, [engineCommandManager.artifactGraph])
|
||||
|
||||
const filterKeys: string[] = ['__meta', 'codeRef', 'pathToNode']
|
||||
return (
|
||||
<details data-testid="debug-feature-tree" className="relative">
|
||||
<summary>Feature Tree</summary>
|
||||
{featureTree.length > 0 ? (
|
||||
<summary>Artifact Graph</summary>
|
||||
{artifactGraphTree.length > 0 ? (
|
||||
<pre className="text-xs">
|
||||
<DebugDisplayArray arr={featureTree} filterKeys={filterKeys} />
|
||||
<DebugDisplayArray arr={artifactGraphTree} filterKeys={filterKeys} />
|
||||
</pre>
|
||||
) : (
|
||||
<p>(Empty)</p>
|
@ -12,7 +12,6 @@ import {
|
||||
StateFrom,
|
||||
fromPromise,
|
||||
} from 'xstate'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { fileMachine } from 'machines/fileMachine'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import {
|
||||
@ -30,6 +29,7 @@ import {
|
||||
} from 'lib/getKclSamplesManifest'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { markOnce } from 'lib/performance'
|
||||
import { commandBarActor } from 'machines/commandBarMachine'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
@ -47,9 +47,9 @@ export const FileMachineProvider = ({
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
const navigate = useNavigate()
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const { settings } = useSettingsAuthContext()
|
||||
const { project, file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
||||
const { settings, auth } = useSettingsAuthContext()
|
||||
const projectData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
||||
const { project, file } = projectData
|
||||
const [kclSamples, setKclSamples] = React.useState<KclSamplesManifestItem[]>(
|
||||
[]
|
||||
)
|
||||
@ -90,7 +90,7 @@ export const FileMachineProvider = ({
|
||||
navigateToFile: ({ context, event }) => {
|
||||
if (event.type !== 'xstate.done.actor.create-and-open-file') return
|
||||
if (event.output && 'name' in event.output) {
|
||||
commandBarSend({ type: 'Close' })
|
||||
commandBarActor.send({ type: 'Close' })
|
||||
navigate(
|
||||
`..${PATHS.FILE}/${encodeURIComponent(
|
||||
context.selectedDirectory +
|
||||
@ -296,55 +296,65 @@ export const FileMachineProvider = ({
|
||||
|
||||
const kclCommandMemo = useMemo(
|
||||
() =>
|
||||
kclCommands(
|
||||
async (data) => {
|
||||
if (data.method === 'overwrite') {
|
||||
codeManager.updateCodeStateEditor(data.code)
|
||||
await kclManager.executeCode(true)
|
||||
await codeManager.writeToFile()
|
||||
} else if (data.method === 'newFile' && isDesktop()) {
|
||||
send({
|
||||
type: 'Create file',
|
||||
data: {
|
||||
name: data.sampleName,
|
||||
content: data.code,
|
||||
makeDir: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Either way, we want to overwrite the defaultUnit project setting
|
||||
// with the sample's setting.
|
||||
if (data.sampleUnits) {
|
||||
settings.send({
|
||||
type: 'set.modeling.defaultUnit',
|
||||
data: {
|
||||
level: 'project',
|
||||
value: data.sampleUnits,
|
||||
},
|
||||
})
|
||||
}
|
||||
kclCommands({
|
||||
authToken: auth?.context?.token ?? '',
|
||||
projectData,
|
||||
settings: {
|
||||
defaultUnit: settings?.context?.modeling.defaultUnit.current ?? 'mm',
|
||||
},
|
||||
kclSamples.map((sample) => ({
|
||||
value: sample.pathFromProjectDirectoryToFirstFile,
|
||||
name: sample.title,
|
||||
}))
|
||||
).filter(
|
||||
specialPropsForSampleCommand: {
|
||||
onSubmit: async (data) => {
|
||||
if (data.method === 'overwrite') {
|
||||
codeManager.updateCodeStateEditor(data.code)
|
||||
await kclManager.executeCode(true)
|
||||
await codeManager.writeToFile()
|
||||
} else if (data.method === 'newFile' && isDesktop()) {
|
||||
send({
|
||||
type: 'Create file',
|
||||
data: {
|
||||
name: data.sampleName,
|
||||
content: data.code,
|
||||
makeDir: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Either way, we want to overwrite the defaultUnit project setting
|
||||
// with the sample's setting.
|
||||
if (data.sampleUnits) {
|
||||
settings.send({
|
||||
type: 'set.modeling.defaultUnit',
|
||||
data: {
|
||||
level: 'project',
|
||||
value: data.sampleUnits,
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
providedOptions: kclSamples.map((sample) => ({
|
||||
value: sample.pathFromProjectDirectoryToFirstFile,
|
||||
name: sample.title,
|
||||
})),
|
||||
},
|
||||
}).filter(
|
||||
(command) => kclSamples.length || command.name !== 'open-kcl-example'
|
||||
),
|
||||
[codeManager, kclManager, send, kclSamples]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
commandBarSend({ type: 'Add commands', data: { commands: kclCommandMemo } })
|
||||
commandBarActor.send({
|
||||
type: 'Add commands',
|
||||
data: { commands: kclCommandMemo },
|
||||
})
|
||||
|
||||
return () => {
|
||||
commandBarSend({
|
||||
commandBarActor.send({
|
||||
type: 'Remove commands',
|
||||
data: { commands: kclCommandMemo },
|
||||
})
|
||||
}
|
||||
}, [commandBarSend, kclCommandMemo])
|
||||
}, [commandBarActor.send, kclCommandMemo])
|
||||
|
||||
return (
|
||||
<FileContext.Provider
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { createContext, useEffect, useState } from 'react'
|
||||
|
||||
import { engineCommandManager } from 'lib/singletons'
|
||||
import { CommandsContext } from 'components/CommandBar/CommandBarProvider'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { components } from 'lib/machine-api'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { toSync } from 'lib/utils'
|
||||
import { commandBarActor } from 'machines/commandBarMachine'
|
||||
|
||||
export type MachinesListing = Array<
|
||||
components['schemas']['MachineInfoResponse']
|
||||
@ -42,8 +42,6 @@ export const MachineManagerProvider = ({
|
||||
components['schemas']['MachineInfoResponse'] | null
|
||||
>(null)
|
||||
|
||||
const commandBarActor = CommandsContext.useActorRef()
|
||||
|
||||
// Get the reason message for why there are no machines.
|
||||
const noMachinesReason = (): string | undefined => {
|
||||
if (machines.length > 0) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useMachine } from '@xstate/react'
|
||||
import { useMachine, useSelector } from '@xstate/react'
|
||||
import React, {
|
||||
createContext,
|
||||
useEffect,
|
||||
@ -11,6 +11,7 @@ import {
|
||||
AnyStateMachine,
|
||||
ContextFrom,
|
||||
Prop,
|
||||
SnapshotFrom,
|
||||
StateFrom,
|
||||
assign,
|
||||
fromPromise,
|
||||
@ -67,18 +68,14 @@ import {
|
||||
startSketchOnDefault,
|
||||
} from 'lang/modifyAst'
|
||||
import { PathToNode, Program, parse, recast, resultIsOk } from 'lang/wasm'
|
||||
import {
|
||||
artifactIsPlaneWithPaths,
|
||||
getNodePathFromSourceRange,
|
||||
isSingleCursorInPipe,
|
||||
} from 'lang/queryAst'
|
||||
import { artifactIsPlaneWithPaths, isSingleCursorInPipe } from 'lang/queryAst'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
import { exportFromEngine } from 'lib/exportFromEngine'
|
||||
import { Models } from '@kittycad/lib/dist/types/src'
|
||||
import toast from 'react-hot-toast'
|
||||
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
|
||||
import { err, reportRejection, trap } from 'lib/trap'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import {
|
||||
ExportIntent,
|
||||
EngineConnectionStateType,
|
||||
@ -91,6 +88,7 @@ import { IndexLoaderData } from 'lib/types'
|
||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
import { promptToEditFlow } from 'lib/promptToEdit'
|
||||
import { kclEditorActor } from 'machines/kclEditorMachine'
|
||||
import { commandBarActor } from 'machines/commandBarMachine'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
@ -102,6 +100,10 @@ export const ModelingMachineContext = createContext(
|
||||
{} as MachineContext<typeof modelingMachine>
|
||||
)
|
||||
|
||||
const commandBarIsClosedSelector = (
|
||||
state: SnapshotFrom<typeof commandBarActor>
|
||||
) => state.matches('Closed')
|
||||
|
||||
export const ModelingMachineProvider = ({
|
||||
children,
|
||||
}: {
|
||||
@ -132,8 +134,10 @@ export const ModelingMachineProvider = ({
|
||||
let [searchParams] = useSearchParams()
|
||||
const pool = searchParams.get('pool')
|
||||
|
||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||
|
||||
const isCommandBarClosed = useSelector(
|
||||
commandBarActor,
|
||||
commandBarIsClosedSelector
|
||||
)
|
||||
// Settings machine setup
|
||||
// const retrievedSettings = useRef(
|
||||
// localStorage?.getItem(MODELING_PERSIST_KEY) || '{}'
|
||||
@ -388,7 +392,16 @@ export const ModelingMachineProvider = ({
|
||||
}
|
||||
|
||||
if (setSelections.selectionType === 'completeSelection') {
|
||||
editorManager.selectRange(setSelections.selection)
|
||||
const codeMirrorSelection = editorManager.createEditorSelection(
|
||||
setSelections.selection
|
||||
)
|
||||
kclEditorActor.send({
|
||||
type: 'setLastSelectionEvent',
|
||||
data: {
|
||||
codeMirrorSelection,
|
||||
scrollIntoView: false,
|
||||
},
|
||||
})
|
||||
if (!sketchDetails)
|
||||
return {
|
||||
selectionRanges: setSelections.selection,
|
||||
@ -529,7 +542,6 @@ export const ModelingMachineProvider = ({
|
||||
trimmedPrompt,
|
||||
fileMachineSend,
|
||||
navigate,
|
||||
commandBarSend,
|
||||
context,
|
||||
token,
|
||||
settings: {
|
||||
@ -543,7 +555,7 @@ export const ModelingMachineProvider = ({
|
||||
'has valid selection for deletion': ({
|
||||
context: { selectionRanges },
|
||||
}) => {
|
||||
if (!commandBarState.matches('Closed')) return false
|
||||
if (!isCommandBarClosed) return false
|
||||
if (selectionRanges.graphSelections.length <= 0) return false
|
||||
return true
|
||||
},
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { DebugFeatureTree } from 'components/DebugFeatureTree'
|
||||
import { DebugArtifactGraph } from 'components/DebugArtifactGraph'
|
||||
import { AstExplorer } from '../../AstExplorer'
|
||||
import { EngineCommands } from '../../EngineCommands'
|
||||
import { CamDebugSettings } from 'clientSideScene/ClientSideSceneComp'
|
||||
@ -14,7 +14,7 @@ export const DebugPane = () => {
|
||||
<EngineCommands />
|
||||
<CamDebugSettings />
|
||||
<AstExplorer />
|
||||
<DebugFeatureTree />
|
||||
<DebugArtifactGraph />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
@ -3,6 +3,7 @@
|
||||
@apply font-mono !no-underline text-xs font-bold select-none text-chalkboard-90;
|
||||
@apply ui-active:bg-primary/10 ui-active:text-primary ui-active:text-inherit;
|
||||
@apply transition-colors ease-out;
|
||||
@apply m-0;
|
||||
}
|
||||
|
||||
:global(.dark) .button {
|
||||
|
@ -9,12 +9,11 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { kclManager } from 'lib/singletons'
|
||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { commandBarActor } from 'machines/commandBarMachine'
|
||||
|
||||
export const KclEditorMenu = ({ children }: PropsWithChildren) => {
|
||||
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
|
||||
useConvertToVariable()
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
@ -85,7 +84,7 @@ export const KclEditorMenu = ({ children }: PropsWithChildren) => {
|
||||
<Menu.Item>
|
||||
<button
|
||||
onClick={() => {
|
||||
commandBarSend({
|
||||
commandBarActor.send({
|
||||
type: 'Find and select command',
|
||||
data: {
|
||||
groupId: 'code',
|
||||
|
@ -95,9 +95,11 @@ export const processMemory = (programMemory: ProgramMemory) => {
|
||||
) {
|
||||
const sk = sketchFromKclValueOptional(val, key)
|
||||
if (val.type === 'Solid') {
|
||||
processedMemory[key] = val.value.map(({ ...rest }: ExtrudeSurface) => {
|
||||
return rest
|
||||
})
|
||||
processedMemory[key] = val.value.value.map(
|
||||
({ ...rest }: ExtrudeSurface) => {
|
||||
return rest
|
||||
}
|
||||
)
|
||||
} else if (!(sk instanceof Reason)) {
|
||||
processedMemory[key] = sk.paths.map(({ __geoMeta, ...rest }: Path) => {
|
||||
return rest
|
||||
|
@ -15,12 +15,12 @@ import { ModelingPane } from './ModelingPane'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { CustomIconName } from 'components/CustomIcon'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
|
||||
import { useKclContext } from 'lang/KclProvider'
|
||||
import { MachineManagerContext } from 'components/MachineManagerProvider'
|
||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||
import { SIDEBAR_BUTTON_SUFFIX } from 'lib/constants'
|
||||
import { commandBarActor } from 'machines/commandBarMachine'
|
||||
|
||||
interface ModelingSidebarProps {
|
||||
paneOpacity: '' | 'opacity-20' | 'opacity-40'
|
||||
@ -37,7 +37,6 @@ function getPlatformString(): 'web' | 'desktop' {
|
||||
|
||||
export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
||||
const machineManager = useContext(MachineManagerContext)
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const kclContext = useKclContext()
|
||||
const { settings } = useSettingsAuthContext()
|
||||
const onboardingStatus = settings.context.app.onboardingStatus
|
||||
@ -66,7 +65,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
||||
icon: 'floppyDiskArrow',
|
||||
keybinding: 'Ctrl + Shift + E',
|
||||
action: () =>
|
||||
commandBarSend({
|
||||
commandBarActor.send({
|
||||
type: 'Find and select command',
|
||||
data: { name: 'Export', groupId: 'modeling' },
|
||||
}),
|
||||
@ -79,7 +78,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
||||
keybinding: 'Ctrl + Shift + M',
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
action: async () => {
|
||||
commandBarSend({
|
||||
commandBarActor.send({
|
||||
type: 'Find and select command',
|
||||
data: { name: 'Make', groupId: 'modeling' },
|
||||
})
|
||||
@ -298,7 +297,7 @@ function ModelingPaneButton({
|
||||
})
|
||||
|
||||
return (
|
||||
<div id={paneConfig.id + '-button-holder'}>
|
||||
<div id={paneConfig.id + '-button-holder'} className="relative">
|
||||
<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"
|
||||
onClick={onClick}
|
||||
@ -340,7 +339,7 @@ function ModelingPaneButton({
|
||||
<p
|
||||
id={`${paneConfig.id}-badge`}
|
||||
className={
|
||||
'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'
|
||||
'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'
|
||||
}
|
||||
onClick={showBadge.onClick}
|
||||
title={`Click to view ${showBadge.value} notification${
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
|
||||
import { CommandBarProvider } from './CommandBar/CommandBarProvider'
|
||||
import {
|
||||
NETWORK_HEALTH_TEXT,
|
||||
NetworkHealthIndicator,
|
||||
@ -12,9 +11,7 @@ function TestWrap({ children }: { children: React.ReactNode }) {
|
||||
// wrap in router and xState context
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<CommandBarProvider>
|
||||
<SettingsAuthProviderJest>{children}</SettingsAuthProviderJest>
|
||||
</CommandBarProvider>
|
||||
<SettingsAuthProviderJest>{children}</SettingsAuthProviderJest>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
|
68
src/components/OpenInDesktopAppHandler.test.tsx
Normal file
@ -0,0 +1,68 @@
|
||||
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()
|
||||
})
|
||||
})
|
125
src/components/OpenInDesktopAppHandler.tsx
Normal file
@ -0,0 +1,125 @@
|
||||
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
|
||||
)
|
||||
}
|
@ -2,7 +2,6 @@ import { render, screen } from '@testing-library/react'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
||||
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
|
||||
import { CommandBarProvider } from './CommandBar/CommandBarProvider'
|
||||
import { Project } from 'lib/project'
|
||||
|
||||
const now = new Date()
|
||||
@ -33,11 +32,9 @@ describe('ProjectSidebarMenu tests', () => {
|
||||
test('Disables popover menu by default', () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<CommandBarProvider>
|
||||
<SettingsAuthProviderJest>
|
||||
<ProjectSidebarMenu project={projectWellFormed} />
|
||||
</SettingsAuthProviderJest>
|
||||
</CommandBarProvider>
|
||||
<SettingsAuthProviderJest>
|
||||
<ProjectSidebarMenu project={projectWellFormed} />
|
||||
</SettingsAuthProviderJest>
|
||||
</BrowserRouter>
|
||||
)
|
||||
|
||||
|
@ -7,14 +7,19 @@ import { Link, useLocation, useNavigate } from 'react-router-dom'
|
||||
import { Fragment, useMemo, useContext } from 'react'
|
||||
import { Logo } from './Logo'
|
||||
import { APP_NAME } from 'lib/constants'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { CustomIcon } from './CustomIcon'
|
||||
import { useLspContext } from './LspProvider'
|
||||
import { engineCommandManager, kclManager } from 'lib/singletons'
|
||||
import { codeManager, engineCommandManager, kclManager } from 'lib/singletons'
|
||||
import { MachineManagerContext } from 'components/MachineManagerProvider'
|
||||
import usePlatform from 'hooks/usePlatform'
|
||||
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
||||
import Tooltip from './Tooltip'
|
||||
import { SnapshotFrom } from 'xstate'
|
||||
import { commandBarActor } from 'machines/commandBarMachine'
|
||||
import { useSelector } from '@xstate/react'
|
||||
import { copyFileShareLink } from 'lib/links'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { DEV } from 'env'
|
||||
|
||||
const ProjectSidebarMenu = ({
|
||||
project,
|
||||
@ -84,6 +89,9 @@ function AppLogoLink({
|
||||
)
|
||||
}
|
||||
|
||||
const commandsSelector = (state: SnapshotFrom<typeof commandBarActor>) =>
|
||||
state.context.commands
|
||||
|
||||
function ProjectMenuPopover({
|
||||
project,
|
||||
file,
|
||||
@ -95,17 +103,16 @@ function ProjectMenuPopover({
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const filePath = useAbsoluteFilePath()
|
||||
const { settings, auth } = useSettingsAuthContext()
|
||||
const machineManager = useContext(MachineManagerContext)
|
||||
const commands = useSelector(commandBarActor, commandsSelector)
|
||||
|
||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||
const { onProjectClose } = useLspContext()
|
||||
const exportCommandInfo = { name: 'Export', groupId: 'modeling' }
|
||||
const makeCommandInfo = { name: 'Make', groupId: 'modeling' }
|
||||
const findCommand = (obj: { name: string; groupId: string }) =>
|
||||
Boolean(
|
||||
commandBarState.context.commands.find(
|
||||
(c) => c.name === obj.name && c.groupId === obj.groupId
|
||||
)
|
||||
commands.find((c) => c.name === obj.name && c.groupId === obj.groupId)
|
||||
)
|
||||
const machineCount = machineManager.machines.length
|
||||
|
||||
@ -150,12 +157,11 @@ function ProjectMenuPopover({
|
||||
),
|
||||
disabled: !findCommand(exportCommandInfo),
|
||||
onClick: () =>
|
||||
commandBarSend({
|
||||
commandBarActor.send({
|
||||
type: 'Find and select command',
|
||||
data: exportCommandInfo,
|
||||
}),
|
||||
},
|
||||
'break',
|
||||
{
|
||||
id: 'make',
|
||||
Element: 'button',
|
||||
@ -175,12 +181,26 @@ function ProjectMenuPopover({
|
||||
),
|
||||
disabled: !findCommand(makeCommandInfo) || machineCount === 0,
|
||||
onClick: () => {
|
||||
commandBarSend({
|
||||
commandBarActor.send({
|
||||
type: 'Find and select command',
|
||||
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',
|
||||
{
|
||||
id: 'go-home',
|
||||
@ -200,7 +220,7 @@ function ProjectMenuPopover({
|
||||
[
|
||||
platform,
|
||||
findCommand,
|
||||
commandBarSend,
|
||||
commandBarActor.send,
|
||||
engineCommandManager,
|
||||
onProjectClose,
|
||||
isDesktop,
|
||||
|
@ -1,13 +1,12 @@
|
||||
import { useMachine } from '@xstate/react'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
||||
import { useProjectsLoader } from 'hooks/useProjectsLoader'
|
||||
import { projectsMachine } from 'machines/projectsMachine'
|
||||
import { createContext, useEffect, useState } from 'react'
|
||||
import { createContext, useCallback, useEffect, useState } from 'react'
|
||||
import { Actor, AnyStateMachine, fromPromise, Prop, StateFrom } from 'xstate'
|
||||
import { useLspContext } from './LspProvider'
|
||||
import toast from 'react-hot-toast'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { PATHS } from 'lib/paths'
|
||||
import {
|
||||
createNewProjectDirectory,
|
||||
@ -19,11 +18,28 @@ import {
|
||||
interpolateProjectNameWithIndex,
|
||||
doesProjectNameNeedInterpolated,
|
||||
getUniqueProjectName,
|
||||
getNextFileName,
|
||||
} from 'lib/desktopFS'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import useStateMachineCommands from 'hooks/useStateMachineCommands'
|
||||
import { projectsCommandBarConfig } from 'lib/commandBarConfigs/projectsCommandConfig'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { commandBarActor } from 'machines/commandBarMachine'
|
||||
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> = {
|
||||
state?: StateFrom<T>
|
||||
@ -53,12 +69,110 @@ 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 [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 (
|
||||
<ProjectsMachineContext.Provider
|
||||
value={{
|
||||
state: undefined,
|
||||
send: () => {},
|
||||
state,
|
||||
send,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
@ -73,19 +187,21 @@ const ProjectsContextDesktop = ({
|
||||
}) => {
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
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 { onProjectOpen } = useLspContext()
|
||||
const {
|
||||
settings: { context: settings },
|
||||
} = useSettingsAuthContext()
|
||||
|
||||
useEffect(() => {
|
||||
console.log(
|
||||
'project directory changed',
|
||||
settings.app.projectDirectory.current
|
||||
)
|
||||
}, [settings.app.projectDirectory.current])
|
||||
|
||||
const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0)
|
||||
const { projectPaths, projectsDir } = useProjectsLoader([
|
||||
projectsLoaderTrigger,
|
||||
@ -126,7 +242,7 @@ const ProjectsContextDesktop = ({
|
||||
},
|
||||
null
|
||||
)
|
||||
commandBarSend({ type: 'Close' })
|
||||
commandBarActor.send({ type: 'Close' })
|
||||
const newPathName = `${PATHS.FILE}/${encodeURIComponent(
|
||||
projectPath
|
||||
)}`
|
||||
@ -169,6 +285,31 @@ 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 }) =>
|
||||
toast.success(
|
||||
('data' in event && typeof event.data === 'string' && event.data) ||
|
||||
@ -201,7 +342,7 @@ const ProjectsContextDesktop = ({
|
||||
|
||||
return {
|
||||
message: `Successfully created "${uniqueName}"`,
|
||||
name,
|
||||
name: uniqueName,
|
||||
}
|
||||
}),
|
||||
renameProject: fromPromise(async ({ input }) => {
|
||||
@ -218,8 +359,6 @@ const ProjectsContextDesktop = ({
|
||||
name = interpolateProjectNameWithIndex(name, nextIndex)
|
||||
}
|
||||
|
||||
console.log('from Project')
|
||||
|
||||
await renameProjectDirectory(
|
||||
window.electron.path.join(defaultDirectory, oldName),
|
||||
name
|
||||
@ -242,13 +381,82 @@ const ProjectsContextDesktop = ({
|
||||
name: input.name,
|
||||
}
|
||||
}),
|
||||
},
|
||||
guards: {
|
||||
'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
|
||||
},
|
||||
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: {
|
||||
base_unit: input.units,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}),
|
||||
},
|
||||
}),
|
||||
{
|
||||
@ -271,6 +479,7 @@ const ProjectsContextDesktop = ({
|
||||
state,
|
||||
commandBarConfig: projectsCommandBarConfig,
|
||||
actor,
|
||||
onCancel: clearImportSearchParams,
|
||||
})
|
||||
|
||||
return (
|
||||
|
@ -29,7 +29,6 @@ import {
|
||||
createSettingsCommand,
|
||||
settingsWithCommandConfigs,
|
||||
} from 'lib/commandBarConfigs/settingsCommandConfig'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { Command } from 'lib/commandTypes'
|
||||
import { BaseUnit } from 'lib/settings/settingsTypes'
|
||||
import {
|
||||
@ -42,6 +41,7 @@ import { isDesktop } from 'lib/isDesktop'
|
||||
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
||||
import { codeManager } from 'lib/singletons'
|
||||
import { createRouteCommands } from 'lib/commandBarConfigs/routeCommandConfig'
|
||||
import { commandBarActor } from 'machines/commandBarMachine'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
@ -109,7 +109,6 @@ export const SettingsAuthProviderBase = ({
|
||||
}) => {
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const [settingsPath, setSettingsPath] = useState<string | undefined>(
|
||||
undefined
|
||||
)
|
||||
@ -278,10 +277,10 @@ export const SettingsAuthProviderBase = ({
|
||||
)
|
||||
.filter((c) => c !== null) as Command[]
|
||||
|
||||
commandBarSend({ type: 'Add commands', data: { commands: commands } })
|
||||
commandBarActor.send({ type: 'Add commands', data: { commands: commands } })
|
||||
|
||||
return () => {
|
||||
commandBarSend({
|
||||
commandBarActor.send({
|
||||
type: 'Remove commands',
|
||||
data: { commands },
|
||||
})
|
||||
@ -290,7 +289,7 @@ export const SettingsAuthProviderBase = ({
|
||||
settingsState,
|
||||
settingsSend,
|
||||
settingsActor,
|
||||
commandBarSend,
|
||||
commandBarActor.send,
|
||||
settingsWithCommandConfigs,
|
||||
])
|
||||
|
||||
@ -303,7 +302,7 @@ export const SettingsAuthProviderBase = ({
|
||||
encodeURIComponent(loadedProject?.file?.path || BROWSER_PATH)
|
||||
const { RouteTelemetryCommand, RouteHomeCommand, RouteSettingsCommand } =
|
||||
createRouteCommands(navigate, location, filePath)
|
||||
commandBarSend({
|
||||
commandBarActor.send({
|
||||
type: 'Remove commands',
|
||||
data: {
|
||||
commands: [
|
||||
@ -314,12 +313,12 @@ export const SettingsAuthProviderBase = ({
|
||||
},
|
||||
})
|
||||
if (location.pathname === PATHS.HOME) {
|
||||
commandBarSend({
|
||||
commandBarActor.send({
|
||||
type: 'Add commands',
|
||||
data: { commands: [RouteTelemetryCommand, RouteSettingsCommand] },
|
||||
})
|
||||
} else if (location.pathname.includes(PATHS.FILE)) {
|
||||
commandBarSend({
|
||||
commandBarActor.send({
|
||||
type: 'Add commands',
|
||||
data: {
|
||||
commands: [
|
||||
|
@ -17,10 +17,11 @@ import {
|
||||
import { useRouteLoaderData } from 'react-router-dom'
|
||||
import { PATHS } from 'lib/paths'
|
||||
import { IndexLoaderData } from 'lib/types'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { err, reportRejection } from 'lib/trap'
|
||||
import { getArtifactOfTypes } from 'lang/std/artifactGraph'
|
||||
import { ViewControlContextMenu } from './ViewControlMenu'
|
||||
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||
import { useSelector } from '@xstate/react'
|
||||
|
||||
enum StreamState {
|
||||
Playing = 'playing',
|
||||
@ -35,7 +36,7 @@ export const Stream = () => {
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
const { settings } = useSettingsAuthContext()
|
||||
const { state, send } = useModelingContext()
|
||||
const { commandBarState } = useCommandsContext()
|
||||
const commandBarState = useCommandBarState()
|
||||
const { mediaStream } = useAppStream()
|
||||
const { overallState, immediateState } = useNetworkContext()
|
||||
const [streamState, setStreamState] = useState(StreamState.Unset)
|
||||
|
@ -28,7 +28,7 @@ import { base64Decode } from 'lang/wasm'
|
||||
import { sendTelemetry } from 'lib/textToCad'
|
||||
import { Themes } from 'lib/theme'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import { commandBarMachine } from 'machines/commandBarMachine'
|
||||
import { commandBarActor, commandBarMachine } from 'machines/commandBarMachine'
|
||||
import { EventFrom } from 'xstate'
|
||||
import { fileMachine } from 'machines/fileMachine'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
@ -43,15 +43,10 @@ export function ToastTextToCadError({
|
||||
toastId,
|
||||
message,
|
||||
prompt,
|
||||
commandBarSend,
|
||||
}: {
|
||||
toastId: string
|
||||
message: string
|
||||
prompt: string
|
||||
commandBarSend: (
|
||||
event: EventFrom<typeof commandBarMachine>,
|
||||
data?: unknown
|
||||
) => void
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col justify-between gap-6">
|
||||
@ -81,7 +76,7 @@ export function ToastTextToCadError({
|
||||
}}
|
||||
name="Edit prompt"
|
||||
onClick={() => {
|
||||
commandBarSend({
|
||||
commandBarActor.send({
|
||||
type: 'Find and select command',
|
||||
data: {
|
||||
groupId: 'modeling',
|
||||
|
@ -1,10 +1,8 @@
|
||||
import { toolTips } from 'lang/langHelpers'
|
||||
import { Selections } from 'lib/selections'
|
||||
import { Program, Expr, VariableDeclarator } from '../../lang/wasm'
|
||||
import {
|
||||
getNodePathFromSourceRange,
|
||||
getNodeFromPath,
|
||||
} from '../../lang/queryAst'
|
||||
import { getNodeFromPath } from '../../lang/queryAst'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints'
|
||||
import {
|
||||
transformSecondarySketchLinesTagFirst,
|
||||
|
@ -8,7 +8,6 @@ import {
|
||||
} from 'react-router-dom'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
|
||||
import { CommandBarProvider } from './CommandBar/CommandBarProvider'
|
||||
|
||||
type User = Models['User_type']
|
||||
|
||||
@ -124,9 +123,7 @@ function TestWrap({ children }: { children: React.ReactNode }) {
|
||||
<Route
|
||||
path="/file/:id"
|
||||
element={
|
||||
<CommandBarProvider>
|
||||
<SettingsAuthProviderJest>{children}</SettingsAuthProviderJest>
|
||||
</CommandBarProvider>
|
||||
<SettingsAuthProviderJest>{children}</SettingsAuthProviderJest>
|
||||
}
|
||||
/>
|
||||
),
|
||||
|
@ -5,7 +5,6 @@ import { engineCommandManager, kclManager } from 'lib/singletons'
|
||||
import { modelingMachine, ModelingMachineEvent } from 'machines/modelingMachine'
|
||||
import { Selections, Selection, processCodeMirrorRanges } from 'lib/selections'
|
||||
import { undo, redo } from '@codemirror/commands'
|
||||
import { CommandBarMachineEvent } from 'machines/commandBarMachine'
|
||||
import { addLineHighlight, addLineHighlightEvent } from './highlightextension'
|
||||
import {
|
||||
Diagnostic,
|
||||
@ -52,9 +51,6 @@ export default class EditorManager {
|
||||
private _modelingSend: (eventInfo: ModelingMachineEvent) => void = () => {}
|
||||
private _modelingState: StateFrom<typeof modelingMachine> | null = null
|
||||
|
||||
private _commandBarSend: (eventInfo: CommandBarMachineEvent) => void =
|
||||
() => {}
|
||||
|
||||
private _convertToVariableEnabled: boolean = false
|
||||
private _convertToVariableCallback: () => void = () => {}
|
||||
|
||||
@ -161,14 +157,6 @@ export default class EditorManager {
|
||||
this._modelingState = state
|
||||
}
|
||||
|
||||
setCommandBarSend(send: (eventInfo: CommandBarMachineEvent) => void) {
|
||||
this._commandBarSend = send
|
||||
}
|
||||
|
||||
commandBarSend(eventInfo: CommandBarMachineEvent): void {
|
||||
return this._commandBarSend(eventInfo)
|
||||
}
|
||||
|
||||
get highlightRange(): Array<[number, number]> {
|
||||
return this._highlightRange
|
||||
}
|
||||
@ -315,6 +303,21 @@ export default class EditorManager {
|
||||
if (selections?.graphSelections?.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!this._editorView) {
|
||||
return
|
||||
}
|
||||
const codeBaseSelections = this.createEditorSelection(selections)
|
||||
this._editorView.dispatch({
|
||||
selection: codeBaseSelections,
|
||||
annotations: [
|
||||
updateOutsideEditorEvent,
|
||||
Transaction.addToHistory.of(false),
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
createEditorSelection(selections: Selections) {
|
||||
let codeBasedSelections = []
|
||||
for (const selection of selections.graphSelections) {
|
||||
const safeEnd = Math.min(
|
||||
@ -331,18 +334,7 @@ export default class EditorManager {
|
||||
.range[1]
|
||||
const safeEnd = Math.min(end, this._editorView?.state.doc.length || end)
|
||||
codeBasedSelections.push(EditorSelection.cursor(safeEnd))
|
||||
|
||||
if (!this._editorView) {
|
||||
return
|
||||
}
|
||||
|
||||
this._editorView.dispatch({
|
||||
selection: EditorSelection.create(codeBasedSelections, 1),
|
||||
annotations: [
|
||||
updateOutsideEditorEvent,
|
||||
Transaction.addToHistory.of(false),
|
||||
],
|
||||
})
|
||||
return EditorSelection.create(codeBasedSelections, 1)
|
||||
}
|
||||
|
||||
// We will ONLY get here if the user called a select event.
|
||||
|
@ -1,10 +0,0 @@
|
||||
import { CommandsContext } from 'components/CommandBar/CommandBarProvider'
|
||||
|
||||
export const useCommandsContext = () => {
|
||||
const commandBarActor = CommandsContext.useActorRef()
|
||||
const commandBarState = CommandsContext.useSelector((state) => state)
|
||||
return {
|
||||
commandBarSend: commandBarActor.send,
|
||||
commandBarState,
|
||||
}
|
||||
}
|
65
src/hooks/useCreateFileLinkQueryWatcher.ts
Normal file
@ -0,0 +1,65 @@
|
||||
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])
|
||||
}
|
@ -17,7 +17,8 @@ import {
|
||||
} from 'lang/std/artifactGraph'
|
||||
import { err, reportRejection } from 'lib/trap'
|
||||
import { DefaultPlaneStr, getFaceDetails } from 'clientSideScene/sceneEntities'
|
||||
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
||||
import { getNodeFromPath } from 'lang/queryAst'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
import { CallExpression, defaultSourceRange } from 'lang/wasm'
|
||||
import { EdgeCutInfo, ExtrudeFacePlane } from 'machines/modelingMachine'
|
||||
|
||||
|
@ -14,7 +14,7 @@ export const useProjectsLoader = (deps?: [number]) => {
|
||||
|
||||
useEffect(() => {
|
||||
// Useless on web, until we get fake filesystems over there.
|
||||
if (!isDesktop) return
|
||||
if (!isDesktop()) return
|
||||
|
||||
if (deps && deps[0] === lastTs) return
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { useEffect } from 'react'
|
||||
import { AnyStateMachine, Actor, StateFrom, EventFrom } from 'xstate'
|
||||
import { createMachineCommand } from '../lib/createMachineCommand'
|
||||
import { useCommandsContext } from './useCommandsContext'
|
||||
import { modelingMachine } from 'machines/modelingMachine'
|
||||
import { authMachine } from 'machines/authMachine'
|
||||
import { settingsMachine } from 'machines/settingsMachine'
|
||||
@ -15,6 +14,7 @@ import { useKclContext } from 'lang/KclProvider'
|
||||
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||
import { useAppState } from 'AppState'
|
||||
import { commandBarActor } from 'machines/commandBarMachine'
|
||||
|
||||
// This might not be necessary, AnyStateMachine from xstate is working
|
||||
export type AllMachines =
|
||||
@ -48,7 +48,6 @@ export default function useStateMachineCommands<
|
||||
allCommandsRequireNetwork = false,
|
||||
onCancel,
|
||||
}: UseStateMachineCommandsArgs<T, S>) {
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const { overallState } = useNetworkContext()
|
||||
const { isExecuting } = useKclContext()
|
||||
const { isStreamReady } = useAppState()
|
||||
@ -76,10 +75,13 @@ export default function useStateMachineCommands<
|
||||
})
|
||||
.filter((c) => c !== null) as Command[] // TS isn't smart enough to know this filter removes nulls
|
||||
|
||||
commandBarSend({ type: 'Add commands', data: { commands: newCommands } })
|
||||
commandBarActor.send({
|
||||
type: 'Add commands',
|
||||
data: { commands: newCommands },
|
||||
})
|
||||
|
||||
return () => {
|
||||
commandBarSend({
|
||||
commandBarActor.send({
|
||||
type: 'Remove commands',
|
||||
data: { commands: newCommands },
|
||||
})
|
||||
|
@ -322,6 +322,7 @@ export class KclManager {
|
||||
await this.ensureWasmInit()
|
||||
const { logs, errors, execState, isInterrupted } = await executeAst({
|
||||
ast,
|
||||
path: codeManager.currentFilePath || undefined,
|
||||
engineCommandManager: this.engineCommandManager,
|
||||
})
|
||||
|
||||
|
@ -24,7 +24,10 @@ describe('testing AST', () => {
|
||||
type: 'Literal',
|
||||
start: 0,
|
||||
end: 1,
|
||||
value: 5,
|
||||
value: {
|
||||
suffix: 'None',
|
||||
value: 5,
|
||||
},
|
||||
raw: '5',
|
||||
},
|
||||
operator: '+',
|
||||
@ -32,7 +35,10 @@ describe('testing AST', () => {
|
||||
type: 'Literal',
|
||||
start: 3,
|
||||
end: 4,
|
||||
value: 6,
|
||||
value: {
|
||||
suffix: 'None',
|
||||
value: 6,
|
||||
},
|
||||
raw: '6',
|
||||
},
|
||||
},
|
||||
|
@ -54,6 +54,9 @@ const mySketch001 = startSketchOn('XY')
|
||||
},
|
||||
],
|
||||
id: expect.any(String),
|
||||
units: {
|
||||
type: 'Mm',
|
||||
},
|
||||
__meta: [{ sourceRange: [46, 71, 0] }],
|
||||
},
|
||||
})
|
||||
@ -72,56 +75,65 @@ const mySketch001 = startSketchOn('XY')
|
||||
const sketch001 = execState.memory.get('mySketch001')
|
||||
expect(sketch001).toEqual({
|
||||
type: 'Solid',
|
||||
id: expect.any(String),
|
||||
value: [
|
||||
{
|
||||
type: 'extrudePlane',
|
||||
faceId: expect.any(String),
|
||||
tag: null,
|
||||
id: expect.any(String),
|
||||
sourceRange: [77, 102, 0],
|
||||
},
|
||||
{
|
||||
type: 'extrudePlane',
|
||||
faceId: expect.any(String),
|
||||
tag: null,
|
||||
id: expect.any(String),
|
||||
sourceRange: [108, 132, 0],
|
||||
},
|
||||
],
|
||||
sketch: {
|
||||
value: {
|
||||
type: 'Solid',
|
||||
id: expect.any(String),
|
||||
__meta: expect.any(Array),
|
||||
on: expect.any(Object),
|
||||
start: expect.any(Object),
|
||||
type: 'Sketch',
|
||||
paths: [
|
||||
value: [
|
||||
{
|
||||
type: 'ToPoint',
|
||||
from: [0, 0],
|
||||
to: [-1.59, -1.54],
|
||||
type: 'extrudePlane',
|
||||
faceId: expect.any(String),
|
||||
tag: null,
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [77, 102, 0],
|
||||
},
|
||||
id: expect.any(String),
|
||||
sourceRange: [77, 102, 0],
|
||||
},
|
||||
{
|
||||
type: 'ToPoint',
|
||||
from: [-1.59, -1.54],
|
||||
to: [0.46, -5.82],
|
||||
type: 'extrudePlane',
|
||||
faceId: expect.any(String),
|
||||
tag: null,
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [108, 132, 0],
|
||||
},
|
||||
id: expect.any(String),
|
||||
sourceRange: [108, 132, 0],
|
||||
},
|
||||
],
|
||||
sketch: {
|
||||
id: expect.any(String),
|
||||
units: {
|
||||
type: 'Mm',
|
||||
},
|
||||
__meta: expect.any(Array),
|
||||
on: expect.any(Object),
|
||||
start: expect.any(Object),
|
||||
type: 'Sketch',
|
||||
paths: [
|
||||
{
|
||||
type: 'ToPoint',
|
||||
from: [0, 0],
|
||||
to: [-1.59, -1.54],
|
||||
tag: null,
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [77, 102, 0],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'ToPoint',
|
||||
from: [-1.59, -1.54],
|
||||
to: [0.46, -5.82],
|
||||
tag: null,
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [108, 132, 0],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
height: 2,
|
||||
startCapId: expect.any(String),
|
||||
endCapId: expect.any(String),
|
||||
units: {
|
||||
type: 'Mm',
|
||||
},
|
||||
__meta: [{ sourceRange: [46, 71, 0] }],
|
||||
},
|
||||
height: 2,
|
||||
startCapId: expect.any(String),
|
||||
endCapId: expect.any(String),
|
||||
__meta: [{ sourceRange: [46, 71, 0] }],
|
||||
})
|
||||
})
|
||||
test('sketch extrude and sketch on one of the faces', async () => {
|
||||
@ -154,187 +166,205 @@ const sk2 = startSketchOn('XY')
|
||||
expect(geos).toEqual([
|
||||
{
|
||||
type: 'Solid',
|
||||
id: expect.any(String),
|
||||
value: [
|
||||
{
|
||||
type: 'extrudePlane',
|
||||
faceId: expect.any(String),
|
||||
tag: null,
|
||||
id: expect.any(String),
|
||||
sourceRange: [69, 89, 0],
|
||||
},
|
||||
{
|
||||
type: 'extrudePlane',
|
||||
faceId: expect.any(String),
|
||||
tag: {
|
||||
end: 116,
|
||||
start: 114,
|
||||
type: 'TagDeclarator',
|
||||
value: 'p',
|
||||
},
|
||||
id: expect.any(String),
|
||||
sourceRange: [95, 117, 0],
|
||||
},
|
||||
{
|
||||
type: 'extrudePlane',
|
||||
faceId: expect.any(String),
|
||||
tag: null,
|
||||
id: expect.any(String),
|
||||
sourceRange: [123, 142, 0],
|
||||
},
|
||||
],
|
||||
sketch: {
|
||||
value: {
|
||||
type: 'Solid',
|
||||
id: expect.any(String),
|
||||
__meta: expect.any(Array),
|
||||
on: expect.any(Object),
|
||||
start: expect.any(Object),
|
||||
type: 'Sketch',
|
||||
tags: {
|
||||
p: {
|
||||
__meta: [
|
||||
{
|
||||
sourceRange: [114, 116, 0],
|
||||
},
|
||||
],
|
||||
type: 'TagIdentifier',
|
||||
value: 'p',
|
||||
info: expect.any(Object),
|
||||
},
|
||||
},
|
||||
paths: [
|
||||
value: [
|
||||
{
|
||||
type: 'ToPoint',
|
||||
from: [0, 0],
|
||||
to: [-2.5, 0],
|
||||
type: 'extrudePlane',
|
||||
faceId: expect.any(String),
|
||||
tag: null,
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [69, 89, 0],
|
||||
},
|
||||
id: expect.any(String),
|
||||
sourceRange: [69, 89, 0],
|
||||
},
|
||||
{
|
||||
type: 'ToPoint',
|
||||
from: [-2.5, 0],
|
||||
to: [0, 10],
|
||||
type: 'extrudePlane',
|
||||
faceId: expect.any(String),
|
||||
tag: {
|
||||
end: 116,
|
||||
start: 114,
|
||||
type: 'TagDeclarator',
|
||||
value: 'p',
|
||||
},
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [95, 117, 0],
|
||||
},
|
||||
id: expect.any(String),
|
||||
sourceRange: [95, 117, 0],
|
||||
},
|
||||
{
|
||||
type: 'ToPoint',
|
||||
from: [0, 10],
|
||||
to: [2.5, 0],
|
||||
type: 'extrudePlane',
|
||||
faceId: expect.any(String),
|
||||
tag: null,
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [123, 142, 0],
|
||||
},
|
||||
id: expect.any(String),
|
||||
sourceRange: [123, 142, 0],
|
||||
},
|
||||
],
|
||||
sketch: {
|
||||
id: expect.any(String),
|
||||
__meta: expect.any(Array),
|
||||
on: expect.any(Object),
|
||||
start: expect.any(Object),
|
||||
type: 'Sketch',
|
||||
units: {
|
||||
type: 'Mm',
|
||||
},
|
||||
tags: {
|
||||
p: {
|
||||
__meta: [
|
||||
{
|
||||
sourceRange: [114, 116, 0],
|
||||
},
|
||||
],
|
||||
type: 'TagIdentifier',
|
||||
value: 'p',
|
||||
info: expect.any(Object),
|
||||
},
|
||||
},
|
||||
paths: [
|
||||
{
|
||||
type: 'ToPoint',
|
||||
from: [0, 0],
|
||||
to: [-2.5, 0],
|
||||
tag: null,
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [69, 89, 0],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'ToPoint',
|
||||
from: [-2.5, 0],
|
||||
to: [0, 10],
|
||||
tag: {
|
||||
end: 116,
|
||||
start: 114,
|
||||
type: 'TagDeclarator',
|
||||
value: 'p',
|
||||
},
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [95, 117, 0],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'ToPoint',
|
||||
from: [0, 10],
|
||||
to: [2.5, 0],
|
||||
tag: null,
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [123, 142, 0],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
height: 2,
|
||||
startCapId: expect.any(String),
|
||||
endCapId: expect.any(String),
|
||||
units: {
|
||||
type: 'Mm',
|
||||
},
|
||||
__meta: [{ sourceRange: [38, 63, 0] }],
|
||||
},
|
||||
height: 2,
|
||||
startCapId: expect.any(String),
|
||||
endCapId: expect.any(String),
|
||||
__meta: [{ sourceRange: [38, 63, 0] }],
|
||||
},
|
||||
{
|
||||
type: 'Solid',
|
||||
id: expect.any(String),
|
||||
value: [
|
||||
{
|
||||
type: 'extrudePlane',
|
||||
faceId: expect.any(String),
|
||||
tag: null,
|
||||
id: expect.any(String),
|
||||
sourceRange: [373, 393, 0],
|
||||
},
|
||||
{
|
||||
type: 'extrudePlane',
|
||||
faceId: expect.any(String),
|
||||
tag: {
|
||||
end: 419,
|
||||
start: 417,
|
||||
type: 'TagDeclarator',
|
||||
value: 'o',
|
||||
},
|
||||
id: expect.any(String),
|
||||
sourceRange: [399, 420, 0],
|
||||
},
|
||||
{
|
||||
type: 'extrudePlane',
|
||||
faceId: expect.any(String),
|
||||
tag: null,
|
||||
id: expect.any(String),
|
||||
sourceRange: [426, 445, 0],
|
||||
},
|
||||
],
|
||||
sketch: {
|
||||
value: {
|
||||
type: 'Solid',
|
||||
id: expect.any(String),
|
||||
__meta: expect.any(Array),
|
||||
on: expect.any(Object),
|
||||
start: expect.any(Object),
|
||||
type: 'Sketch',
|
||||
tags: {
|
||||
o: {
|
||||
__meta: [
|
||||
{
|
||||
sourceRange: [417, 419, 0],
|
||||
},
|
||||
],
|
||||
type: 'TagIdentifier',
|
||||
value: 'o',
|
||||
info: expect.any(Object),
|
||||
},
|
||||
},
|
||||
paths: [
|
||||
value: [
|
||||
{
|
||||
type: 'ToPoint',
|
||||
from: [0, 0],
|
||||
to: [-2.5, 0],
|
||||
type: 'extrudePlane',
|
||||
faceId: expect.any(String),
|
||||
tag: null,
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [373, 393, 0],
|
||||
},
|
||||
id: expect.any(String),
|
||||
sourceRange: [373, 393, 0],
|
||||
},
|
||||
{
|
||||
type: 'ToPoint',
|
||||
from: [-2.5, 0],
|
||||
to: [0, 3],
|
||||
type: 'extrudePlane',
|
||||
faceId: expect.any(String),
|
||||
tag: {
|
||||
end: 419,
|
||||
start: 417,
|
||||
type: 'TagDeclarator',
|
||||
value: 'o',
|
||||
},
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [399, 420, 0],
|
||||
},
|
||||
id: expect.any(String),
|
||||
sourceRange: [399, 420, 0],
|
||||
},
|
||||
{
|
||||
type: 'ToPoint',
|
||||
from: [0, 3],
|
||||
to: [2.5, 0],
|
||||
type: 'extrudePlane',
|
||||
faceId: expect.any(String),
|
||||
tag: null,
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [426, 445, 0],
|
||||
},
|
||||
id: expect.any(String),
|
||||
sourceRange: [426, 445, 0],
|
||||
},
|
||||
],
|
||||
sketch: {
|
||||
id: expect.any(String),
|
||||
units: {
|
||||
type: 'Mm',
|
||||
},
|
||||
__meta: expect.any(Array),
|
||||
on: expect.any(Object),
|
||||
start: expect.any(Object),
|
||||
type: 'Sketch',
|
||||
tags: {
|
||||
o: {
|
||||
__meta: [
|
||||
{
|
||||
sourceRange: [417, 419, 0],
|
||||
},
|
||||
],
|
||||
type: 'TagIdentifier',
|
||||
value: 'o',
|
||||
info: expect.any(Object),
|
||||
},
|
||||
},
|
||||
paths: [
|
||||
{
|
||||
type: 'ToPoint',
|
||||
from: [0, 0],
|
||||
to: [-2.5, 0],
|
||||
tag: null,
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [373, 393, 0],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'ToPoint',
|
||||
from: [-2.5, 0],
|
||||
to: [0, 3],
|
||||
tag: {
|
||||
end: 419,
|
||||
start: 417,
|
||||
type: 'TagDeclarator',
|
||||
value: 'o',
|
||||
},
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [399, 420, 0],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'ToPoint',
|
||||
from: [0, 3],
|
||||
to: [2.5, 0],
|
||||
tag: null,
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [426, 445, 0],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
height: 2,
|
||||
startCapId: expect.any(String),
|
||||
endCapId: expect.any(String),
|
||||
__meta: [{ sourceRange: [342, 367, 0] }],
|
||||
units: {
|
||||
type: 'Mm',
|
||||
},
|
||||
},
|
||||
height: 2,
|
||||
startCapId: expect.any(String),
|
||||
endCapId: expect.any(String),
|
||||
__meta: [{ sourceRange: [342, 367, 0] }],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
@ -80,6 +80,10 @@ export default class CodeManager {
|
||||
}))
|
||||
}
|
||||
|
||||
get currentFilePath(): string | null {
|
||||
return this._currentFilePath
|
||||
}
|
||||
|
||||
updateCurrentFilePath(path: string) {
|
||||
this._currentFilePath = path
|
||||
}
|
||||
|
@ -221,6 +221,9 @@ const newVar = myVar + 1`
|
||||
},
|
||||
],
|
||||
id: expect.any(String),
|
||||
units: {
|
||||
type: 'Mm',
|
||||
},
|
||||
__meta: [{ sourceRange: [39, 63, 0] }],
|
||||
},
|
||||
})
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { getNodePathFromSourceRange, getNodeFromPath } from './queryAst'
|
||||
import { getNodeFromPath } from './queryAst'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
import {
|
||||
Identifier,
|
||||
assertParse,
|
||||
|
@ -52,27 +52,22 @@ afterAll(async () => {
|
||||
} catch (e) {}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.chdir('..')
|
||||
})
|
||||
|
||||
// The tests have to be sequential because we need to change directories
|
||||
// to support `import` working properly.
|
||||
// @ts-expect-error
|
||||
describe.sequential('Test KCL Samples from public Github repository', () => {
|
||||
// @ts-expect-error
|
||||
describe.sequential('when performing enginelessExecutor', () => {
|
||||
describe('Test KCL Samples from public Github repository', () => {
|
||||
describe('when performing enginelessExecutor', () => {
|
||||
manifest.forEach((file: KclSampleFile) => {
|
||||
// @ts-expect-error
|
||||
it.sequential(
|
||||
it(
|
||||
`should execute ${file.title} (${file.file}) successfully`,
|
||||
async () => {
|
||||
const [dirProject, fileKcl] =
|
||||
file.pathFromProjectDirectoryToFirstFile.split('/')
|
||||
process.chdir(dirProject)
|
||||
const code = await fs.readFile(fileKcl, 'utf-8')
|
||||
const code = await fs.readFile(
|
||||
file.pathFromProjectDirectoryToFirstFile,
|
||||
'utf-8'
|
||||
)
|
||||
const ast = assertParse(code)
|
||||
await enginelessExecutor(ast, programMemoryInit())
|
||||
await enginelessExecutor(
|
||||
ast,
|
||||
programMemoryInit(),
|
||||
file.pathFromProjectDirectoryToFirstFile
|
||||
)
|
||||
},
|
||||
files.length * 1000
|
||||
)
|
||||
|
@ -46,12 +46,14 @@ export const toolTips: Array<ToolTip> = [
|
||||
|
||||
export async function executeAst({
|
||||
ast,
|
||||
path,
|
||||
engineCommandManager,
|
||||
// If you set programMemoryOverride we assume you mean mock mode. Since that
|
||||
// is the only way to go about it.
|
||||
programMemoryOverride,
|
||||
}: {
|
||||
ast: Node<Program>
|
||||
path?: string
|
||||
engineCommandManager: EngineCommandManager
|
||||
programMemoryOverride?: ProgramMemory
|
||||
isInterrupted?: boolean
|
||||
@ -63,8 +65,8 @@ export async function executeAst({
|
||||
}> {
|
||||
try {
|
||||
const execState = await (programMemoryOverride
|
||||
? enginelessExecutor(ast, programMemoryOverride)
|
||||
: executor(ast, engineCommandManager))
|
||||
? enginelessExecutor(ast, programMemoryOverride, path)
|
||||
: executor(ast, engineCommandManager, path))
|
||||
|
||||
await engineCommandManager.waitForAllCommands()
|
||||
|
||||
|
@ -25,7 +25,8 @@ import {
|
||||
deleteFromSelection,
|
||||
} from './modifyAst'
|
||||
import { enginelessExecutor } from '../lib/testHelpers'
|
||||
import { findUsesOfTagInPipe, getNodePathFromSourceRange } from './queryAst'
|
||||
import { findUsesOfTagInPipe } from './queryAst'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
import { err } from 'lib/trap'
|
||||
import { SimplifiedArgDetails } from './std/stdTypes'
|
||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
@ -39,7 +40,7 @@ describe('Testing createLiteral', () => {
|
||||
it('should create a literal', () => {
|
||||
const result = createLiteral(5)
|
||||
expect(result.type).toBe('Literal')
|
||||
expect(result.value).toBe(5)
|
||||
expect((result as any).value.value).toBe(5)
|
||||
})
|
||||
})
|
||||
describe('Testing createIdentifier', () => {
|
||||
@ -56,7 +57,7 @@ describe('Testing createCallExpression', () => {
|
||||
expect(result.callee.type).toBe('Identifier')
|
||||
expect(result.callee.name).toBe('myFunc')
|
||||
expect(result.arguments[0].type).toBe('Literal')
|
||||
expect((result.arguments[0] as any).value).toBe(5)
|
||||
expect((result.arguments[0] as any).value.value).toBe(5)
|
||||
})
|
||||
})
|
||||
describe('Testing createObjectExpression', () => {
|
||||
@ -68,7 +69,7 @@ describe('Testing createObjectExpression', () => {
|
||||
expect(result.properties[0].type).toBe('ObjectProperty')
|
||||
expect(result.properties[0].key.name).toBe('myProp')
|
||||
expect(result.properties[0].value.type).toBe('Literal')
|
||||
expect((result.properties[0].value as any).value).toBe(5)
|
||||
expect((result.properties[0].value as any).value.value).toBe(5)
|
||||
})
|
||||
})
|
||||
describe('Testing createArrayExpression', () => {
|
||||
@ -76,7 +77,7 @@ describe('Testing createArrayExpression', () => {
|
||||
const result = createArrayExpression([createLiteral(5)])
|
||||
expect(result.type).toBe('ArrayExpression')
|
||||
expect(result.elements[0].type).toBe('Literal')
|
||||
expect((result.elements[0] as any).value).toBe(5)
|
||||
expect((result.elements[0] as any).value.value).toBe(5)
|
||||
})
|
||||
})
|
||||
describe('Testing createPipeSubstitution', () => {
|
||||
@ -93,7 +94,7 @@ describe('Testing createVariableDeclaration', () => {
|
||||
expect(result.declaration.id.type).toBe('Identifier')
|
||||
expect(result.declaration.id.name).toBe('myVar')
|
||||
expect(result.declaration.init.type).toBe('Literal')
|
||||
expect((result.declaration.init as any).value).toBe(5)
|
||||
expect((result.declaration.init as any).value.value).toBe(5)
|
||||
})
|
||||
})
|
||||
describe('Testing createPipeExpression', () => {
|
||||
@ -101,7 +102,7 @@ describe('Testing createPipeExpression', () => {
|
||||
const result = createPipeExpression([createLiteral(5)])
|
||||
expect(result.type).toBe('PipeExpression')
|
||||
expect(result.body[0].type).toBe('Literal')
|
||||
expect((result.body[0] as any).value).toBe(5)
|
||||
expect((result.body[0] as any).value.value).toBe(5)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -26,10 +26,10 @@ import {
|
||||
findAllPreviousVariables,
|
||||
findAllPreviousVariablesPath,
|
||||
getNodeFromPath,
|
||||
getNodePathFromSourceRange,
|
||||
isNodeSafeToReplace,
|
||||
traverse,
|
||||
} from './queryAst'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
import { addTagForSketchOnFace, getConstraintInfo } from './std/sketch'
|
||||
import {
|
||||
PathToNodeMap,
|
||||
@ -743,14 +743,18 @@ export function splitPathAtPipeExpression(pathToNode: PathToNode): {
|
||||
return splitPathAtPipeExpression(pathToNode.slice(0, -1))
|
||||
}
|
||||
|
||||
export function createLiteral(value: LiteralValue): Node<Literal> {
|
||||
export function createLiteral(value: LiteralValue | number): Node<Literal> {
|
||||
const raw = `${value}`
|
||||
if (typeof value === 'number') {
|
||||
value = { value, suffix: 'None' }
|
||||
}
|
||||
return {
|
||||
type: 'Literal',
|
||||
start: 0,
|
||||
end: 0,
|
||||
moduleId: 0,
|
||||
value,
|
||||
raw: `${value}`,
|
||||
raw,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,7 +21,8 @@ import {
|
||||
ChamferParameters,
|
||||
EdgeTreatmentParameters,
|
||||
} from './addEdgeTreatment'
|
||||
import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst'
|
||||
import { getNodeFromPath } from '../queryAst'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
import { createLiteral } from 'lang/modifyAst'
|
||||
import { err } from 'lib/trap'
|
||||
import { Selection, Selections } from 'lib/selections'
|
||||
|
@ -20,10 +20,10 @@ import {
|
||||
} from '../modifyAst'
|
||||
import {
|
||||
getNodeFromPath,
|
||||
getNodePathFromSourceRange,
|
||||
hasSketchPipeBeenExtruded,
|
||||
traverse,
|
||||
} from '../queryAst'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
import {
|
||||
addTagForSketchOnFace,
|
||||
getTagFromCallExpression,
|
||||
|
@ -19,7 +19,8 @@ import {
|
||||
findUniqueName,
|
||||
createVariableDeclaration,
|
||||
} from 'lang/modifyAst'
|
||||
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
||||
import { getNodeFromPath } from 'lang/queryAst'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
import {
|
||||
mutateAstWithTagForSketchSegment,
|
||||
getEdgeTagCall,
|
||||
|
@ -10,7 +10,6 @@ import {
|
||||
findAllPreviousVariables,
|
||||
isNodeSafeToReplace,
|
||||
isTypeInValue,
|
||||
getNodePathFromSourceRange,
|
||||
hasExtrudeSketch,
|
||||
findUsesOfTagInPipe,
|
||||
hasSketchPipeBeenExtruded,
|
||||
@ -19,6 +18,7 @@ import {
|
||||
getNodeFromPath,
|
||||
doesSceneHaveExtrudedSketch,
|
||||
} from './queryAst'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
import { enginelessExecutor } from '../lib/testHelpers'
|
||||
import {
|
||||
createArrayExpression,
|
||||
@ -660,7 +660,7 @@ myNestedVar = [
|
||||
enter: (node, path) => {
|
||||
if (
|
||||
node.type === 'Literal' &&
|
||||
String(node.value) === literalOfInterest
|
||||
String((node as any).value.value) === literalOfInterest
|
||||
) {
|
||||
pathToNode = path
|
||||
} else if (
|
||||
|
@ -22,6 +22,7 @@ import {
|
||||
VariableDeclaration,
|
||||
VariableDeclarator,
|
||||
} from './wasm'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
import { createIdentifier, splitPathAtLastIndex } from './modifyAst'
|
||||
import { getSketchSegmentFromSourceRange } from './std/sketchConstraints'
|
||||
import { getAngle } from '../lib/utils'
|
||||
@ -125,311 +126,6 @@ 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<
|
||||
| Expr
|
||||
| ExpressionStatement
|
||||
@ -717,16 +413,6 @@ function isTypeInArrayExp(
|
||||
return node.elements.some((el) => isTypeInValue(el, syntaxType))
|
||||
}
|
||||
|
||||
export function isValueZero(val?: Expr): boolean {
|
||||
return (
|
||||
(val?.type === 'Literal' && Number(val.value) === 0) ||
|
||||
(val?.type === 'UnaryExpression' &&
|
||||
val.operator === '-' &&
|
||||
val.argument.type === 'Literal' &&
|
||||
Number(val.argument.value) === 0)
|
||||
)
|
||||
}
|
||||
|
||||
export function isLinesParallelAndConstrained(
|
||||
ast: Program,
|
||||
artifactGraph: ArtifactGraph,
|
||||
|
316
src/lang/queryAstNodePathUtils.ts
Normal file
@ -0,0 +1,316 @@
|
||||
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
|
||||
}
|
@ -16,7 +16,7 @@ import {
|
||||
EdgeCut,
|
||||
} from 'lang/wasm'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAst'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
import { err } from 'lib/trap'
|
||||
|
||||
export type { Artifact, ArtifactId, SegmentArtifact } from 'lang/wasm'
|
||||
|
@ -1014,6 +1014,11 @@ class EngineConnection extends EventTarget {
|
||||
this.pingPongSpan.pong = new Date()
|
||||
break
|
||||
|
||||
case 'modeling_session_data':
|
||||
let api_call_id = resp.data?.session?.api_call_id
|
||||
console.log(`API Call ID: ${api_call_id}`)
|
||||
break
|
||||
|
||||
// Only fires on successful authentication.
|
||||
case 'ice_server_info':
|
||||
let ice_servers = resp.data?.ice_servers
|
||||
|
@ -31,6 +31,9 @@ class FileSystemManager {
|
||||
}
|
||||
|
||||
async join(dir: string, path: string): Promise<string> {
|
||||
if (path.startsWith(dir)) {
|
||||
path = path.slice(dir.length)
|
||||
}
|
||||
return Promise.resolve(window.electron.path.join(dir, path))
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,8 @@ import {
|
||||
CallExpression,
|
||||
topLevelRange,
|
||||
} from '../wasm'
|
||||
import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst'
|
||||
import { getNodeFromPath } from '../queryAst'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
import { enginelessExecutor } from '../../lib/testHelpers'
|
||||
import { err } from 'lib/trap'
|
||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
|
@ -17,9 +17,9 @@ import {
|
||||
import {
|
||||
getNodeFromPath,
|
||||
getNodeFromPathCurry,
|
||||
getNodePathFromSourceRange,
|
||||
getObjExprProperty,
|
||||
} from 'lang/queryAst'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
import {
|
||||
isLiteralArrayOrStatic,
|
||||
isNotLiteralArrayOrStatic,
|
||||
|
@ -20,13 +20,10 @@ import {
|
||||
sketchFromKclValue,
|
||||
Literal,
|
||||
SourceRange,
|
||||
LiteralValue,
|
||||
} from '../wasm'
|
||||
import {
|
||||
getNodeFromPath,
|
||||
getNodeFromPathCurry,
|
||||
getNodePathFromSourceRange,
|
||||
isValueZero,
|
||||
} from '../queryAst'
|
||||
import { getNodeFromPath, getNodeFromPathCurry } from '../queryAst'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
import {
|
||||
createArrayExpression,
|
||||
createBinaryExpression,
|
||||
@ -79,11 +76,32 @@ export type ConstraintType =
|
||||
| 'setAngleBetween'
|
||||
|
||||
const REF_NUM_ERR = new Error('Referenced segment does not have a to value')
|
||||
|
||||
function asNum(val: LiteralValue): number | Error {
|
||||
if (typeof val === 'object') return val.value
|
||||
return REF_NUM_ERR
|
||||
}
|
||||
|
||||
function forceNum(arg: Literal): number {
|
||||
if (typeof arg.value === 'boolean' || typeof arg.value === 'string') {
|
||||
return Number(arg.value)
|
||||
} else {
|
||||
return arg.value.value
|
||||
}
|
||||
}
|
||||
|
||||
function isUndef(val: any): val is undefined {
|
||||
return typeof val === 'undefined'
|
||||
}
|
||||
function isNum(val: any): val is number {
|
||||
return typeof val === 'number'
|
||||
|
||||
function isValueZero(val?: Expr): boolean {
|
||||
return (
|
||||
(val?.type === 'Literal' && forceNum(val) === 0) ||
|
||||
(val?.type === 'UnaryExpression' &&
|
||||
val.operator === '-' &&
|
||||
val.argument.type === 'Literal' &&
|
||||
Number(val.argument.value) === 0)
|
||||
)
|
||||
}
|
||||
|
||||
function createCallWrapper(
|
||||
@ -190,7 +208,7 @@ const xyLineSetLength =
|
||||
: referenceSeg
|
||||
? segRef
|
||||
: args[0].expr
|
||||
const literalARg = getArgLiteralVal(args[0].expr)
|
||||
const literalARg = asNum(args[0].expr.value)
|
||||
if (err(literalARg)) return literalARg
|
||||
return createCallWrapper(xOrY, lineVal, tag, literalARg)
|
||||
}
|
||||
@ -211,13 +229,14 @@ const basicAngledLineCreateNode =
|
||||
referencedSegment: path,
|
||||
}) => {
|
||||
const refAng = path ? getAngle(path?.from, path?.to) : 0
|
||||
if (!isNum(args[0].expr.value)) return REF_NUM_ERR
|
||||
const argValue = asNum(args[0].expr.value)
|
||||
if (err(argValue)) return argValue
|
||||
const nonForcedAng =
|
||||
varValToUse === 'ang'
|
||||
? inputs[0].expr
|
||||
: referenceSeg === 'ang'
|
||||
? getClosesAngleDirection(
|
||||
args[0].expr.value,
|
||||
argValue,
|
||||
refAng,
|
||||
createSegAngle(referenceSegName)
|
||||
)
|
||||
@ -230,8 +249,8 @@ const basicAngledLineCreateNode =
|
||||
: args[1].expr
|
||||
const shouldForceAng = valToForce === 'ang' && forceValueUsedInTransform
|
||||
const shouldForceLen = valToForce === 'len' && forceValueUsedInTransform
|
||||
const literalArg = getArgLiteralVal(
|
||||
valToForce === 'ang' ? args[0].expr : args[1].expr
|
||||
const literalArg = asNum(
|
||||
valToForce === 'ang' ? args[0].expr.value : args[1].expr.value
|
||||
)
|
||||
if (err(literalArg)) return literalArg
|
||||
return createCallWrapper(
|
||||
@ -283,7 +302,7 @@ const getMinAndSegAngVals = (
|
||||
}
|
||||
|
||||
const getSignedLeg = (arg: Literal, legLenVal: BinaryPart) =>
|
||||
Number(arg.value) < 0 ? createUnaryExpression(legLenVal) : legLenVal
|
||||
forceNum(arg) < 0 ? createUnaryExpression(legLenVal) : legLenVal
|
||||
|
||||
const getLegAng = (ang: number, legAngleVal: BinaryPart) => {
|
||||
const normalisedAngle = ((ang % 360) + 360) % 360 // between 0 and 360
|
||||
@ -322,8 +341,7 @@ const setHorzVertDistanceCreateNode =
|
||||
referencedSegment,
|
||||
}) => {
|
||||
const refNum = referencedSegment?.to?.[index]
|
||||
const literalArg = getArgLiteralVal(args?.[index].expr)
|
||||
if (err(literalArg)) return literalArg
|
||||
const literalArg = asNum(args?.[index].expr.value)
|
||||
if (isUndef(refNum) || err(literalArg)) return REF_NUM_ERR
|
||||
|
||||
const valueUsedInTransform = roundOff(literalArg - refNum, 2)
|
||||
@ -352,7 +370,7 @@ const setHorzVertDistanceForAngleLineCreateNode =
|
||||
referencedSegment,
|
||||
}) => {
|
||||
const refNum = referencedSegment?.to?.[index]
|
||||
const literalArg = getArgLiteralVal(args?.[1].expr)
|
||||
const literalArg = asNum(args?.[1].expr.value)
|
||||
if (isUndef(refNum) || err(literalArg)) return REF_NUM_ERR
|
||||
const valueUsedInTransform = roundOff(literalArg - refNum, 2)
|
||||
const binExp = createBinaryExpressionWithUnary([
|
||||
@ -374,8 +392,8 @@ const setAbsDistanceCreateNode =
|
||||
index = xOrY === 'x' ? 0 : 1
|
||||
): CreateStdLibSketchCallExpr =>
|
||||
({ tag, forceValueUsedInTransform, rawArgs: args }) => {
|
||||
const literalArg = getArgLiteralVal(args?.[index].expr)
|
||||
if (err(literalArg)) return REF_NUM_ERR
|
||||
const literalArg = asNum(args?.[index].expr.value)
|
||||
if (err(literalArg)) return literalArg
|
||||
const valueUsedInTransform = roundOff(literalArg, 2)
|
||||
const val = forceValueUsedInTransform || createLiteral(valueUsedInTransform)
|
||||
if (isXOrYLine) {
|
||||
@ -396,8 +414,8 @@ const setAbsDistanceCreateNode =
|
||||
const setAbsDistanceForAngleLineCreateNode =
|
||||
(xOrY: 'x' | 'y'): CreateStdLibSketchCallExpr =>
|
||||
({ tag, forceValueUsedInTransform, inputs, rawArgs: args }) => {
|
||||
const literalArg = getArgLiteralVal(args?.[1].expr)
|
||||
if (err(literalArg)) return REF_NUM_ERR
|
||||
const literalArg = asNum(args?.[1].expr.value)
|
||||
if (err(literalArg)) return literalArg
|
||||
const valueUsedInTransform = roundOff(literalArg, 2)
|
||||
const val = forceValueUsedInTransform || createLiteral(valueUsedInTransform)
|
||||
return createCallWrapper(
|
||||
@ -419,7 +437,7 @@ const setHorVertDistanceForXYLines =
|
||||
}) => {
|
||||
const index = xOrY === 'x' ? 0 : 1
|
||||
const refNum = referencedSegment?.to?.[index]
|
||||
const literalArg = getArgLiteralVal(args?.[index].expr)
|
||||
const literalArg = asNum(args?.[index].expr.value)
|
||||
if (isUndef(refNum) || err(literalArg)) return REF_NUM_ERR
|
||||
const valueUsedInTransform = roundOff(literalArg - refNum, 2)
|
||||
const makeBinExp = createBinaryExpressionWithUnary([
|
||||
@ -445,9 +463,9 @@ const setHorzVertDistanceConstraintLineCreateNode =
|
||||
])
|
||||
|
||||
const makeBinExp = (index: 0 | 1) => {
|
||||
const arg = getArgLiteralVal(args?.[index].expr)
|
||||
const arg = asNum(args?.[index].expr.value)
|
||||
const refNum = referencedSegment?.to?.[index]
|
||||
if (err(arg) || !isNum(refNum)) return REF_NUM_ERR
|
||||
if (err(arg) || isUndef(refNum)) return REF_NUM_ERR
|
||||
return createBinaryExpressionWithUnary([
|
||||
createSegEnd(referenceSegName, isX),
|
||||
createLiteral(roundOff(arg - refNum, 2)),
|
||||
@ -468,9 +486,9 @@ const setAngledIntersectLineForLines: CreateStdLibSketchCallExpr = ({
|
||||
forceValueUsedInTransform,
|
||||
rawArgs: args,
|
||||
}) => {
|
||||
const val = args[1].expr.value,
|
||||
angle = args[0].expr.value
|
||||
if (!isNum(val) || !isNum(angle)) return REF_NUM_ERR
|
||||
const val = asNum(args[1].expr.value),
|
||||
angle = asNum(args[0].expr.value)
|
||||
if (err(val) || err(angle)) return REF_NUM_ERR
|
||||
const valueUsedInTransform = roundOff(val, 2)
|
||||
const varNamMap: { [key: number]: string } = {
|
||||
0: 'ZERO',
|
||||
@ -498,8 +516,8 @@ const setAngledIntersectForAngledLines: CreateStdLibSketchCallExpr = ({
|
||||
inputs,
|
||||
rawArgs: args,
|
||||
}) => {
|
||||
const val = args[1].expr.value
|
||||
if (!isNum(val)) return REF_NUM_ERR
|
||||
const val = asNum(args[1].expr.value)
|
||||
if (err(val)) return val
|
||||
const valueUsedInTransform = roundOff(val, 2)
|
||||
return intersectCallWrapper({
|
||||
fnName: 'angledLineThatIntersects',
|
||||
@ -524,8 +542,8 @@ const setAngleBetweenCreateNode =
|
||||
const refAngle = referencedSegment
|
||||
? getAngle(referencedSegment?.from, referencedSegment?.to)
|
||||
: 0
|
||||
const val = args[0].expr.value
|
||||
if (!isNum(val)) return REF_NUM_ERR
|
||||
const val = asNum(args[0].expr.value)
|
||||
if (err(val)) return val
|
||||
let valueUsedInTransform = roundOff(normaliseAngle(val - refAngle))
|
||||
let firstHalfValue = createSegAngle(referenceSegName)
|
||||
if (Math.abs(valueUsedInTransform) > 90) {
|
||||
@ -706,13 +724,11 @@ const transformMap: TransformMap = {
|
||||
createPipeSubstitution(),
|
||||
]
|
||||
)
|
||||
if (!isNum(args[0].expr.value)) return REF_NUM_ERR
|
||||
const val = asNum(args[0].expr.value)
|
||||
if (err(val)) return val
|
||||
return createCallWrapper(
|
||||
'angledLineToX',
|
||||
[
|
||||
getAngleLengthSign(args[0].expr.value, angleToMatchLengthXCall),
|
||||
inputs[0].expr,
|
||||
],
|
||||
[getAngleLengthSign(val, angleToMatchLengthXCall), inputs[0].expr],
|
||||
tag
|
||||
)
|
||||
},
|
||||
@ -739,13 +755,11 @@ const transformMap: TransformMap = {
|
||||
createPipeSubstitution(),
|
||||
]
|
||||
)
|
||||
if (!isNum(args[0].expr.value)) return REF_NUM_ERR
|
||||
const val = asNum(args[0].expr.value)
|
||||
if (err(val)) return val
|
||||
return createCallWrapper(
|
||||
'angledLineToY',
|
||||
[
|
||||
getAngleLengthSign(args[0].expr.value, angleToMatchLengthYCall),
|
||||
inputs[1].expr,
|
||||
],
|
||||
[getAngleLengthSign(val, angleToMatchLengthYCall), inputs[1].expr],
|
||||
tag
|
||||
)
|
||||
},
|
||||
@ -763,7 +777,7 @@ const transformMap: TransformMap = {
|
||||
forceValueUsedInTransform,
|
||||
rawArgs: args,
|
||||
}) => {
|
||||
const val = getArgLiteralVal(args[0].expr)
|
||||
const val = asNum(args[0].expr.value)
|
||||
if (err(val)) return val
|
||||
return createCallWrapper(
|
||||
'angledLineToY',
|
||||
@ -844,7 +858,7 @@ const transformMap: TransformMap = {
|
||||
tooltip: 'yLine',
|
||||
createNode: ({ inputs, tag, rawArgs: args }) => {
|
||||
const expr = inputs[1].expr
|
||||
if (Number(args[0].expr.value) >= 0)
|
||||
if (forceNum(args[0].expr) >= 0)
|
||||
return createCallWrapper('yLine', expr, tag)
|
||||
if (isExprBinaryPart(expr))
|
||||
return createCallWrapper('yLine', createUnaryExpression(expr), tag)
|
||||
@ -856,7 +870,7 @@ const transformMap: TransformMap = {
|
||||
tooltip: 'xLine',
|
||||
createNode: ({ inputs, tag, rawArgs: args }) => {
|
||||
const expr = inputs[1].expr
|
||||
if (Number(args[0].expr.value) >= 0)
|
||||
if (forceNum(args[0].expr) >= 0)
|
||||
return createCallWrapper('xLine', expr, tag)
|
||||
if (isExprBinaryPart(expr))
|
||||
return createCallWrapper('xLine', createUnaryExpression(expr), tag)
|
||||
@ -900,10 +914,11 @@ const transformMap: TransformMap = {
|
||||
referenceSegName,
|
||||
getInputOfType(inputs, 'xRelative').expr
|
||||
)
|
||||
if (!isNum(args[0].expr.value)) return REF_NUM_ERR
|
||||
const val = asNum(args[0].expr.value)
|
||||
if (err(val)) return val
|
||||
return createCallWrapper(
|
||||
'angledLineOfXLength',
|
||||
[getLegAng(args[0].expr.value, legAngle), minVal],
|
||||
[getLegAng(val, legAngle), minVal],
|
||||
tag
|
||||
)
|
||||
},
|
||||
@ -912,7 +927,7 @@ const transformMap: TransformMap = {
|
||||
tooltip: 'xLine',
|
||||
createNode: ({ inputs, tag, rawArgs: args }) => {
|
||||
const expr = inputs[1].expr
|
||||
if (Number(args[0].expr.value) >= 0)
|
||||
if (forceNum(args[0].expr) >= 0)
|
||||
return createCallWrapper('xLine', expr, tag)
|
||||
if (isExprBinaryPart(expr))
|
||||
return createCallWrapper('xLine', createUnaryExpression(expr), tag)
|
||||
@ -953,10 +968,11 @@ const transformMap: TransformMap = {
|
||||
inputs[1].expr,
|
||||
'legAngY'
|
||||
)
|
||||
if (!isNum(args[0].expr.value)) return REF_NUM_ERR
|
||||
const val = asNum(args[0].expr.value)
|
||||
if (err(val)) return val
|
||||
return createCallWrapper(
|
||||
'angledLineOfXLength',
|
||||
[getLegAng(args[0].expr.value, legAngle), minVal],
|
||||
[getLegAng(val, legAngle), minVal],
|
||||
tag
|
||||
)
|
||||
},
|
||||
@ -965,7 +981,7 @@ const transformMap: TransformMap = {
|
||||
tooltip: 'yLine',
|
||||
createNode: ({ inputs, tag, rawArgs: args }) => {
|
||||
const expr = inputs[1].expr
|
||||
if (Number(args[0].expr.value) >= 0)
|
||||
if (forceNum(args[0].expr) >= 0)
|
||||
return createCallWrapper('yLine', expr, tag)
|
||||
if (isExprBinaryPart(expr))
|
||||
return createCallWrapper('yLine', createUnaryExpression(expr), tag)
|
||||
@ -1005,13 +1021,11 @@ const transformMap: TransformMap = {
|
||||
createPipeSubstitution(),
|
||||
]
|
||||
)
|
||||
if (!isNum(args[0].expr.value)) return REF_NUM_ERR
|
||||
const val = asNum(args[0].expr.value)
|
||||
if (err(val)) return val
|
||||
return createCallWrapper(
|
||||
'angledLineToX',
|
||||
[
|
||||
getAngleLengthSign(args[0].expr.value, angleToMatchLengthXCall),
|
||||
inputs[1].expr,
|
||||
],
|
||||
[getAngleLengthSign(val, angleToMatchLengthXCall), inputs[1].expr],
|
||||
tag
|
||||
)
|
||||
},
|
||||
@ -1057,13 +1071,11 @@ const transformMap: TransformMap = {
|
||||
createPipeSubstitution(),
|
||||
]
|
||||
)
|
||||
if (!isNum(args[0].expr.value)) return REF_NUM_ERR
|
||||
const val = asNum(args[0].expr.value)
|
||||
if (err(val)) return val
|
||||
return createCallWrapper(
|
||||
'angledLineToY',
|
||||
[
|
||||
getAngleLengthSign(args[0].expr.value, angleToMatchLengthXCall),
|
||||
inputs[1].expr,
|
||||
],
|
||||
[getAngleLengthSign(val, angleToMatchLengthXCall), inputs[1].expr],
|
||||
tag
|
||||
)
|
||||
},
|
||||
@ -1080,7 +1092,7 @@ const transformMap: TransformMap = {
|
||||
equalLength: {
|
||||
tooltip: 'xLine',
|
||||
createNode: ({ referenceSegName, tag, rawArgs: args }) => {
|
||||
const argVal = getArgLiteralVal(args[0].expr)
|
||||
const argVal = asNum(args[0].expr.value)
|
||||
if (err(argVal)) return argVal
|
||||
const segLen = createSegLen(referenceSegName)
|
||||
if (argVal > 0) return createCallWrapper('xLine', segLen, tag, argVal)
|
||||
@ -1118,7 +1130,7 @@ const transformMap: TransformMap = {
|
||||
equalLength: {
|
||||
tooltip: 'yLine',
|
||||
createNode: ({ referenceSegName, tag, rawArgs: args }) => {
|
||||
const argVal = getArgLiteralVal(args[0].expr)
|
||||
const argVal = asNum(args[0].expr.value)
|
||||
if (err(argVal)) return argVal
|
||||
let segLen = createSegLen(referenceSegName)
|
||||
if (argVal < 0) segLen = createUnaryExpression(segLen)
|
||||
@ -1714,7 +1726,7 @@ export function transformAstSketchLines({
|
||||
let kclVal = programMemory.get(varName)
|
||||
let sketch
|
||||
if (kclVal?.type === 'Solid') {
|
||||
sketch = kclVal.sketch
|
||||
sketch = kclVal.value.sketch
|
||||
} else {
|
||||
sketch = sketchFromKclValue(kclVal, varName)
|
||||
if (err(sketch)) {
|
||||
@ -1823,11 +1835,6 @@ function createLastSeg(isX: boolean): Node<CallExpression> {
|
||||
])
|
||||
}
|
||||
|
||||
function getArgLiteralVal(arg: Literal): number | Error {
|
||||
if (!isNum(arg.value)) return REF_NUM_ERR
|
||||
return arg.value
|
||||
}
|
||||
|
||||
export type ConstraintLevel = 'free' | 'partial' | 'full'
|
||||
|
||||
export function getConstraintLevelFromSourceRange(
|
||||
|
@ -53,7 +53,7 @@ import { ArtifactId } 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 { Artifact } from './std/artifactGraph'
|
||||
import { getNodePathFromSourceRange } from './queryAst'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
|
||||
export type { Artifact } from 'wasm-lib/kcl/bindings/Artifact'
|
||||
export type { ArtifactCommand } from 'wasm-lib/kcl/bindings/Artifact'
|
||||
@ -539,7 +539,8 @@ export function sketchFromKclValueOptional(
|
||||
): Sketch | Reason {
|
||||
if (obj?.value?.type === 'Sketch') return obj.value
|
||||
if (obj?.value?.type === 'Solid') return obj.value.sketch
|
||||
if (obj?.type === 'Solid') return obj.sketch
|
||||
if (obj?.type === 'Sketch') return obj.value
|
||||
if (obj?.type === 'Solid') return obj.value.sketch
|
||||
if (!varName) {
|
||||
varName = 'a KCL value'
|
||||
}
|
||||
@ -565,9 +566,19 @@ export function sketchFromKclValue(
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a KCL program.
|
||||
* @param node The AST of the program to execute.
|
||||
* @param path The full path of the file being executed. Use `null` for
|
||||
* expressions that don't have a file, like expressions in the command bar.
|
||||
* @param programMemoryOverride If this is not `null`, this will be used as the
|
||||
* initial program memory, and the execution will be engineless (AKA mock
|
||||
* execution).
|
||||
*/
|
||||
export const executor = async (
|
||||
node: Node<Program>,
|
||||
engineCommandManager: EngineCommandManager,
|
||||
path?: string,
|
||||
programMemoryOverride: ProgramMemory | Error | null = null
|
||||
): Promise<ExecState> => {
|
||||
if (programMemoryOverride !== null && err(programMemoryOverride))
|
||||
@ -589,6 +600,7 @@ export const executor = async (
|
||||
}
|
||||
const execOutcome: RustExecOutcome = await execute(
|
||||
JSON.stringify(node),
|
||||
path,
|
||||
JSON.stringify(programMemoryOverride?.toRaw() || null),
|
||||
JSON.stringify({ settings: jsAppSettings }),
|
||||
engineCommandManager,
|
||||
|
40
src/lib/base64.test.ts
Normal file
@ -0,0 +1,40 @@
|
||||
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)
|
||||
})
|
||||
})
|