Compare commits
19 Commits
revert-510
...
nightly-v2
Author | SHA1 | Date | |
---|---|---|---|
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}}
|
|
||||||
|
|
20425
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 |
|
| `revolutions` |`number`| Number of revolutions. | No |
|
||||||
| `angleStart` |`number`| Start angle (in degrees). | No |
|
| `angleStart` |`number`| Start angle (in degrees). | No |
|
||||||
| `ccw` |`boolean`| Is the helix rotation counter clockwise? | No |
|
| `ccw` |`boolean`| Is the helix rotation counter clockwise? | No |
|
||||||
|
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A helix. | No |
|
||||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ A helix.
|
|||||||
| `revolutions` |`number`| Number of revolutions. | No |
|
| `revolutions` |`number`| Number of revolutions. | No |
|
||||||
| `angleStart` |`number`| Start angle (in degrees). | No |
|
| `angleStart` |`number`| Start angle (in degrees). | No |
|
||||||
| `ccw` |`boolean`| Is the helix rotation counter clockwise? | No |
|
| `ccw` |`boolean`| Is the helix rotation counter clockwise? | No |
|
||||||
|
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A helix. | No |
|
||||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||||
|
|
||||||
|
|
||||||
|
@ -168,7 +168,6 @@ Any KCL value.
|
|||||||
|
|
||||||
|
|
||||||
----
|
----
|
||||||
A plane.
|
|
||||||
|
|
||||||
**Type:** `object`
|
**Type:** `object`
|
||||||
|
|
||||||
@ -181,17 +180,10 @@ A plane.
|
|||||||
| Property | Type | Description | Required |
|
| Property | Type | Description | Required |
|
||||||
|----------|------|-------------|----------|
|
|----------|------|-------------|----------|
|
||||||
| `type` |enum: [`Plane`](/docs/kcl/types/Plane)| | No |
|
| `type` |enum: [`Plane`](/docs/kcl/types/Plane)| | No |
|
||||||
| `id` |`string`| The id of the plane. | No |
|
| `value` |[`Plane`](/docs/kcl/types/Plane)| Any KCL value. | 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 |
|
|
||||||
|
|
||||||
|
|
||||||
----
|
----
|
||||||
A face.
|
|
||||||
|
|
||||||
**Type:** `object`
|
**Type:** `object`
|
||||||
|
|
||||||
@ -203,14 +195,8 @@ A face.
|
|||||||
|
|
||||||
| Property | Type | Description | Required |
|
| Property | Type | Description | Required |
|
||||||
|----------|------|-------------|----------|
|
|----------|------|-------------|----------|
|
||||||
| `type` |enum: `Face`| | No |
|
| `type` |enum: [`Face`](/docs/kcl/types/Face)| | No |
|
||||||
| `id` |`string`| The id of the face. | No |
|
| `value` |[`Face`](/docs/kcl/types/Face)| Any KCL value. | 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 |
|
|
||||||
|
|
||||||
|
|
||||||
----
|
----
|
||||||
@ -246,7 +232,6 @@ A face.
|
|||||||
|
|
||||||
|
|
||||||
----
|
----
|
||||||
An solid is a collection of extrude surfaces.
|
|
||||||
|
|
||||||
**Type:** `object`
|
**Type:** `object`
|
||||||
|
|
||||||
@ -259,14 +244,7 @@ An solid is a collection of extrude surfaces.
|
|||||||
| Property | Type | Description | Required |
|
| Property | Type | Description | Required |
|
||||||
|----------|------|-------------|----------|
|
|----------|------|-------------|----------|
|
||||||
| `type` |enum: [`Solid`](/docs/kcl/types/Solid)| | No |
|
| `type` |enum: [`Solid`](/docs/kcl/types/Solid)| | No |
|
||||||
| `id` |`string`| The id of the solid. | No |
|
| `value` |[`Solid`](/docs/kcl/types/Solid)| Any KCL value. | 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 |
|
|
||||||
|
|
||||||
|
|
||||||
----
|
----
|
||||||
@ -286,7 +264,6 @@ An solid is a collection of extrude surfaces.
|
|||||||
|
|
||||||
|
|
||||||
----
|
----
|
||||||
A helix.
|
|
||||||
|
|
||||||
**Type:** `object`
|
**Type:** `object`
|
||||||
|
|
||||||
@ -299,11 +276,7 @@ A helix.
|
|||||||
| Property | Type | Description | Required |
|
| Property | Type | Description | Required |
|
||||||
|----------|------|-------------|----------|
|
|----------|------|-------------|----------|
|
||||||
| `type` |enum: [`Helix`](/docs/kcl/types/Helix)| | No |
|
| `type` |enum: [`Helix`](/docs/kcl/types/Helix)| | No |
|
||||||
| `value` |`string`| The id of the helix. | No |
|
| `value` |[`Helix`](/docs/kcl/types/Helix)| Any KCL value. | 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 |
|
|
||||||
|
|
||||||
|
|
||||||
----
|
----
|
||||||
|
@ -22,6 +22,7 @@ A plane.
|
|||||||
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s X axis be? | 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 |
|
| `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 |
|
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
|
||||||
|
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A plane. | No |
|
||||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||||
|
|
||||||
|
|
||||||
|
@ -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 |
|
| `on` |[`SketchSurface`](/docs/kcl/types/SketchSurface)| What the sketch is on (can be a plane or a face). | No |
|
||||||
| `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No |
|
| `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No |
|
||||||
| `tags` |`object`| Tag identifiers that have been declared in this sketch. | No |
|
| `tags` |`object`| Tag identifiers that have been declared in this sketch. | No |
|
||||||
|
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch is a collection of paths. | No |
|
||||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |
|
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |
|
||||||
|
|
||||||
|
|
||||||
|
@ -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 |
|
| `on` |[`SketchSurface`](/docs/kcl/types/SketchSurface)| What the sketch is on (can be a plane or a face). | No |
|
||||||
| `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No |
|
| `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No |
|
||||||
| `tags` |`object`| Tag identifiers that have been declared in this sketch. | No |
|
| `tags` |`object`| Tag identifiers that have been declared in this sketch. | No |
|
||||||
|
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch or a group of sketches. | No |
|
||||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |
|
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |
|
||||||
|
|
||||||
|
|
||||||
|
@ -31,6 +31,7 @@ A plane.
|
|||||||
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s X axis be? | 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 |
|
| `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 |
|
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
|
||||||
|
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch type. | No |
|
||||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||||
|
|
||||||
|
|
||||||
@ -54,6 +55,7 @@ A face.
|
|||||||
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face’s Y 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 |
|
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
|
||||||
| `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No |
|
| `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No |
|
||||||
|
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch type. | No |
|
||||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||||
|
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ An solid is a collection of extrude surfaces.
|
|||||||
| `startCapId` |`string`| The id of the extrusion start cap | No |
|
| `startCapId` |`string`| The id of the extrusion start cap | No |
|
||||||
| `endCapId` |`string`| The id of the extrusion end cap | No |
|
| `endCapId` |`string`| The id of the extrusion end cap | No |
|
||||||
| `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No |
|
| `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No |
|
||||||
|
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| An solid is a collection of extrude surfaces. | No |
|
||||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |
|
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |
|
||||||
|
|
||||||
|
|
||||||
|
@ -32,6 +32,7 @@ An solid is a collection of extrude surfaces.
|
|||||||
| `startCapId` |`string`| The id of the extrusion start cap | No |
|
| `startCapId` |`string`| The id of the extrusion start cap | No |
|
||||||
| `endCapId` |`string`| The id of the extrusion end cap | No |
|
| `endCapId` |`string`| The id of the extrusion end cap | No |
|
||||||
| `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No |
|
| `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No |
|
||||||
|
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A solid or a group of solids. | No |
|
||||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |
|
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |
|
||||||
|
|
||||||
|
|
||||||
|
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 { test, expect } from './zoo-test'
|
||||||
|
import * as fsp from 'fs/promises'
|
||||||
import { getUtils } from './test-utils'
|
import { executorInputPath, getUtils } from './test-utils'
|
||||||
import { KCL_DEFAULT_LENGTH } from 'lib/constants'
|
import { KCL_DEFAULT_LENGTH } from 'lib/constants'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
test.describe('Command bar tests', () => {
|
test.describe('Command bar tests', () => {
|
||||||
test('Extrude from command bar selects extrude line after', async ({
|
test('Extrude from command bar selects extrude line after', async ({
|
||||||
@ -305,4 +306,132 @@ test.describe('Command bar tests', () => {
|
|||||||
await arcToolCommand.click()
|
await arcToolCommand.click()
|
||||||
await expect(arcToolButton).toHaveAttribute('aria-pressed', 'true')
|
await expect(arcToolButton).toHaveAttribute('aria-pressed', 'true')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test(`Reacts to query param to open "import from URL" command`, async ({
|
||||||
|
page,
|
||||||
|
cmdBar,
|
||||||
|
editor,
|
||||||
|
homePage,
|
||||||
|
}) => {
|
||||||
|
await test.step(`Prepare and navigate to home page with query params`, async () => {
|
||||||
|
const targetURL = `?create-file&name=test&units=mm&code=ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg%3D%3D&ask-open-desktop`
|
||||||
|
await homePage.expectState({
|
||||||
|
projectCards: [],
|
||||||
|
sortBy: 'last-modified-desc',
|
||||||
|
})
|
||||||
|
await page.goto(page.url() + targetURL)
|
||||||
|
expect(page.url()).toContain(targetURL)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step(`Submit the command`, async () => {
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
commandName: 'Import file from URL',
|
||||||
|
currentArgKey: 'method',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Method: '',
|
||||||
|
Name: 'test',
|
||||||
|
Code: '1 line',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'method',
|
||||||
|
})
|
||||||
|
await cmdBar.selectOption({ name: 'New Project' }).click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'review',
|
||||||
|
commandName: 'Import file from URL',
|
||||||
|
headerArguments: {
|
||||||
|
Method: 'New project',
|
||||||
|
Name: 'test',
|
||||||
|
Code: '1 line',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step(`Ensure we created the project and are in the modeling scene`, async () => {
|
||||||
|
await editor.expectEditor.toContain('extrusionDistance = 12')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test(`"import from URL" can add to existing project`, async ({
|
||||||
|
page,
|
||||||
|
cmdBar,
|
||||||
|
editor,
|
||||||
|
homePage,
|
||||||
|
toolbar,
|
||||||
|
context,
|
||||||
|
}) => {
|
||||||
|
await context.folderSetupFn(async (dir) => {
|
||||||
|
const testProjectDir = path.join(dir, 'testProjectDir')
|
||||||
|
await Promise.all([fsp.mkdir(testProjectDir, { recursive: true })])
|
||||||
|
await Promise.all([
|
||||||
|
fsp.copyFile(
|
||||||
|
executorInputPath('cylinder.kcl'),
|
||||||
|
path.join(testProjectDir, 'main.kcl')
|
||||||
|
),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
await test.step(`Prepare and navigate to home page with query params`, async () => {
|
||||||
|
const targetURL = `?create-file&name=test&units=mm&code=ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg%3D%3D&ask-open-desktop`
|
||||||
|
await homePage.expectState({
|
||||||
|
projectCards: [
|
||||||
|
{
|
||||||
|
fileCount: 1,
|
||||||
|
title: 'testProjectDir',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sortBy: 'last-modified-desc',
|
||||||
|
})
|
||||||
|
await page.goto(page.url() + targetURL)
|
||||||
|
expect(page.url()).toContain(targetURL)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step(`Submit the command`, async () => {
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
commandName: 'Import file from URL',
|
||||||
|
currentArgKey: 'method',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Method: '',
|
||||||
|
Name: 'test',
|
||||||
|
Code: '1 line',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'method',
|
||||||
|
})
|
||||||
|
await cmdBar.selectOption({ name: 'Existing Project' }).click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
commandName: 'Import file from URL',
|
||||||
|
currentArgKey: 'projectName',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Method: 'Existing project',
|
||||||
|
Name: 'test',
|
||||||
|
ProjectName: '',
|
||||||
|
Code: '1 line',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'projectName',
|
||||||
|
})
|
||||||
|
await cmdBar.selectOption({ name: 'testProjectDir' }).click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'review',
|
||||||
|
commandName: 'Import file from URL',
|
||||||
|
headerArguments: {
|
||||||
|
Method: 'Existing project',
|
||||||
|
ProjectName: 'testProjectDir',
|
||||||
|
Name: 'test',
|
||||||
|
Code: '1 line',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step(`Ensure we created the project and are in the modeling scene`, async () => {
|
||||||
|
await editor.expectEditor.toContain('extrusionDistance = 12')
|
||||||
|
await toolbar.openPane('files')
|
||||||
|
await toolbar.expectFileTreeState(['main.kcl', 'test.kcl'])
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -38,14 +38,14 @@ test.describe('Debug pane', () => {
|
|||||||
// Set the code in the code editor.
|
// Set the code in the code editor.
|
||||||
await u.codeLocator.click()
|
await u.codeLocator.click()
|
||||||
await page.keyboard.type(code, { delay: 0 })
|
await page.keyboard.type(code, { delay: 0 })
|
||||||
// Scroll to the feature tree.
|
// Scroll to the artifact graph.
|
||||||
await tree.scrollIntoViewIfNeeded()
|
await tree.scrollIntoViewIfNeeded()
|
||||||
// Expand the feature tree.
|
// Expand the artifact graph.
|
||||||
await tree.getByText('Feature Tree').click()
|
await tree.getByText('Artifact Graph').click()
|
||||||
// Just expanded the details, making the element taller, so scroll again.
|
// Just expanded the details, making the element taller, so scroll again.
|
||||||
await tree.getByText('Plane').first().scrollIntoViewIfNeeded()
|
await tree.getByText('Plane').first().scrollIntoViewIfNeeded()
|
||||||
})
|
})
|
||||||
// Extract the artifact IDs from the debug feature tree.
|
// Extract the artifact IDs from the debug artifact graph.
|
||||||
const initialSegmentIds = await segment.innerText({ timeout: 5_000 })
|
const initialSegmentIds = await segment.innerText({ timeout: 5_000 })
|
||||||
// The artifact ID should include a UUID.
|
// The artifact ID should include a UUID.
|
||||||
expect(initialSegmentIds).toMatch(
|
expect(initialSegmentIds).toMatch(
|
||||||
|
@ -151,4 +151,11 @@ export class CmdBarFixture {
|
|||||||
chooseCommand = async (commandName: string) => {
|
chooseCommand = async (commandName: string) => {
|
||||||
await this.cmdOptions.getByText(commandName).click()
|
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)
|
.toEqual(expectedState)
|
||||||
}
|
}
|
||||||
|
|
||||||
createAndGoToProject = async (projectTitle: string) => {
|
createAndGoToProject = async (projectTitle = 'project-$nnn') => {
|
||||||
await expect(this.projectSection).not.toHaveText('Loading your Projects...')
|
await expect(this.projectSection).not.toHaveText('Loading your Projects...')
|
||||||
await this.projectButtonNew.click()
|
await this.projectButtonNew.click()
|
||||||
await this.projectTextName.click()
|
await this.projectTextName.click()
|
||||||
|
@ -963,37 +963,31 @@ sketch002 = startSketchOn('XZ')
|
|||||||
await toolbar.sweepButton.click()
|
await toolbar.sweepButton.click()
|
||||||
await cmdBar.expectState({
|
await cmdBar.expectState({
|
||||||
commandName: 'Sweep',
|
commandName: 'Sweep',
|
||||||
currentArgKey: 'profile',
|
currentArgKey: 'target',
|
||||||
currentArgValue: '',
|
currentArgValue: '',
|
||||||
headerArguments: {
|
headerArguments: {
|
||||||
Path: '',
|
Target: '',
|
||||||
Profile: '',
|
Trajectory: '',
|
||||||
},
|
},
|
||||||
highlightedHeaderArg: 'profile',
|
highlightedHeaderArg: 'target',
|
||||||
stage: 'arguments',
|
stage: 'arguments',
|
||||||
})
|
})
|
||||||
await clickOnSketch1()
|
await clickOnSketch1()
|
||||||
await cmdBar.expectState({
|
await cmdBar.expectState({
|
||||||
commandName: 'Sweep',
|
commandName: 'Sweep',
|
||||||
currentArgKey: 'path',
|
currentArgKey: 'trajectory',
|
||||||
currentArgValue: '',
|
currentArgValue: '',
|
||||||
headerArguments: {
|
headerArguments: {
|
||||||
Path: '',
|
Target: '1 face',
|
||||||
Profile: '1 face',
|
Trajectory: '',
|
||||||
},
|
},
|
||||||
highlightedHeaderArg: 'path',
|
highlightedHeaderArg: 'trajectory',
|
||||||
stage: 'arguments',
|
stage: 'arguments',
|
||||||
})
|
})
|
||||||
await clickOnSketch2()
|
await clickOnSketch2()
|
||||||
await cmdBar.expectState({
|
await page.waitForTimeout(500)
|
||||||
commandName: 'Sweep',
|
|
||||||
headerArguments: {
|
|
||||||
Path: '1 face',
|
|
||||||
Profile: '1 face',
|
|
||||||
},
|
|
||||||
stage: 'review',
|
|
||||||
})
|
|
||||||
await cmdBar.progressCmdBar()
|
await cmdBar.progressCmdBar()
|
||||||
|
await page.waitForTimeout(500)
|
||||||
})
|
})
|
||||||
|
|
||||||
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
|
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
|
||||||
@ -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 ({
|
test(`Fillet point-and-click`, async ({
|
||||||
context,
|
context,
|
||||||
page,
|
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(
|
test(
|
||||||
'File in the file pane should open with a single click',
|
'File in the file pane should open with a single click',
|
||||||
{ tag: '@electron' },
|
{ 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 |
@ -113,9 +113,9 @@
|
|||||||
"test:unit": "vitest run --mode development --exclude **/kclSamples.test.ts",
|
"test:unit": "vitest run --mode development --exclude **/kclSamples.test.ts",
|
||||||
"test:unit:kcl-samples": "vitest run --mode development ./src/lang/kclSamples.test.ts",
|
"test:unit:kcl-samples": "vitest run --mode development ./src/lang/kclSamples.test.ts",
|
||||||
"test:playwright:electron": "playwright test --config=playwright.electron.config.ts --grep-invert='@snapshot'",
|
"test:playwright:electron": "playwright test --config=playwright.electron.config.ts --grep-invert='@snapshot'",
|
||||||
"test:playwright:electron:windows": "playwright test --config=playwright.electron.config.ts --grep-invert=\"@skipWin|@snapshot\"",
|
"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'",
|
"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'",
|
"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:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@snapshot'",
|
||||||
"test:playwright:electron:windows:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert=\"@skipWin|@snapshot\"",
|
"test:playwright:electron:windows:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert=\"@skipWin|@snapshot\"",
|
||||||
"test:playwright:electron:macos:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@skipMacos|@snapshot'",
|
"test:playwright:electron:macos:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@skipMacos|@snapshot'",
|
||||||
@ -201,7 +201,7 @@
|
|||||||
"ts-node": "^10.0.0",
|
"ts-node": "^10.0.0",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3",
|
||||||
"typescript-eslint": "^8.19.1",
|
"typescript-eslint": "^8.19.1",
|
||||||
"vite": "^5.4.6",
|
"vite": "^5.4.12",
|
||||||
"vite-plugin-package-version": "^1.1.0",
|
"vite-plugin-package-version": "^1.1.0",
|
||||||
"vite-tsconfig-paths": "^4.3.2",
|
"vite-tsconfig-paths": "^4.3.2",
|
||||||
"vitest": "^1.6.0",
|
"vitest": "^1.6.0",
|
||||||
|
15
src/App.tsx
@ -22,13 +22,28 @@ import Gizmo from 'components/Gizmo'
|
|||||||
import { CoreDumpManager } from 'lib/coredump'
|
import { CoreDumpManager } from 'lib/coredump'
|
||||||
import { UnitsMenu } from 'components/UnitsMenu'
|
import { UnitsMenu } from 'components/UnitsMenu'
|
||||||
import { CameraProjectionToggle } from 'components/CameraProjectionToggle'
|
import { CameraProjectionToggle } from 'components/CameraProjectionToggle'
|
||||||
|
import { useCreateFileLinkQuery } from 'hooks/useCreateFileLinkQueryWatcher'
|
||||||
import { maybeWriteToDisk } from 'lib/telemetry'
|
import { maybeWriteToDisk } from 'lib/telemetry'
|
||||||
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
maybeWriteToDisk()
|
maybeWriteToDisk()
|
||||||
.then(() => {})
|
.then(() => {})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const { project, file } = useLoaderData() as IndexLoaderData
|
const { project, file } = useLoaderData() as IndexLoaderData
|
||||||
|
|
||||||
|
// Keep a lookout for a URL query string that invokes the 'import file from URL' command
|
||||||
|
useCreateFileLinkQuery((argDefaultValues) => {
|
||||||
|
commandBarActor.send({
|
||||||
|
type: 'Find and select command',
|
||||||
|
data: {
|
||||||
|
groupId: 'projects',
|
||||||
|
name: 'Import file from URL',
|
||||||
|
argDefaultValues,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
useRefreshSettings(PATHS.FILE + 'SETTINGS')
|
useRefreshSettings(PATHS.FILE + 'SETTINGS')
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const filePath = useAbsoluteFilePath()
|
const filePath = useAbsoluteFilePath()
|
||||||
|
@ -31,11 +31,10 @@ import {
|
|||||||
settingsLoader,
|
settingsLoader,
|
||||||
telemetryLoader,
|
telemetryLoader,
|
||||||
} from 'lib/routeLoaders'
|
} from 'lib/routeLoaders'
|
||||||
import { CommandBarProvider } from 'components/CommandBar/CommandBarProvider'
|
|
||||||
import SettingsAuthProvider from 'components/SettingsAuthProvider'
|
import SettingsAuthProvider from 'components/SettingsAuthProvider'
|
||||||
import LspProvider from 'components/LspProvider'
|
import LspProvider from 'components/LspProvider'
|
||||||
import { KclContextProvider } from 'lang/KclProvider'
|
import { KclContextProvider } from 'lang/KclProvider'
|
||||||
import { BROWSER_PROJECT_NAME } from 'lib/constants'
|
import { ASK_TO_OPEN_QUERY_PARAM, BROWSER_PROJECT_NAME } from 'lib/constants'
|
||||||
import { CoreDumpManager } from 'lib/coredump'
|
import { CoreDumpManager } from 'lib/coredump'
|
||||||
import { codeManager, engineCommandManager } from 'lib/singletons'
|
import { codeManager, engineCommandManager } from 'lib/singletons'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
@ -47,6 +46,7 @@ import { AppStateProvider } from 'AppState'
|
|||||||
import { reportRejection } from 'lib/trap'
|
import { reportRejection } from 'lib/trap'
|
||||||
import { RouteProvider } from 'components/RouteProvider'
|
import { RouteProvider } from 'components/RouteProvider'
|
||||||
import { ProjectsContextProvider } from 'components/ProjectsContextProvider'
|
import { ProjectsContextProvider } from 'components/ProjectsContextProvider'
|
||||||
|
import { OpenInDesktopAppHandler } from 'components/OpenInDesktopAppHandler'
|
||||||
|
|
||||||
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
|
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
|
||||||
|
|
||||||
@ -58,7 +58,7 @@ const router = createRouter([
|
|||||||
/* Make sure auth is the outermost provider or else we will have
|
/* Make sure auth is the outermost provider or else we will have
|
||||||
* inefficient re-renders, use the react profiler to see. */
|
* inefficient re-renders, use the react profiler to see. */
|
||||||
element: (
|
element: (
|
||||||
<CommandBarProvider>
|
<OpenInDesktopAppHandler>
|
||||||
<RouteProvider>
|
<RouteProvider>
|
||||||
<SettingsAuthProvider>
|
<SettingsAuthProvider>
|
||||||
<LspProvider>
|
<LspProvider>
|
||||||
@ -74,17 +74,26 @@ const router = createRouter([
|
|||||||
</LspProvider>
|
</LspProvider>
|
||||||
</SettingsAuthProvider>
|
</SettingsAuthProvider>
|
||||||
</RouteProvider>
|
</RouteProvider>
|
||||||
</CommandBarProvider>
|
</OpenInDesktopAppHandler>
|
||||||
),
|
),
|
||||||
errorElement: <ErrorPage />,
|
errorElement: <ErrorPage />,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: PATHS.INDEX,
|
path: PATHS.INDEX,
|
||||||
loader: async () => {
|
loader: async ({ request }) => {
|
||||||
const onDesktop = isDesktop()
|
const onDesktop = isDesktop()
|
||||||
return onDesktop
|
const url = new URL(request.url)
|
||||||
? redirect(PATHS.HOME)
|
if (onDesktop) {
|
||||||
: redirect(PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME)
|
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 { isCursorInSketchCommandRange } from 'lang/util'
|
||||||
import { engineCommandManager, kclManager } from 'lib/singletons'
|
import { engineCommandManager, kclManager } from 'lib/singletons'
|
||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import { useNetworkContext } from 'hooks/useNetworkContext'
|
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||||
import { ActionButton } from 'components/ActionButton'
|
import { ActionButton } from 'components/ActionButton'
|
||||||
@ -22,20 +21,19 @@ import {
|
|||||||
} from 'lib/toolbar'
|
} from 'lib/toolbar'
|
||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||||
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
|
|
||||||
export function Toolbar({
|
export function Toolbar({
|
||||||
className = '',
|
className = '',
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLElement>) {
|
}: React.HTMLAttributes<HTMLElement>) {
|
||||||
const { state, send, context } = useModelingContext()
|
const { state, send, context } = useModelingContext()
|
||||||
const { commandBarSend } = useCommandsContext()
|
|
||||||
const iconClassName =
|
const iconClassName =
|
||||||
'group-disabled:text-chalkboard-50 !text-inherit dark:group-enabled:group-hover:!text-inherit'
|
'group-disabled:text-chalkboard-50 !text-inherit dark:group-enabled:group-hover:!text-inherit'
|
||||||
const bgClassName = '!bg-transparent'
|
const bgClassName = '!bg-transparent'
|
||||||
const buttonBgClassName =
|
const buttonBgClassName =
|
||||||
'bg-chalkboard-transparent dark:bg-transparent disabled:bg-transparent dark:disabled:bg-transparent enabled:hover:bg-chalkboard-10 dark:enabled:hover:bg-chalkboard-100 pressed:!bg-primary pressed:enabled:hover:!text-chalkboard-10'
|
'bg-chalkboard-transparent dark:bg-transparent disabled:bg-transparent dark:disabled:bg-transparent enabled:hover:bg-chalkboard-10 dark:enabled:hover:bg-chalkboard-100 pressed:!bg-primary pressed:enabled:hover:!text-chalkboard-10'
|
||||||
const buttonBorderClassName =
|
const buttonBorderClassName = '!border-transparent'
|
||||||
'!border-transparent hover:!border-chalkboard-20 dark:enabled:hover:!border-primary pressed:!border-primary ui-open:!border-primary'
|
|
||||||
|
|
||||||
const sketchPathId = useMemo(() => {
|
const sketchPathId = useMemo(() => {
|
||||||
if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast))
|
if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast))
|
||||||
@ -50,6 +48,7 @@ export function Toolbar({
|
|||||||
const { overallState } = useNetworkContext()
|
const { overallState } = useNetworkContext()
|
||||||
const { isExecuting } = useKclContext()
|
const { isExecuting } = useKclContext()
|
||||||
const { isStreamReady } = useAppState()
|
const { isStreamReady } = useAppState()
|
||||||
|
const [showRichContent, setShowRichContent] = useState(false)
|
||||||
|
|
||||||
const disableAllButtons =
|
const disableAllButtons =
|
||||||
(overallState !== NetworkHealthState.Ok &&
|
(overallState !== NetworkHealthState.Ok &&
|
||||||
@ -71,12 +70,45 @@ export function Toolbar({
|
|||||||
() => ({
|
() => ({
|
||||||
modelingState: state,
|
modelingState: state,
|
||||||
modelingSend: send,
|
modelingSend: send,
|
||||||
commandBarSend,
|
|
||||||
sketchPathId,
|
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,
|
* Resolve all the callbacks and values for the current mode,
|
||||||
* so we don't need to worry about the other modes
|
* so we don't need to worry about the other modes
|
||||||
@ -174,43 +206,64 @@ export function Toolbar({
|
|||||||
status: itemConfig.status,
|
status: itemConfig.status,
|
||||||
}))}
|
}))}
|
||||||
>
|
>
|
||||||
<ActionButton
|
<div
|
||||||
Element="button"
|
className="contents"
|
||||||
id={maybeIconConfig[0].id}
|
// Mouse events do not fire on disabled buttons
|
||||||
data-testid={maybeIconConfig[0].id}
|
onMouseEnter={handleMouseEnter}
|
||||||
iconStart={{
|
onMouseLeave={handleMouseLeave}
|
||||||
icon: maybeIconConfig[0].icon,
|
|
||||||
className: iconClassName,
|
|
||||||
bgClassName: bgClassName,
|
|
||||||
}}
|
|
||||||
className={
|
|
||||||
'!border-transparent !px-0 pressed:!text-chalkboard-10 pressed:enabled:hovered:!text-chalkboard-10 ' +
|
|
||||||
buttonBgClassName
|
|
||||||
}
|
|
||||||
aria-pressed={maybeIconConfig[0].isActive}
|
|
||||||
disabled={
|
|
||||||
disableAllButtons ||
|
|
||||||
maybeIconConfig[0].status !== 'available' ||
|
|
||||||
maybeIconConfig[0].disabled
|
|
||||||
}
|
|
||||||
name={maybeIconConfig[0].title}
|
|
||||||
// aria-description is still in ARIA 1.3 draft.
|
|
||||||
// eslint-disable-next-line jsx-a11y/aria-props
|
|
||||||
aria-description={maybeIconConfig[0].description}
|
|
||||||
onClick={() =>
|
|
||||||
maybeIconConfig[0].onClick(configCallbackProps)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<span
|
<ActionButton
|
||||||
className={!maybeIconConfig[0].showTitle ? 'sr-only' : ''}
|
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
|
||||||
</span>
|
className={!maybeIconConfig[0].showTitle ? 'sr-only' : ''}
|
||||||
</ActionButton>
|
>
|
||||||
<ToolbarItemTooltip
|
{maybeIconConfig[0].title}
|
||||||
itemConfig={maybeIconConfig[0]}
|
</span>
|
||||||
configCallbackProps={configCallbackProps}
|
<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>
|
</ActionButtonDropdown>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -218,7 +271,13 @@ export function Toolbar({
|
|||||||
|
|
||||||
// A single button
|
// A single button
|
||||||
return (
|
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
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
key={itemConfig.id}
|
key={itemConfig.id}
|
||||||
@ -255,7 +314,18 @@ export function Toolbar({
|
|||||||
<ToolbarItemTooltip
|
<ToolbarItemTooltip
|
||||||
itemConfig={itemConfig}
|
itemConfig={itemConfig}
|
||||||
configCallbackProps={configCallbackProps}
|
configCallbackProps={configCallbackProps}
|
||||||
/>
|
contentClassName={tooltipContentClassName}
|
||||||
|
>
|
||||||
|
{showRichContent ? (
|
||||||
|
<ToolbarItemTooltipRichContent itemConfig={itemConfig} />
|
||||||
|
) : (
|
||||||
|
<ToolbarItemTooltipShortContent
|
||||||
|
status={itemConfig.status}
|
||||||
|
title={itemConfig.title}
|
||||||
|
hotkey={itemConfig.hotkey}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ToolbarItemTooltip>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@ -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
|
* The single button and dropdown button share content, so we extract it here
|
||||||
* It contains a tooltip with the title, description, and links
|
* It contains a tooltip with the title, description, and links
|
||||||
@ -277,12 +353,10 @@ export function Toolbar({
|
|||||||
const ToolbarItemTooltip = memo(function ToolbarItemContents({
|
const ToolbarItemTooltip = memo(function ToolbarItemContents({
|
||||||
itemConfig,
|
itemConfig,
|
||||||
configCallbackProps,
|
configCallbackProps,
|
||||||
}: {
|
wrapperClassName = '',
|
||||||
itemConfig: ToolbarItemResolved
|
contentClassName = '',
|
||||||
configCallbackProps: ToolbarItemCallbackProps
|
children,
|
||||||
}) {
|
}: ToolbarItemContentsProps) {
|
||||||
const { state } = useModelingContext()
|
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
itemConfig.hotkey || '',
|
itemConfig.hotkey || '',
|
||||||
() => {
|
() => {
|
||||||
@ -305,11 +379,50 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
|
|||||||
? ({ '-webkit-app-region': 'no-drag' } as React.CSSProperties)
|
? ({ '-webkit-app-region': 'no-drag' } as React.CSSProperties)
|
||||||
: {}
|
: {}
|
||||||
}
|
}
|
||||||
|
hoverOnly
|
||||||
position="bottom"
|
position="bottom"
|
||||||
wrapperClassName="!p-4 !pointer-events-auto"
|
wrapperClassName={'!p-4 !pointer-events-auto ' + wrapperClassName}
|
||||||
contentClassName="!text-left text-wrap !text-xs !p-0 !pb-2 flex gap-2 !max-w-none !w-72 flex-col items-stretch"
|
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">
|
<div className="rounded-top flex items-center gap-2 pt-3 pb-2 px-2 bg-chalkboard-20/50 dark:bg-chalkboard-80/50">
|
||||||
|
{itemConfig.icon && (
|
||||||
|
<CustomIcon className="w-5 h-5" name={itemConfig.icon} />
|
||||||
|
)}
|
||||||
<span
|
<span
|
||||||
className={`text-sm flex-1 ${
|
className={`text-sm flex-1 ${
|
||||||
itemConfig.status !== 'available'
|
itemConfig.status !== 'available'
|
||||||
@ -378,6 +491,6 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
|
|||||||
</ul>
|
</ul>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Tooltip>
|
</>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
@ -46,8 +46,8 @@ import {
|
|||||||
} from 'lang/modifyAst'
|
} from 'lang/modifyAst'
|
||||||
import { ActionButton } from 'components/ActionButton'
|
import { ActionButton } from 'components/ActionButton'
|
||||||
import { err, reportRejection, trap } from 'lib/trap'
|
import { err, reportRejection, trap } from 'lib/trap'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||||
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
|
|
||||||
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
|
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
|
||||||
const [isCamMoving, setIsCamMoving] = useState(false)
|
const [isCamMoving, setIsCamMoving] = useState(false)
|
||||||
@ -510,7 +510,6 @@ const ConstraintSymbol = ({
|
|||||||
constrainInfo: ConstrainInfo
|
constrainInfo: ConstrainInfo
|
||||||
verticalPosition: 'top' | 'bottom'
|
verticalPosition: 'top' | 'bottom'
|
||||||
}) => {
|
}) => {
|
||||||
const { commandBarSend } = useCommandsContext()
|
|
||||||
const { context } = useModelingContext()
|
const { context } = useModelingContext()
|
||||||
const varNameMap: {
|
const varNameMap: {
|
||||||
[key in ConstrainInfo['type']]: {
|
[key in ConstrainInfo['type']]: {
|
||||||
@ -630,7 +629,7 @@ const ConstraintSymbol = ({
|
|||||||
// disabled={implicitDesc} TODO why does this change styles that are hard to override?
|
// disabled={implicitDesc} TODO why does this change styles that are hard to override?
|
||||||
onClick={toSync(async () => {
|
onClick={toSync(async () => {
|
||||||
if (!isConstrained) {
|
if (!isConstrained) {
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: {
|
data: {
|
||||||
name: 'Constrain with named value',
|
name: 'Constrain with named value',
|
||||||
@ -756,7 +755,6 @@ export const CamDebugSettings = () => {
|
|||||||
sceneInfra.camControls.reactCameraProperties
|
sceneInfra.camControls.reactCameraProperties
|
||||||
)
|
)
|
||||||
const [fov, setFov] = useState(12)
|
const [fov, setFov] = useState(12)
|
||||||
const { commandBarSend } = useCommandsContext()
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
sceneInfra.camControls.setReactCameraPropertiesCallback(setCamSettings)
|
sceneInfra.camControls.setReactCameraPropertiesCallback(setCamSettings)
|
||||||
@ -775,7 +773,7 @@ export const CamDebugSettings = () => {
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={camSettings.type === 'perspective'}
|
checked={camSettings.type === 'perspective'}
|
||||||
onChange={() =>
|
onChange={() =>
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: {
|
data: {
|
||||||
groupId: 'settings',
|
groupId: 'settings',
|
||||||
|
@ -1398,23 +1398,23 @@ export class SceneEntities {
|
|||||||
|
|
||||||
const arg0 = arg(kclCircle3PointArgs[0])
|
const arg0 = arg(kclCircle3PointArgs[0])
|
||||||
if (!arg0) return kclManager.ast
|
if (!arg0) return kclManager.ast
|
||||||
arg0[0].value = points[0].x
|
arg0[0].value = { value: points[0].x, suffix: 'None' }
|
||||||
arg0[0].raw = points[0].x.toString()
|
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()
|
arg0[1].raw = points[0].y.toString()
|
||||||
|
|
||||||
const arg1 = arg(kclCircle3PointArgs[1])
|
const arg1 = arg(kclCircle3PointArgs[1])
|
||||||
if (!arg1) return kclManager.ast
|
if (!arg1) return kclManager.ast
|
||||||
arg1[0].value = points[1].x
|
arg1[0].value = { value: points[1].x, suffix: 'None' }
|
||||||
arg1[0].raw = points[1].x.toString()
|
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()
|
arg1[1].raw = points[1].y.toString()
|
||||||
|
|
||||||
const arg2 = arg(kclCircle3PointArgs[2])
|
const arg2 = arg(kclCircle3PointArgs[2])
|
||||||
if (!arg2) return kclManager.ast
|
if (!arg2) return kclManager.ast
|
||||||
arg2[0].value = points[2].x
|
arg2[0].value = { value: points[2].x, suffix: 'None' }
|
||||||
arg2[0].raw = points[2].x.toString()
|
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()
|
arg2[1].raw = points[2].y.toString()
|
||||||
|
|
||||||
const astSnapshot = structuredClone(kclManager.ast)
|
const astSnapshot = structuredClone(kclManager.ast)
|
||||||
@ -2051,8 +2051,8 @@ export class SceneEntities {
|
|||||||
)
|
)
|
||||||
if (!(sk instanceof Reason)) {
|
if (!(sk instanceof Reason)) {
|
||||||
sketch = sk
|
sketch = sk
|
||||||
} else if ((maybeSketch as Solid).sketch) {
|
} else if (maybeSketch && (maybeSketch.value as Solid)?.sketch) {
|
||||||
sketch = (maybeSketch as Solid).sketch
|
sketch = (maybeSketch.value as Solid).sketch
|
||||||
}
|
}
|
||||||
if (!sketch) return
|
if (!sketch) return
|
||||||
|
|
||||||
@ -2541,7 +2541,7 @@ export function sketchFromPathToNode({
|
|||||||
const varDec = _varDec.node
|
const varDec = _varDec.node
|
||||||
const result = programMemory.get(varDec?.id?.name || '')
|
const result = programMemory.get(varDec?.id?.name || '')
|
||||||
if (result?.type === 'Solid') {
|
if (result?.type === 'Solid') {
|
||||||
return result.sketch
|
return result.value.sketch
|
||||||
}
|
}
|
||||||
const sg = sketchFromKclValue(result, varDec?.id?.name)
|
const sg = sketchFromKclValue(result, varDec?.id?.name)
|
||||||
if (err(sg)) {
|
if (err(sg)) {
|
||||||
|
@ -61,6 +61,7 @@ import { SegmentInputs } from 'lang/std/stdTypes'
|
|||||||
import { err } from 'lib/trap'
|
import { err } from 'lib/trap'
|
||||||
import { editorManager, sceneInfra } from 'lib/singletons'
|
import { editorManager, sceneInfra } from 'lib/singletons'
|
||||||
import { Selections } from 'lib/selections'
|
import { Selections } from 'lib/selections'
|
||||||
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
|
|
||||||
interface CreateSegmentArgs {
|
interface CreateSegmentArgs {
|
||||||
input: SegmentInputs
|
input: SegmentInputs
|
||||||
@ -847,7 +848,7 @@ function createLengthIndicator({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Command Bar
|
// Command Bar
|
||||||
editorManager.commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: {
|
data: {
|
||||||
name: 'Constrain length',
|
name: 'Constrain length',
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import { Popover } from '@headlessui/react'
|
import { Popover } from '@headlessui/react'
|
||||||
import { ActionButtonProps } from './ActionButton'
|
import { ActionButtonProps } from './ActionButton'
|
||||||
import { CustomIcon } from './CustomIcon'
|
import { CustomIcon } from './CustomIcon'
|
||||||
|
import Tooltip from './Tooltip'
|
||||||
|
|
||||||
type ActionButtonSplitProps = ActionButtonProps & { Element: 'button' } & {
|
type ActionButtonSplitProps = ActionButtonProps & { Element: 'button' } & {
|
||||||
name?: string
|
name?: string
|
||||||
|
dropdownTooltipText?: string
|
||||||
splitMenuItems: {
|
splitMenuItems: {
|
||||||
id: string
|
id: string
|
||||||
label: string
|
label: string
|
||||||
@ -17,6 +19,7 @@ type ActionButtonSplitProps = ActionButtonProps & { Element: 'button' } & {
|
|||||||
export function ActionButtonDropdown({
|
export function ActionButtonDropdown({
|
||||||
splitMenuItems,
|
splitMenuItems,
|
||||||
className,
|
className,
|
||||||
|
dropdownTooltipText = 'More tools',
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: ActionButtonSplitProps) {
|
}: ActionButtonSplitProps) {
|
||||||
@ -26,7 +29,14 @@ export function ActionButtonDropdown({
|
|||||||
{({ close }) => (
|
{({ close }) => (
|
||||||
<>
|
<>
|
||||||
{children}
|
{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
|
<CustomIcon
|
||||||
name="caretDown"
|
name="caretDown"
|
||||||
className={
|
className={
|
||||||
@ -37,6 +47,14 @@ export function ActionButtonDropdown({
|
|||||||
<span className="sr-only">
|
<span className="sr-only">
|
||||||
{props.name ? props.name + ': ' : ''}open menu
|
{props.name ? props.name + ': ' : ''}open menu
|
||||||
</span>
|
</span>
|
||||||
|
<Tooltip
|
||||||
|
delay={0}
|
||||||
|
position="bottom"
|
||||||
|
hoverOnly
|
||||||
|
wrapperClassName="ui-open:!hidden"
|
||||||
|
>
|
||||||
|
{dropdownTooltipText}
|
||||||
|
</Tooltip>
|
||||||
</Popover.Button>
|
</Popover.Button>
|
||||||
<Popover.Panel
|
<Popover.Panel
|
||||||
as="ul"
|
as="ul"
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { Combobox } from '@headlessui/react'
|
import { Combobox } from '@headlessui/react'
|
||||||
import { useSelector } from '@xstate/react'
|
import { useSelector } from '@xstate/react'
|
||||||
import Fuse from 'fuse.js'
|
import Fuse from 'fuse.js'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import { CommandArgument, CommandArgumentOption } from 'lib/commandTypes'
|
import { CommandArgument, CommandArgumentOption } from 'lib/commandTypes'
|
||||||
|
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { AnyStateMachine, StateFrom } from 'xstate'
|
import { AnyStateMachine, StateFrom } from 'xstate'
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ function CommandArgOptionInput({
|
|||||||
placeholder?: string
|
placeholder?: string
|
||||||
}) {
|
}) {
|
||||||
const actorContext = useSelector(arg.machineActor, contextSelector)
|
const actorContext = useSelector(arg.machineActor, contextSelector)
|
||||||
const { commandBarSend, commandBarState } = useCommandsContext()
|
const commandBarState = useCommandBarState()
|
||||||
const resolvedOptions = useMemo(
|
const resolvedOptions = useMemo(
|
||||||
() =>
|
() =>
|
||||||
typeof arg.options === 'function'
|
typeof arg.options === 'function'
|
||||||
@ -129,6 +129,7 @@ function CommandArgOptionInput({
|
|||||||
<label
|
<label
|
||||||
htmlFor="option-input"
|
htmlFor="option-input"
|
||||||
className="capitalize px-2 py-1 rounded-l bg-chalkboard-100 dark:bg-chalkboard-80 text-chalkboard-10 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80"
|
className="capitalize px-2 py-1 rounded-l bg-chalkboard-100 dark:bg-chalkboard-80 text-chalkboard-10 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80"
|
||||||
|
data-testid="cmd-bar-arg-name"
|
||||||
>
|
>
|
||||||
{argName}
|
{argName}
|
||||||
</label>
|
</label>
|
||||||
@ -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"
|
className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none"
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.metaKey && event.key === 'k')
|
if (event.metaKey && event.key === 'k')
|
||||||
commandBarSend({ type: 'Close' })
|
commandBarActor.send({ type: 'Close' })
|
||||||
if (event.key === 'Backspace' && !event.currentTarget.value) {
|
if (event.key === 'Backspace' && !event.currentTarget.value) {
|
||||||
stepBack()
|
stepBack()
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { Dialog, Popover, Transition } from '@headlessui/react'
|
import { Dialog, Popover, Transition } from '@headlessui/react'
|
||||||
import { Fragment, useEffect } from 'react'
|
import { Fragment, useEffect } from 'react'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import CommandBarArgument from './CommandBarArgument'
|
import CommandBarArgument from './CommandBarArgument'
|
||||||
import CommandComboBox from '../CommandComboBox'
|
import CommandComboBox from '../CommandComboBox'
|
||||||
import CommandBarReview from './CommandBarReview'
|
import CommandBarReview from './CommandBarReview'
|
||||||
@ -8,12 +7,13 @@ import { useLocation } from 'react-router-dom'
|
|||||||
import useHotkeyWrapper from 'lib/hotkeyWrapper'
|
import useHotkeyWrapper from 'lib/hotkeyWrapper'
|
||||||
import { CustomIcon } from 'components/CustomIcon'
|
import { CustomIcon } from 'components/CustomIcon'
|
||||||
import Tooltip from 'components/Tooltip'
|
import Tooltip from 'components/Tooltip'
|
||||||
|
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||||
|
|
||||||
export const COMMAND_PALETTE_HOTKEY = 'mod+k'
|
export const COMMAND_PALETTE_HOTKEY = 'mod+k'
|
||||||
|
|
||||||
export const CommandBar = () => {
|
export const CommandBar = () => {
|
||||||
const { pathname } = useLocation()
|
const { pathname } = useLocation()
|
||||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
const commandBarState = useCommandBarState()
|
||||||
const {
|
const {
|
||||||
context: { selectedCommand, currentArgument, commands },
|
context: { selectedCommand, currentArgument, commands },
|
||||||
} = commandBarState
|
} = commandBarState
|
||||||
@ -23,16 +23,16 @@ export const CommandBar = () => {
|
|||||||
// Close the command bar when navigating
|
// Close the command bar when navigating
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (commandBarState.matches('Closed')) return
|
if (commandBarState.matches('Closed')) return
|
||||||
commandBarSend({ type: 'Close' })
|
commandBarActor.send({ type: 'Close' })
|
||||||
}, [pathname])
|
}, [pathname])
|
||||||
|
|
||||||
// Hook up keyboard shortcuts
|
// Hook up keyboard shortcuts
|
||||||
useHotkeyWrapper([COMMAND_PALETTE_HOTKEY], () => {
|
useHotkeyWrapper([COMMAND_PALETTE_HOTKEY], () => {
|
||||||
if (commandBarState.context.commands.length === 0) return
|
if (commandBarState.context.commands.length === 0) return
|
||||||
if (commandBarState.matches('Closed')) {
|
if (commandBarState.matches('Closed')) {
|
||||||
commandBarSend({ type: 'Open' })
|
commandBarActor.send({ type: 'Open' })
|
||||||
} else {
|
} else {
|
||||||
commandBarSend({ type: 'Close' })
|
commandBarActor.send({ type: 'Close' })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -52,14 +52,14 @@ export const CommandBar = () => {
|
|||||||
...entries[entries.length - 1][1],
|
...entries[entries.length - 1][1],
|
||||||
}
|
}
|
||||||
|
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Edit argument',
|
type: 'Edit argument',
|
||||||
data: {
|
data: {
|
||||||
arg: currentArg,
|
arg: currentArg,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
commandBarSend({ type: 'Deselect command' })
|
commandBarActor.send({ type: 'Deselect command' })
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const entries = Object.entries(selectedCommand?.args || {})
|
const entries = Object.entries(selectedCommand?.args || {})
|
||||||
@ -68,9 +68,9 @@ export const CommandBar = () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
commandBarSend({ type: 'Deselect command' })
|
commandBarActor.send({ type: 'Deselect command' })
|
||||||
} else {
|
} else {
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Change current argument',
|
type: 'Change current argument',
|
||||||
data: {
|
data: {
|
||||||
arg: { name: entries[index - 1][0], ...entries[index - 1][1] },
|
arg: { name: entries[index - 1][0], ...entries[index - 1][1] },
|
||||||
@ -85,14 +85,14 @@ export const CommandBar = () => {
|
|||||||
show={!commandBarState.matches('Closed') || false}
|
show={!commandBarState.matches('Closed') || false}
|
||||||
afterLeave={() => {
|
afterLeave={() => {
|
||||||
if (selectedCommand?.onCancel) selectedCommand.onCancel()
|
if (selectedCommand?.onCancel) selectedCommand.onCancel()
|
||||||
commandBarSend({ type: 'Clear' })
|
commandBarActor.send({ type: 'Clear' })
|
||||||
}}
|
}}
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
>
|
>
|
||||||
<WrapperComponent
|
<WrapperComponent
|
||||||
open={!commandBarState.matches('Closed') || isSelectionArgument}
|
open={!commandBarState.matches('Closed') || isSelectionArgument}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
commandBarSend({ type: 'Close' })
|
commandBarActor.send({ type: 'Close' })
|
||||||
}}
|
}}
|
||||||
className={
|
className={
|
||||||
'fixed inset-0 z-50 overflow-y-auto pb-4 pt-1 ' +
|
'fixed inset-0 z-50 overflow-y-auto pb-4 pt-1 ' +
|
||||||
@ -122,7 +122,7 @@ export const CommandBar = () => {
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={() => 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"
|
className="group block !absolute left-auto right-full top-[-3px] m-2.5 p-0 border-none bg-transparent hover:bg-transparent"
|
||||||
>
|
>
|
||||||
<CustomIcon
|
<CustomIcon
|
||||||
|
@ -2,13 +2,13 @@ import CommandArgOptionInput from './CommandArgOptionInput'
|
|||||||
import CommandBarBasicInput from './CommandBarBasicInput'
|
import CommandBarBasicInput from './CommandBarBasicInput'
|
||||||
import CommandBarSelectionInput from './CommandBarSelectionInput'
|
import CommandBarSelectionInput from './CommandBarSelectionInput'
|
||||||
import { CommandArgument } from 'lib/commandTypes'
|
import { CommandArgument } from 'lib/commandTypes'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import CommandBarHeader from './CommandBarHeader'
|
import CommandBarHeader from './CommandBarHeader'
|
||||||
import CommandBarKclInput from './CommandBarKclInput'
|
import CommandBarKclInput from './CommandBarKclInput'
|
||||||
import CommandBarTextareaInput from './CommandBarTextareaInput'
|
import CommandBarTextareaInput from './CommandBarTextareaInput'
|
||||||
|
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||||
|
|
||||||
function CommandBarArgument({ stepBack }: { stepBack: () => void }) {
|
function CommandBarArgument({ stepBack }: { stepBack: () => void }) {
|
||||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
const commandBarState = useCommandBarState()
|
||||||
const {
|
const {
|
||||||
context: { currentArgument },
|
context: { currentArgument },
|
||||||
} = commandBarState
|
} = commandBarState
|
||||||
@ -16,7 +16,7 @@ function CommandBarArgument({ stepBack }: { stepBack: () => void }) {
|
|||||||
function onSubmit(data: unknown) {
|
function onSubmit(data: unknown) {
|
||||||
if (!currentArgument) return
|
if (!currentArgument) return
|
||||||
|
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Submit argument',
|
type: 'Submit argument',
|
||||||
data: {
|
data: {
|
||||||
[currentArgument.name]: data,
|
[currentArgument.name]: data,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import { CommandArgument } from 'lib/commandTypes'
|
import { CommandArgument } from 'lib/commandTypes'
|
||||||
|
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
|
||||||
@ -15,8 +15,8 @@ function CommandBarBasicInput({
|
|||||||
stepBack: () => void
|
stepBack: () => void
|
||||||
onSubmit: (event: unknown) => void
|
onSubmit: (event: unknown) => void
|
||||||
}) {
|
}) {
|
||||||
const { commandBarSend, commandBarState } = useCommandsContext()
|
const commandBarState = useCommandBarState()
|
||||||
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
|
useHotkeys('mod + k, mod + /', () => commandBarActor.send({ type: 'Close' }))
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import { CustomIcon } from '../CustomIcon'
|
import { CustomIcon } from '../CustomIcon'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { ActionButton } from '../ActionButton'
|
import { ActionButton } from '../ActionButton'
|
||||||
@ -7,9 +6,10 @@ import { useHotkeys } from 'react-hotkeys-hook'
|
|||||||
import { KclCommandValue, KclExpressionWithVariable } from 'lib/commandTypes'
|
import { KclCommandValue, KclExpressionWithVariable } from 'lib/commandTypes'
|
||||||
import Tooltip from 'components/Tooltip'
|
import Tooltip from 'components/Tooltip'
|
||||||
import { roundOff } from 'lib/utils'
|
import { roundOff } from 'lib/utils'
|
||||||
|
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||||
|
|
||||||
function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
|
function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
|
||||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
const commandBarState = useCommandBarState()
|
||||||
const {
|
const {
|
||||||
context: { selectedCommand, currentArgument, argumentsToSubmit },
|
context: { selectedCommand, currentArgument, argumentsToSubmit },
|
||||||
} = commandBarState
|
} = commandBarState
|
||||||
@ -49,7 +49,7 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
|
|||||||
]
|
]
|
||||||
const arg = selectedCommand?.args[argName]
|
const arg = selectedCommand?.args[argName]
|
||||||
if (!argName || !arg) return
|
if (!argName || !arg) return
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Change current argument',
|
type: 'Change current argument',
|
||||||
data: { arg: { ...arg, name: argName } },
|
data: { arg: { ...arg, name: argName } },
|
||||||
})
|
})
|
||||||
@ -100,7 +100,7 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
|
|||||||
}
|
}
|
||||||
disabled={!isReviewing && currentArgument?.name === argName}
|
disabled={!isReviewing && currentArgument?.name === argName}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: isReviewing
|
type: isReviewing
|
||||||
? 'Edit argument'
|
? 'Edit argument'
|
||||||
: 'Change current argument',
|
: 'Change current argument',
|
||||||
|
@ -7,7 +7,6 @@ import {
|
|||||||
} from '@codemirror/autocomplete'
|
} from '@codemirror/autocomplete'
|
||||||
import { EditorView, keymap, ViewUpdate } from '@codemirror/view'
|
import { EditorView, keymap, ViewUpdate } from '@codemirror/view'
|
||||||
import { CustomIcon } from 'components/CustomIcon'
|
import { CustomIcon } from 'components/CustomIcon'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
import { CommandArgument, KclCommandValue } from 'lib/commandTypes'
|
import { CommandArgument, KclCommandValue } from 'lib/commandTypes'
|
||||||
import { getSystemTheme } from 'lib/theme'
|
import { getSystemTheme } from 'lib/theme'
|
||||||
@ -20,6 +19,7 @@ import styles from './CommandBarKclInput.module.css'
|
|||||||
import { createIdentifier, createVariableDeclaration } from 'lang/modifyAst'
|
import { createIdentifier, createVariableDeclaration } from 'lang/modifyAst'
|
||||||
import { useCodeMirror } from 'components/ModelingSidebar/ModelingPanes/CodeEditor'
|
import { useCodeMirror } from 'components/ModelingSidebar/ModelingPanes/CodeEditor'
|
||||||
import { useSelector } from '@xstate/react'
|
import { useSelector } from '@xstate/react'
|
||||||
|
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||||
|
|
||||||
const machineContextSelector = (snapshot?: {
|
const machineContextSelector = (snapshot?: {
|
||||||
context: Record<string, unknown>
|
context: Record<string, unknown>
|
||||||
@ -37,7 +37,7 @@ function CommandBarKclInput({
|
|||||||
stepBack: () => void
|
stepBack: () => void
|
||||||
onSubmit: (event: unknown) => void
|
onSubmit: (event: unknown) => void
|
||||||
}) {
|
}) {
|
||||||
const { commandBarSend, commandBarState } = useCommandsContext()
|
const commandBarState = useCommandBarState()
|
||||||
const previouslySetValue = commandBarState.context.argumentsToSubmit[
|
const previouslySetValue = commandBarState.context.argumentsToSubmit[
|
||||||
arg.name
|
arg.name
|
||||||
] as KclCommandValue | undefined
|
] as KclCommandValue | undefined
|
||||||
@ -82,7 +82,7 @@ function CommandBarKclInput({
|
|||||||
false
|
false
|
||||||
)
|
)
|
||||||
const [canSubmit, setCanSubmit] = useState(true)
|
const [canSubmit, setCanSubmit] = useState(true)
|
||||||
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
|
useHotkeys('mod + k, mod + /', () => commandBarActor.send({ type: 'Close' }))
|
||||||
const editorRef = useRef<HTMLDivElement>(null)
|
const editorRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const {
|
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 CommandBarHeader from './CommandBarHeader'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
|
||||||
function CommandBarReview({ stepBack }: { stepBack: () => void }) {
|
function CommandBarReview({ stepBack }: { stepBack: () => void }) {
|
||||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
const commandBarState = useCommandBarState()
|
||||||
const {
|
const {
|
||||||
context: { argumentsToSubmit, selectedCommand },
|
context: { argumentsToSubmit, selectedCommand },
|
||||||
} = commandBarState
|
} = commandBarState
|
||||||
@ -33,7 +33,7 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) {
|
|||||||
parseInt(b.keys[0], 10) - 1
|
parseInt(b.keys[0], 10) - 1
|
||||||
]
|
]
|
||||||
const arg = selectedCommand?.args[argName]
|
const arg = selectedCommand?.args[argName]
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Edit argument',
|
type: 'Edit argument',
|
||||||
data: { arg: { ...arg, name: argName } },
|
data: { arg: { ...arg, name: argName } },
|
||||||
})
|
})
|
||||||
@ -50,7 +50,7 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) {
|
|||||||
|
|
||||||
function submitCommand(e: React.FormEvent<HTMLFormElement>) {
|
function submitCommand(e: React.FormEvent<HTMLFormElement>) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Submit command',
|
type: 'Submit command',
|
||||||
output: argumentsToSubmit,
|
output: argumentsToSubmit,
|
||||||
})
|
})
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { useSelector } from '@xstate/react'
|
import { useSelector } from '@xstate/react'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import { Artifact } from 'lang/std/artifactGraph'
|
import { Artifact } from 'lang/std/artifactGraph'
|
||||||
import { CommandArgument } from 'lib/commandTypes'
|
import { CommandArgument } from 'lib/commandTypes'
|
||||||
import {
|
import {
|
||||||
@ -10,6 +9,7 @@ import {
|
|||||||
import { kclManager } from 'lib/singletons'
|
import { kclManager } from 'lib/singletons'
|
||||||
import { reportRejection } from 'lib/trap'
|
import { reportRejection } from 'lib/trap'
|
||||||
import { toSync } from 'lib/utils'
|
import { toSync } from 'lib/utils'
|
||||||
|
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||||
import { modelingMachine } from 'machines/modelingMachine'
|
import { modelingMachine } from 'machines/modelingMachine'
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { StateFrom } from 'xstate'
|
import { StateFrom } from 'xstate'
|
||||||
@ -49,7 +49,7 @@ function CommandBarSelectionInput({
|
|||||||
onSubmit: (data: unknown) => void
|
onSubmit: (data: unknown) => void
|
||||||
}) {
|
}) {
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
const commandBarState = useCommandBarState()
|
||||||
const [hasSubmitted, setHasSubmitted] = useState(false)
|
const [hasSubmitted, setHasSubmitted] = useState(false)
|
||||||
const selection = useSelector(arg.machineActor, selectionSelector)
|
const selection = useSelector(arg.machineActor, selectionSelector)
|
||||||
const selectionsByType = useMemo(() => {
|
const selectionsByType = useMemo(() => {
|
||||||
@ -145,7 +145,7 @@ function CommandBarSelectionInput({
|
|||||||
if (event.key === 'Backspace') {
|
if (event.key === 'Backspace') {
|
||||||
stepBack()
|
stepBack()
|
||||||
} else if (event.key === 'Escape') {
|
} else if (event.key === 'Escape') {
|
||||||
commandBarSend({ type: 'Close' })
|
commandBarActor.send({ type: 'Close' })
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import { CommandArgument } from 'lib/commandTypes'
|
import { CommandArgument } from 'lib/commandTypes'
|
||||||
|
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||||
import { RefObject, useEffect, useRef } from 'react'
|
import { RefObject, useEffect, useRef } from 'react'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
|
||||||
@ -15,8 +15,8 @@ function CommandBarTextareaInput({
|
|||||||
stepBack: () => void
|
stepBack: () => void
|
||||||
onSubmit: (event: unknown) => void
|
onSubmit: (event: unknown) => void
|
||||||
}) {
|
}) {
|
||||||
const { commandBarSend, commandBarState } = useCommandsContext()
|
const commandBarState = useCommandBarState()
|
||||||
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
|
useHotkeys('mod + k, mod + /', () => commandBarActor.send({ type: 'Close' }))
|
||||||
const formRef = useRef<HTMLFormElement>(null)
|
const formRef = useRef<HTMLFormElement>(null)
|
||||||
const inputRef = useRef<HTMLTextAreaElement>(null)
|
const inputRef = useRef<HTMLTextAreaElement>(null)
|
||||||
useTextareaAutoGrow(inputRef)
|
useTextareaAutoGrow(inputRef)
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import usePlatform from 'hooks/usePlatform'
|
import usePlatform from 'hooks/usePlatform'
|
||||||
import { hotkeyDisplay } from 'lib/hotkeyWrapper'
|
import { hotkeyDisplay } from 'lib/hotkeyWrapper'
|
||||||
import { COMMAND_PALETTE_HOTKEY } from './CommandBar/CommandBar'
|
import { COMMAND_PALETTE_HOTKEY } from './CommandBar/CommandBar'
|
||||||
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
|
|
||||||
export function CommandBarOpenButton() {
|
export function CommandBarOpenButton() {
|
||||||
const { commandBarSend } = useCommandsContext()
|
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className="group rounded-full flex items-center justify-center gap-2 px-2 py-1 bg-primary/10 dark:bg-chalkboard-90 dark:backdrop-blur-sm border-primary hover:border-primary dark:border-chalkboard-50 dark:hover:border-inherit text-primary dark:text-inherit"
|
className="group rounded-full flex items-center justify-center gap-2 px-2 py-1 bg-primary/10 dark:bg-chalkboard-90 dark:backdrop-blur-sm border-primary hover:border-primary dark:border-chalkboard-50 dark:hover:border-inherit text-primary dark:text-inherit"
|
||||||
onClick={() => commandBarSend({ type: 'Open' })}
|
onClick={() => commandBarActor.send({ type: 'Open' })}
|
||||||
data-testid="command-bar-open-button"
|
data-testid="command-bar-open-button"
|
||||||
>
|
>
|
||||||
<span>Commands</span>
|
<span>Commands</span>
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { Combobox } from '@headlessui/react'
|
import { Combobox } from '@headlessui/react'
|
||||||
import Fuse from 'fuse.js'
|
import Fuse from 'fuse.js'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import { Command } from 'lib/commandTypes'
|
import { Command } from 'lib/commandTypes'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { CustomIcon } from './CustomIcon'
|
import { CustomIcon } from './CustomIcon'
|
||||||
import { getActorNextEvents } from 'lib/utils'
|
import { getActorNextEvents } from 'lib/utils'
|
||||||
import { sortCommands } from 'lib/commandUtils'
|
import { sortCommands } from 'lib/commandUtils'
|
||||||
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
|
|
||||||
function CommandComboBox({
|
function CommandComboBox({
|
||||||
options,
|
options,
|
||||||
@ -14,7 +14,6 @@ function CommandComboBox({
|
|||||||
options: Command[]
|
options: Command[]
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
}) {
|
}) {
|
||||||
const { commandBarSend } = useCommandsContext()
|
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState('')
|
||||||
const [filteredOptions, setFilteredOptions] = useState<typeof options>()
|
const [filteredOptions, setFilteredOptions] = useState<typeof options>()
|
||||||
|
|
||||||
@ -41,7 +40,7 @@ function CommandComboBox({
|
|||||||
}, [query])
|
}, [query])
|
||||||
|
|
||||||
function handleSelection(command: Command) {
|
function handleSelection(command: Command) {
|
||||||
commandBarSend({ type: 'Select command', data: { command } })
|
commandBarActor.send({ type: 'Select command', data: { command } })
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -61,7 +60,7 @@ function CommandComboBox({
|
|||||||
(event.key === 'Backspace' && !event.currentTarget.value)
|
(event.key === 'Backspace' && !event.currentTarget.value)
|
||||||
) {
|
) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
commandBarSend({ type: 'Close' })
|
commandBarActor.send({ type: 'Close' })
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder={
|
placeholder={
|
||||||
@ -76,34 +75,40 @@ function CommandComboBox({
|
|||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Combobox.Options
|
{filteredOptions?.length ? (
|
||||||
static
|
<Combobox.Options
|
||||||
className="overflow-y-auto max-h-96 cursor-pointer"
|
static
|
||||||
>
|
className="overflow-y-auto max-h-96 cursor-pointer"
|
||||||
{filteredOptions?.map((option) => (
|
>
|
||||||
<Combobox.Option
|
{filteredOptions?.map((option) => (
|
||||||
key={option.groupId + option.name + (option.displayName || '')}
|
<Combobox.Option
|
||||||
value={option}
|
key={option.groupId + option.name + (option.displayName || '')}
|
||||||
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"
|
value={option}
|
||||||
disabled={optionIsDisabled(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"
|
||||||
data-testid={`cmd-bar-option`}
|
disabled={optionIsDisabled(option)}
|
||||||
>
|
data-testid={`cmd-bar-option`}
|
||||||
{'icon' in option && option.icon && (
|
>
|
||||||
<CustomIcon name={option.icon} className="w-5 h-5" />
|
{'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>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
<div className="flex-grow flex flex-col">
|
||||||
</Combobox.Option>
|
<p className="my-0 leading-tight">
|
||||||
))}
|
{option.displayName || option.name}{' '}
|
||||||
</Combobox.Options>
|
</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>
|
</Combobox>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -4,18 +4,18 @@ import { expandPlane, PlaneArtifactRich } from 'lang/std/artifactGraph'
|
|||||||
import { ArtifactGraph } from 'lang/wasm'
|
import { ArtifactGraph } from 'lang/wasm'
|
||||||
import { DebugDisplayArray, GenericObj } from './DebugDisplayObj'
|
import { DebugDisplayArray, GenericObj } from './DebugDisplayObj'
|
||||||
|
|
||||||
export function DebugFeatureTree() {
|
export function DebugArtifactGraph() {
|
||||||
const featureTree = useMemo(() => {
|
const artifactGraphTree = useMemo(() => {
|
||||||
return computeTree(engineCommandManager.artifactGraph)
|
return computeTree(engineCommandManager.artifactGraph)
|
||||||
}, [engineCommandManager.artifactGraph])
|
}, [engineCommandManager.artifactGraph])
|
||||||
|
|
||||||
const filterKeys: string[] = ['__meta', 'codeRef', 'pathToNode']
|
const filterKeys: string[] = ['__meta', 'codeRef', 'pathToNode']
|
||||||
return (
|
return (
|
||||||
<details data-testid="debug-feature-tree" className="relative">
|
<details data-testid="debug-feature-tree" className="relative">
|
||||||
<summary>Feature Tree</summary>
|
<summary>Artifact Graph</summary>
|
||||||
{featureTree.length > 0 ? (
|
{artifactGraphTree.length > 0 ? (
|
||||||
<pre className="text-xs">
|
<pre className="text-xs">
|
||||||
<DebugDisplayArray arr={featureTree} filterKeys={filterKeys} />
|
<DebugDisplayArray arr={artifactGraphTree} filterKeys={filterKeys} />
|
||||||
</pre>
|
</pre>
|
||||||
) : (
|
) : (
|
||||||
<p>(Empty)</p>
|
<p>(Empty)</p>
|
@ -12,7 +12,6 @@ import {
|
|||||||
StateFrom,
|
StateFrom,
|
||||||
fromPromise,
|
fromPromise,
|
||||||
} from 'xstate'
|
} from 'xstate'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import { fileMachine } from 'machines/fileMachine'
|
import { fileMachine } from 'machines/fileMachine'
|
||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
import {
|
import {
|
||||||
@ -30,6 +29,7 @@ import {
|
|||||||
} from 'lib/getKclSamplesManifest'
|
} from 'lib/getKclSamplesManifest'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
import { markOnce } from 'lib/performance'
|
import { markOnce } from 'lib/performance'
|
||||||
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
@ -47,9 +47,9 @@ export const FileMachineProvider = ({
|
|||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { commandBarSend } = useCommandsContext()
|
const { settings, auth } = useSettingsAuthContext()
|
||||||
const { settings } = useSettingsAuthContext()
|
const projectData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
||||||
const { project, file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
const { project, file } = projectData
|
||||||
const [kclSamples, setKclSamples] = React.useState<KclSamplesManifestItem[]>(
|
const [kclSamples, setKclSamples] = React.useState<KclSamplesManifestItem[]>(
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
@ -90,7 +90,7 @@ export const FileMachineProvider = ({
|
|||||||
navigateToFile: ({ context, event }) => {
|
navigateToFile: ({ context, event }) => {
|
||||||
if (event.type !== 'xstate.done.actor.create-and-open-file') return
|
if (event.type !== 'xstate.done.actor.create-and-open-file') return
|
||||||
if (event.output && 'name' in event.output) {
|
if (event.output && 'name' in event.output) {
|
||||||
commandBarSend({ type: 'Close' })
|
commandBarActor.send({ type: 'Close' })
|
||||||
navigate(
|
navigate(
|
||||||
`..${PATHS.FILE}/${encodeURIComponent(
|
`..${PATHS.FILE}/${encodeURIComponent(
|
||||||
context.selectedDirectory +
|
context.selectedDirectory +
|
||||||
@ -296,55 +296,65 @@ export const FileMachineProvider = ({
|
|||||||
|
|
||||||
const kclCommandMemo = useMemo(
|
const kclCommandMemo = useMemo(
|
||||||
() =>
|
() =>
|
||||||
kclCommands(
|
kclCommands({
|
||||||
async (data) => {
|
authToken: auth?.context?.token ?? '',
|
||||||
if (data.method === 'overwrite') {
|
projectData,
|
||||||
codeManager.updateCodeStateEditor(data.code)
|
settings: {
|
||||||
await kclManager.executeCode(true)
|
defaultUnit: settings?.context?.modeling.defaultUnit.current ?? 'mm',
|
||||||
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,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
kclSamples.map((sample) => ({
|
specialPropsForSampleCommand: {
|
||||||
value: sample.pathFromProjectDirectoryToFirstFile,
|
onSubmit: async (data) => {
|
||||||
name: sample.title,
|
if (data.method === 'overwrite') {
|
||||||
}))
|
codeManager.updateCodeStateEditor(data.code)
|
||||||
).filter(
|
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'
|
(command) => kclSamples.length || command.name !== 'open-kcl-example'
|
||||||
),
|
),
|
||||||
[codeManager, kclManager, send, kclSamples]
|
[codeManager, kclManager, send, kclSamples]
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
commandBarSend({ type: 'Add commands', data: { commands: kclCommandMemo } })
|
commandBarActor.send({
|
||||||
|
type: 'Add commands',
|
||||||
|
data: { commands: kclCommandMemo },
|
||||||
|
})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Remove commands',
|
type: 'Remove commands',
|
||||||
data: { commands: kclCommandMemo },
|
data: { commands: kclCommandMemo },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [commandBarSend, kclCommandMemo])
|
}, [commandBarActor.send, kclCommandMemo])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FileContext.Provider
|
<FileContext.Provider
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { createContext, useEffect, useState } from 'react'
|
import { createContext, useEffect, useState } from 'react'
|
||||||
|
|
||||||
import { engineCommandManager } from 'lib/singletons'
|
import { engineCommandManager } from 'lib/singletons'
|
||||||
import { CommandsContext } from 'components/CommandBar/CommandBarProvider'
|
|
||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
import { components } from 'lib/machine-api'
|
import { components } from 'lib/machine-api'
|
||||||
import { reportRejection } from 'lib/trap'
|
import { reportRejection } from 'lib/trap'
|
||||||
import { toSync } from 'lib/utils'
|
import { toSync } from 'lib/utils'
|
||||||
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
|
|
||||||
export type MachinesListing = Array<
|
export type MachinesListing = Array<
|
||||||
components['schemas']['MachineInfoResponse']
|
components['schemas']['MachineInfoResponse']
|
||||||
@ -42,8 +42,6 @@ export const MachineManagerProvider = ({
|
|||||||
components['schemas']['MachineInfoResponse'] | null
|
components['schemas']['MachineInfoResponse'] | null
|
||||||
>(null)
|
>(null)
|
||||||
|
|
||||||
const commandBarActor = CommandsContext.useActorRef()
|
|
||||||
|
|
||||||
// Get the reason message for why there are no machines.
|
// Get the reason message for why there are no machines.
|
||||||
const noMachinesReason = (): string | undefined => {
|
const noMachinesReason = (): string | undefined => {
|
||||||
if (machines.length > 0) {
|
if (machines.length > 0) {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useMachine } from '@xstate/react'
|
import { useMachine, useSelector } from '@xstate/react'
|
||||||
import React, {
|
import React, {
|
||||||
createContext,
|
createContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
@ -11,6 +11,7 @@ import {
|
|||||||
AnyStateMachine,
|
AnyStateMachine,
|
||||||
ContextFrom,
|
ContextFrom,
|
||||||
Prop,
|
Prop,
|
||||||
|
SnapshotFrom,
|
||||||
StateFrom,
|
StateFrom,
|
||||||
assign,
|
assign,
|
||||||
fromPromise,
|
fromPromise,
|
||||||
@ -78,7 +79,6 @@ import toast from 'react-hot-toast'
|
|||||||
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
|
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
|
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
|
||||||
import { err, reportRejection, trap } from 'lib/trap'
|
import { err, reportRejection, trap } from 'lib/trap'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import {
|
import {
|
||||||
ExportIntent,
|
ExportIntent,
|
||||||
EngineConnectionStateType,
|
EngineConnectionStateType,
|
||||||
@ -91,6 +91,7 @@ import { IndexLoaderData } from 'lib/types'
|
|||||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||||
import { promptToEditFlow } from 'lib/promptToEdit'
|
import { promptToEditFlow } from 'lib/promptToEdit'
|
||||||
import { kclEditorActor } from 'machines/kclEditorMachine'
|
import { kclEditorActor } from 'machines/kclEditorMachine'
|
||||||
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
@ -102,6 +103,10 @@ export const ModelingMachineContext = createContext(
|
|||||||
{} as MachineContext<typeof modelingMachine>
|
{} as MachineContext<typeof modelingMachine>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const commandBarIsClosedSelector = (
|
||||||
|
state: SnapshotFrom<typeof commandBarActor>
|
||||||
|
) => state.matches('Closed')
|
||||||
|
|
||||||
export const ModelingMachineProvider = ({
|
export const ModelingMachineProvider = ({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
@ -132,8 +137,10 @@ export const ModelingMachineProvider = ({
|
|||||||
let [searchParams] = useSearchParams()
|
let [searchParams] = useSearchParams()
|
||||||
const pool = searchParams.get('pool')
|
const pool = searchParams.get('pool')
|
||||||
|
|
||||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
const isCommandBarClosed = useSelector(
|
||||||
|
commandBarActor,
|
||||||
|
commandBarIsClosedSelector
|
||||||
|
)
|
||||||
// Settings machine setup
|
// Settings machine setup
|
||||||
// const retrievedSettings = useRef(
|
// const retrievedSettings = useRef(
|
||||||
// localStorage?.getItem(MODELING_PERSIST_KEY) || '{}'
|
// localStorage?.getItem(MODELING_PERSIST_KEY) || '{}'
|
||||||
@ -388,7 +395,16 @@ export const ModelingMachineProvider = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (setSelections.selectionType === 'completeSelection') {
|
if (setSelections.selectionType === 'completeSelection') {
|
||||||
editorManager.selectRange(setSelections.selection)
|
const codeMirrorSelection = editorManager.createEditorSelection(
|
||||||
|
setSelections.selection
|
||||||
|
)
|
||||||
|
kclEditorActor.send({
|
||||||
|
type: 'setLastSelectionEvent',
|
||||||
|
data: {
|
||||||
|
codeMirrorSelection,
|
||||||
|
scrollIntoView: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
if (!sketchDetails)
|
if (!sketchDetails)
|
||||||
return {
|
return {
|
||||||
selectionRanges: setSelections.selection,
|
selectionRanges: setSelections.selection,
|
||||||
@ -529,7 +545,6 @@ export const ModelingMachineProvider = ({
|
|||||||
trimmedPrompt,
|
trimmedPrompt,
|
||||||
fileMachineSend,
|
fileMachineSend,
|
||||||
navigate,
|
navigate,
|
||||||
commandBarSend,
|
|
||||||
context,
|
context,
|
||||||
token,
|
token,
|
||||||
settings: {
|
settings: {
|
||||||
@ -543,7 +558,7 @@ export const ModelingMachineProvider = ({
|
|||||||
'has valid selection for deletion': ({
|
'has valid selection for deletion': ({
|
||||||
context: { selectionRanges },
|
context: { selectionRanges },
|
||||||
}) => {
|
}) => {
|
||||||
if (!commandBarState.matches('Closed')) return false
|
if (!isCommandBarClosed) return false
|
||||||
if (selectionRanges.graphSelections.length <= 0) return false
|
if (selectionRanges.graphSelections.length <= 0) return false
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { DebugFeatureTree } from 'components/DebugFeatureTree'
|
import { DebugArtifactGraph } from 'components/DebugArtifactGraph'
|
||||||
import { AstExplorer } from '../../AstExplorer'
|
import { AstExplorer } from '../../AstExplorer'
|
||||||
import { EngineCommands } from '../../EngineCommands'
|
import { EngineCommands } from '../../EngineCommands'
|
||||||
import { CamDebugSettings } from 'clientSideScene/ClientSideSceneComp'
|
import { CamDebugSettings } from 'clientSideScene/ClientSideSceneComp'
|
||||||
@ -14,7 +14,7 @@ export const DebugPane = () => {
|
|||||||
<EngineCommands />
|
<EngineCommands />
|
||||||
<CamDebugSettings />
|
<CamDebugSettings />
|
||||||
<AstExplorer />
|
<AstExplorer />
|
||||||
<DebugFeatureTree />
|
<DebugArtifactGraph />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
@apply font-mono !no-underline text-xs font-bold select-none text-chalkboard-90;
|
@apply font-mono !no-underline text-xs font-bold select-none text-chalkboard-90;
|
||||||
@apply ui-active:bg-primary/10 ui-active:text-primary ui-active:text-inherit;
|
@apply ui-active:bg-primary/10 ui-active:text-primary ui-active:text-inherit;
|
||||||
@apply transition-colors ease-out;
|
@apply transition-colors ease-out;
|
||||||
|
@apply m-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dark) .button {
|
:global(.dark) .button {
|
||||||
|
@ -9,12 +9,11 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|||||||
import { kclManager } from 'lib/singletons'
|
import { kclManager } from 'lib/singletons'
|
||||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||||
import { reportRejection } from 'lib/trap'
|
import { reportRejection } from 'lib/trap'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
|
|
||||||
export const KclEditorMenu = ({ children }: PropsWithChildren) => {
|
export const KclEditorMenu = ({ children }: PropsWithChildren) => {
|
||||||
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
|
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
|
||||||
useConvertToVariable()
|
useConvertToVariable()
|
||||||
const { commandBarSend } = useCommandsContext()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu>
|
<Menu>
|
||||||
@ -85,7 +84,7 @@ export const KclEditorMenu = ({ children }: PropsWithChildren) => {
|
|||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: {
|
data: {
|
||||||
groupId: 'code',
|
groupId: 'code',
|
||||||
|
@ -95,9 +95,11 @@ export const processMemory = (programMemory: ProgramMemory) => {
|
|||||||
) {
|
) {
|
||||||
const sk = sketchFromKclValueOptional(val, key)
|
const sk = sketchFromKclValueOptional(val, key)
|
||||||
if (val.type === 'Solid') {
|
if (val.type === 'Solid') {
|
||||||
processedMemory[key] = val.value.map(({ ...rest }: ExtrudeSurface) => {
|
processedMemory[key] = val.value.value.map(
|
||||||
return rest
|
({ ...rest }: ExtrudeSurface) => {
|
||||||
})
|
return rest
|
||||||
|
}
|
||||||
|
)
|
||||||
} else if (!(sk instanceof Reason)) {
|
} else if (!(sk instanceof Reason)) {
|
||||||
processedMemory[key] = sk.paths.map(({ __geoMeta, ...rest }: Path) => {
|
processedMemory[key] = sk.paths.map(({ __geoMeta, ...rest }: Path) => {
|
||||||
return rest
|
return rest
|
||||||
|
@ -15,12 +15,12 @@ import { ModelingPane } from './ModelingPane'
|
|||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
import { CustomIconName } from 'components/CustomIcon'
|
import { CustomIconName } from 'components/CustomIcon'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
|
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { useKclContext } from 'lang/KclProvider'
|
import { useKclContext } from 'lang/KclProvider'
|
||||||
import { MachineManagerContext } from 'components/MachineManagerProvider'
|
import { MachineManagerContext } from 'components/MachineManagerProvider'
|
||||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||||
import { SIDEBAR_BUTTON_SUFFIX } from 'lib/constants'
|
import { SIDEBAR_BUTTON_SUFFIX } from 'lib/constants'
|
||||||
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
|
|
||||||
interface ModelingSidebarProps {
|
interface ModelingSidebarProps {
|
||||||
paneOpacity: '' | 'opacity-20' | 'opacity-40'
|
paneOpacity: '' | 'opacity-20' | 'opacity-40'
|
||||||
@ -37,7 +37,6 @@ function getPlatformString(): 'web' | 'desktop' {
|
|||||||
|
|
||||||
export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
||||||
const machineManager = useContext(MachineManagerContext)
|
const machineManager = useContext(MachineManagerContext)
|
||||||
const { commandBarSend } = useCommandsContext()
|
|
||||||
const kclContext = useKclContext()
|
const kclContext = useKclContext()
|
||||||
const { settings } = useSettingsAuthContext()
|
const { settings } = useSettingsAuthContext()
|
||||||
const onboardingStatus = settings.context.app.onboardingStatus
|
const onboardingStatus = settings.context.app.onboardingStatus
|
||||||
@ -66,7 +65,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
|||||||
icon: 'floppyDiskArrow',
|
icon: 'floppyDiskArrow',
|
||||||
keybinding: 'Ctrl + Shift + E',
|
keybinding: 'Ctrl + Shift + E',
|
||||||
action: () =>
|
action: () =>
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: { name: 'Export', groupId: 'modeling' },
|
data: { name: 'Export', groupId: 'modeling' },
|
||||||
}),
|
}),
|
||||||
@ -79,7 +78,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
|||||||
keybinding: 'Ctrl + Shift + M',
|
keybinding: 'Ctrl + Shift + M',
|
||||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
action: async () => {
|
action: async () => {
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: { name: 'Make', groupId: 'modeling' },
|
data: { name: 'Make', groupId: 'modeling' },
|
||||||
})
|
})
|
||||||
@ -298,7 +297,7 @@ function ModelingPaneButton({
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id={paneConfig.id + '-button-holder'}>
|
<div id={paneConfig.id + '-button-holder'} className="relative">
|
||||||
<button
|
<button
|
||||||
className="group pointer-events-auto flex items-center justify-center border-transparent dark:border-transparent disabled:!border-transparent p-0 m-0 rounded-sm !outline-0 focus-visible:border-primary"
|
className="group pointer-events-auto flex items-center justify-center border-transparent dark:border-transparent disabled:!border-transparent p-0 m-0 rounded-sm !outline-0 focus-visible:border-primary"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
@ -340,7 +339,7 @@ function ModelingPaneButton({
|
|||||||
<p
|
<p
|
||||||
id={`${paneConfig.id}-badge`}
|
id={`${paneConfig.id}-badge`}
|
||||||
className={
|
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}
|
onClick={showBadge.onClick}
|
||||||
title={`Click to view ${showBadge.value} notification${
|
title={`Click to view ${showBadge.value} notification${
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import { BrowserRouter } from 'react-router-dom'
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
|
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
|
||||||
import { CommandBarProvider } from './CommandBar/CommandBarProvider'
|
|
||||||
import {
|
import {
|
||||||
NETWORK_HEALTH_TEXT,
|
NETWORK_HEALTH_TEXT,
|
||||||
NetworkHealthIndicator,
|
NetworkHealthIndicator,
|
||||||
@ -12,9 +11,7 @@ function TestWrap({ children }: { children: React.ReactNode }) {
|
|||||||
// wrap in router and xState context
|
// wrap in router and xState context
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<CommandBarProvider>
|
<SettingsAuthProviderJest>{children}</SettingsAuthProviderJest>
|
||||||
<SettingsAuthProviderJest>{children}</SettingsAuthProviderJest>
|
|
||||||
</CommandBarProvider>
|
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
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 { BrowserRouter } from 'react-router-dom'
|
||||||
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
||||||
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
|
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
|
||||||
import { CommandBarProvider } from './CommandBar/CommandBarProvider'
|
|
||||||
import { Project } from 'lib/project'
|
import { Project } from 'lib/project'
|
||||||
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
@ -33,11 +32,9 @@ describe('ProjectSidebarMenu tests', () => {
|
|||||||
test('Disables popover menu by default', () => {
|
test('Disables popover menu by default', () => {
|
||||||
render(
|
render(
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<CommandBarProvider>
|
<SettingsAuthProviderJest>
|
||||||
<SettingsAuthProviderJest>
|
<ProjectSidebarMenu project={projectWellFormed} />
|
||||||
<ProjectSidebarMenu project={projectWellFormed} />
|
</SettingsAuthProviderJest>
|
||||||
</SettingsAuthProviderJest>
|
|
||||||
</CommandBarProvider>
|
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -7,14 +7,19 @@ import { Link, useLocation, useNavigate } from 'react-router-dom'
|
|||||||
import { Fragment, useMemo, useContext } from 'react'
|
import { Fragment, useMemo, useContext } from 'react'
|
||||||
import { Logo } from './Logo'
|
import { Logo } from './Logo'
|
||||||
import { APP_NAME } from 'lib/constants'
|
import { APP_NAME } from 'lib/constants'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import { CustomIcon } from './CustomIcon'
|
import { CustomIcon } from './CustomIcon'
|
||||||
import { useLspContext } from './LspProvider'
|
import { useLspContext } from './LspProvider'
|
||||||
import { engineCommandManager, kclManager } from 'lib/singletons'
|
import { codeManager, engineCommandManager, kclManager } from 'lib/singletons'
|
||||||
import { MachineManagerContext } from 'components/MachineManagerProvider'
|
import { MachineManagerContext } from 'components/MachineManagerProvider'
|
||||||
import usePlatform from 'hooks/usePlatform'
|
import usePlatform from 'hooks/usePlatform'
|
||||||
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
||||||
import Tooltip from './Tooltip'
|
import Tooltip from './Tooltip'
|
||||||
|
import { SnapshotFrom } from 'xstate'
|
||||||
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
|
import { useSelector } from '@xstate/react'
|
||||||
|
import { copyFileShareLink } from 'lib/links'
|
||||||
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
|
import { DEV } from 'env'
|
||||||
|
|
||||||
const ProjectSidebarMenu = ({
|
const ProjectSidebarMenu = ({
|
||||||
project,
|
project,
|
||||||
@ -84,6 +89,9 @@ function AppLogoLink({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const commandsSelector = (state: SnapshotFrom<typeof commandBarActor>) =>
|
||||||
|
state.context.commands
|
||||||
|
|
||||||
function ProjectMenuPopover({
|
function ProjectMenuPopover({
|
||||||
project,
|
project,
|
||||||
file,
|
file,
|
||||||
@ -95,17 +103,16 @@ function ProjectMenuPopover({
|
|||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const filePath = useAbsoluteFilePath()
|
const filePath = useAbsoluteFilePath()
|
||||||
|
const { settings, auth } = useSettingsAuthContext()
|
||||||
const machineManager = useContext(MachineManagerContext)
|
const machineManager = useContext(MachineManagerContext)
|
||||||
|
const commands = useSelector(commandBarActor, commandsSelector)
|
||||||
|
|
||||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
|
||||||
const { onProjectClose } = useLspContext()
|
const { onProjectClose } = useLspContext()
|
||||||
const exportCommandInfo = { name: 'Export', groupId: 'modeling' }
|
const exportCommandInfo = { name: 'Export', groupId: 'modeling' }
|
||||||
const makeCommandInfo = { name: 'Make', groupId: 'modeling' }
|
const makeCommandInfo = { name: 'Make', groupId: 'modeling' }
|
||||||
const findCommand = (obj: { name: string; groupId: string }) =>
|
const findCommand = (obj: { name: string; groupId: string }) =>
|
||||||
Boolean(
|
Boolean(
|
||||||
commandBarState.context.commands.find(
|
commands.find((c) => c.name === obj.name && c.groupId === obj.groupId)
|
||||||
(c) => c.name === obj.name && c.groupId === obj.groupId
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
const machineCount = machineManager.machines.length
|
const machineCount = machineManager.machines.length
|
||||||
|
|
||||||
@ -150,12 +157,11 @@ function ProjectMenuPopover({
|
|||||||
),
|
),
|
||||||
disabled: !findCommand(exportCommandInfo),
|
disabled: !findCommand(exportCommandInfo),
|
||||||
onClick: () =>
|
onClick: () =>
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: exportCommandInfo,
|
data: exportCommandInfo,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
'break',
|
|
||||||
{
|
{
|
||||||
id: 'make',
|
id: 'make',
|
||||||
Element: 'button',
|
Element: 'button',
|
||||||
@ -175,12 +181,26 @@ function ProjectMenuPopover({
|
|||||||
),
|
),
|
||||||
disabled: !findCommand(makeCommandInfo) || machineCount === 0,
|
disabled: !findCommand(makeCommandInfo) || machineCount === 0,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: makeCommandInfo,
|
data: makeCommandInfo,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'share-link',
|
||||||
|
Element: 'button',
|
||||||
|
children: 'Share link to file',
|
||||||
|
disabled: !DEV,
|
||||||
|
onClick: async () => {
|
||||||
|
await copyFileShareLink({
|
||||||
|
token: auth?.context.token || '',
|
||||||
|
code: codeManager.code,
|
||||||
|
name: project?.name || '',
|
||||||
|
units: settings.context.modeling.defaultUnit.current,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
'break',
|
'break',
|
||||||
{
|
{
|
||||||
id: 'go-home',
|
id: 'go-home',
|
||||||
@ -200,7 +220,7 @@ function ProjectMenuPopover({
|
|||||||
[
|
[
|
||||||
platform,
|
platform,
|
||||||
findCommand,
|
findCommand,
|
||||||
commandBarSend,
|
commandBarActor.send,
|
||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
onProjectClose,
|
onProjectClose,
|
||||||
isDesktop,
|
isDesktop,
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import { useMachine } from '@xstate/react'
|
import { useMachine } from '@xstate/react'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
||||||
import { useProjectsLoader } from 'hooks/useProjectsLoader'
|
import { useProjectsLoader } from 'hooks/useProjectsLoader'
|
||||||
import { projectsMachine } from 'machines/projectsMachine'
|
import { projectsMachine } from 'machines/projectsMachine'
|
||||||
import { createContext, useEffect, useState } from 'react'
|
import { createContext, useCallback, useEffect, useState } from 'react'
|
||||||
import { Actor, AnyStateMachine, fromPromise, Prop, StateFrom } from 'xstate'
|
import { Actor, AnyStateMachine, fromPromise, Prop, StateFrom } from 'xstate'
|
||||||
import { useLspContext } from './LspProvider'
|
import { useLspContext } from './LspProvider'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { useLocation, useNavigate } from 'react-router-dom'
|
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
import { PATHS } from 'lib/paths'
|
import { PATHS } from 'lib/paths'
|
||||||
import {
|
import {
|
||||||
createNewProjectDirectory,
|
createNewProjectDirectory,
|
||||||
@ -19,11 +18,28 @@ import {
|
|||||||
interpolateProjectNameWithIndex,
|
interpolateProjectNameWithIndex,
|
||||||
doesProjectNameNeedInterpolated,
|
doesProjectNameNeedInterpolated,
|
||||||
getUniqueProjectName,
|
getUniqueProjectName,
|
||||||
|
getNextFileName,
|
||||||
} from 'lib/desktopFS'
|
} from 'lib/desktopFS'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
import useStateMachineCommands from 'hooks/useStateMachineCommands'
|
import useStateMachineCommands from 'hooks/useStateMachineCommands'
|
||||||
import { projectsCommandBarConfig } from 'lib/commandBarConfigs/projectsCommandConfig'
|
import { projectsCommandBarConfig } from 'lib/commandBarConfigs/projectsCommandConfig'
|
||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
|
import {
|
||||||
|
CREATE_FILE_URL_PARAM,
|
||||||
|
FILE_EXT,
|
||||||
|
PROJECT_ENTRYPOINT,
|
||||||
|
} from 'lib/constants'
|
||||||
|
import { DeepPartial } from 'lib/types'
|
||||||
|
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
||||||
|
import { codeManager } from 'lib/singletons'
|
||||||
|
import {
|
||||||
|
loadAndValidateSettings,
|
||||||
|
projectConfigurationToSettingsPayload,
|
||||||
|
saveSettings,
|
||||||
|
setSettingsAtLevel,
|
||||||
|
} from 'lib/settings/settingsUtils'
|
||||||
|
import { Project } from 'lib/project'
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
state?: StateFrom<T>
|
state?: StateFrom<T>
|
||||||
@ -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 ProjectsContextWeb = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
|
const clearImportSearchParams = useCallback(() => {
|
||||||
|
// Clear the search parameters related to the "Import file from URL" command
|
||||||
|
// or we'll never be able cancel or submit it.
|
||||||
|
searchParams.delete(CREATE_FILE_URL_PARAM)
|
||||||
|
searchParams.delete('code')
|
||||||
|
searchParams.delete('name')
|
||||||
|
searchParams.delete('units')
|
||||||
|
setSearchParams(searchParams)
|
||||||
|
}, [searchParams, setSearchParams])
|
||||||
|
const {
|
||||||
|
settings: { context: settings, send: settingsSend },
|
||||||
|
} = useSettingsAuthContext()
|
||||||
|
|
||||||
|
const [state, send, actor] = useMachine(
|
||||||
|
projectsMachine.provide({
|
||||||
|
actions: {
|
||||||
|
navigateToProject: () => {},
|
||||||
|
navigateToProjectIfNeeded: () => {},
|
||||||
|
navigateToFile: () => {},
|
||||||
|
toastSuccess: ({ event }) =>
|
||||||
|
toast.success(
|
||||||
|
('data' in event && typeof event.data === 'string' && event.data) ||
|
||||||
|
('output' in event &&
|
||||||
|
'message' in event.output &&
|
||||||
|
typeof event.output.message === 'string' &&
|
||||||
|
event.output.message) ||
|
||||||
|
''
|
||||||
|
),
|
||||||
|
toastError: ({ event }) =>
|
||||||
|
toast.error(
|
||||||
|
('data' in event && typeof event.data === 'string' && event.data) ||
|
||||||
|
('output' in event &&
|
||||||
|
typeof event.output === 'string' &&
|
||||||
|
event.output) ||
|
||||||
|
''
|
||||||
|
),
|
||||||
|
},
|
||||||
|
actors: {
|
||||||
|
readProjects: fromPromise(async () => [] as Project[]),
|
||||||
|
createProject: fromPromise(async () => ({
|
||||||
|
message: 'not implemented on web',
|
||||||
|
})),
|
||||||
|
renameProject: fromPromise(async () => ({
|
||||||
|
message: 'not implemented on web',
|
||||||
|
oldName: '',
|
||||||
|
newName: '',
|
||||||
|
})),
|
||||||
|
deleteProject: fromPromise(async () => ({
|
||||||
|
message: 'not implemented on web',
|
||||||
|
name: '',
|
||||||
|
})),
|
||||||
|
createFile: fromPromise(async ({ input }) => {
|
||||||
|
// Browser version doesn't navigate, just overwrites the current file
|
||||||
|
clearImportSearchParams()
|
||||||
|
codeManager.updateCodeStateEditor(input.code || '')
|
||||||
|
await codeManager.writeToFile()
|
||||||
|
|
||||||
|
settingsSend({
|
||||||
|
type: 'set.modeling.defaultUnit',
|
||||||
|
data: {
|
||||||
|
level: 'project',
|
||||||
|
value: input.units,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: 'File and units overwritten successfully',
|
||||||
|
fileName: input.name,
|
||||||
|
projectName: '',
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
input: {
|
||||||
|
projects: [],
|
||||||
|
defaultProjectName: settings.projects.defaultProjectName.current,
|
||||||
|
defaultDirectory: settings.app.projectDirectory.current,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// register all project-related command palette commands
|
||||||
|
useStateMachineCommands({
|
||||||
|
machineId: 'projects',
|
||||||
|
send,
|
||||||
|
state,
|
||||||
|
commandBarConfig: projectsCommandBarConfig,
|
||||||
|
actor,
|
||||||
|
onCancel: clearImportSearchParams,
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProjectsMachineContext.Provider
|
<ProjectsMachineContext.Provider
|
||||||
value={{
|
value={{
|
||||||
state: undefined,
|
state,
|
||||||
send: () => {},
|
send,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@ -73,19 +187,21 @@ const ProjectsContextDesktop = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
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 { onProjectOpen } = useLspContext()
|
||||||
const {
|
const {
|
||||||
settings: { context: settings },
|
settings: { context: settings },
|
||||||
} = useSettingsAuthContext()
|
} = useSettingsAuthContext()
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log(
|
|
||||||
'project directory changed',
|
|
||||||
settings.app.projectDirectory.current
|
|
||||||
)
|
|
||||||
}, [settings.app.projectDirectory.current])
|
|
||||||
|
|
||||||
const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0)
|
const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0)
|
||||||
const { projectPaths, projectsDir } = useProjectsLoader([
|
const { projectPaths, projectsDir } = useProjectsLoader([
|
||||||
projectsLoaderTrigger,
|
projectsLoaderTrigger,
|
||||||
@ -126,7 +242,7 @@ const ProjectsContextDesktop = ({
|
|||||||
},
|
},
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
commandBarSend({ type: 'Close' })
|
commandBarActor.send({ type: 'Close' })
|
||||||
const newPathName = `${PATHS.FILE}/${encodeURIComponent(
|
const newPathName = `${PATHS.FILE}/${encodeURIComponent(
|
||||||
projectPath
|
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 }) =>
|
toastSuccess: ({ event }) =>
|
||||||
toast.success(
|
toast.success(
|
||||||
('data' in event && typeof event.data === 'string' && event.data) ||
|
('data' in event && typeof event.data === 'string' && event.data) ||
|
||||||
@ -201,7 +342,7 @@ const ProjectsContextDesktop = ({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
message: `Successfully created "${uniqueName}"`,
|
message: `Successfully created "${uniqueName}"`,
|
||||||
name,
|
name: uniqueName,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
renameProject: fromPromise(async ({ input }) => {
|
renameProject: fromPromise(async ({ input }) => {
|
||||||
@ -218,8 +359,6 @@ const ProjectsContextDesktop = ({
|
|||||||
name = interpolateProjectNameWithIndex(name, nextIndex)
|
name = interpolateProjectNameWithIndex(name, nextIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('from Project')
|
|
||||||
|
|
||||||
await renameProjectDirectory(
|
await renameProjectDirectory(
|
||||||
window.electron.path.join(defaultDirectory, oldName),
|
window.electron.path.join(defaultDirectory, oldName),
|
||||||
name
|
name
|
||||||
@ -242,13 +381,82 @@ const ProjectsContextDesktop = ({
|
|||||||
name: input.name,
|
name: input.name,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
},
|
createFile: fromPromise(async ({ input }) => {
|
||||||
guards: {
|
let projectName =
|
||||||
'Has at least 1 project': ({ event }) => {
|
(input.method === 'newProject' ? input.name : input.projectName) ||
|
||||||
if (event.type !== 'xstate.done.actor.read-projects') return false
|
settings.projects.defaultProjectName.current
|
||||||
console.log(`from has at least 1 project: ${event.output.length}`)
|
let fileName =
|
||||||
return event.output.length ? event.output.length >= 1 : false
|
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,
|
state,
|
||||||
commandBarConfig: projectsCommandBarConfig,
|
commandBarConfig: projectsCommandBarConfig,
|
||||||
actor,
|
actor,
|
||||||
|
onCancel: clearImportSearchParams,
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -29,7 +29,6 @@ import {
|
|||||||
createSettingsCommand,
|
createSettingsCommand,
|
||||||
settingsWithCommandConfigs,
|
settingsWithCommandConfigs,
|
||||||
} from 'lib/commandBarConfigs/settingsCommandConfig'
|
} from 'lib/commandBarConfigs/settingsCommandConfig'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import { Command } from 'lib/commandTypes'
|
import { Command } from 'lib/commandTypes'
|
||||||
import { BaseUnit } from 'lib/settings/settingsTypes'
|
import { BaseUnit } from 'lib/settings/settingsTypes'
|
||||||
import {
|
import {
|
||||||
@ -42,6 +41,7 @@ import { isDesktop } from 'lib/isDesktop'
|
|||||||
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
||||||
import { codeManager } from 'lib/singletons'
|
import { codeManager } from 'lib/singletons'
|
||||||
import { createRouteCommands } from 'lib/commandBarConfigs/routeCommandConfig'
|
import { createRouteCommands } from 'lib/commandBarConfigs/routeCommandConfig'
|
||||||
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
@ -109,7 +109,6 @@ export const SettingsAuthProviderBase = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { commandBarSend } = useCommandsContext()
|
|
||||||
const [settingsPath, setSettingsPath] = useState<string | undefined>(
|
const [settingsPath, setSettingsPath] = useState<string | undefined>(
|
||||||
undefined
|
undefined
|
||||||
)
|
)
|
||||||
@ -278,10 +277,10 @@ export const SettingsAuthProviderBase = ({
|
|||||||
)
|
)
|
||||||
.filter((c) => c !== null) as Command[]
|
.filter((c) => c !== null) as Command[]
|
||||||
|
|
||||||
commandBarSend({ type: 'Add commands', data: { commands: commands } })
|
commandBarActor.send({ type: 'Add commands', data: { commands: commands } })
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Remove commands',
|
type: 'Remove commands',
|
||||||
data: { commands },
|
data: { commands },
|
||||||
})
|
})
|
||||||
@ -290,7 +289,7 @@ export const SettingsAuthProviderBase = ({
|
|||||||
settingsState,
|
settingsState,
|
||||||
settingsSend,
|
settingsSend,
|
||||||
settingsActor,
|
settingsActor,
|
||||||
commandBarSend,
|
commandBarActor.send,
|
||||||
settingsWithCommandConfigs,
|
settingsWithCommandConfigs,
|
||||||
])
|
])
|
||||||
|
|
||||||
@ -303,7 +302,7 @@ export const SettingsAuthProviderBase = ({
|
|||||||
encodeURIComponent(loadedProject?.file?.path || BROWSER_PATH)
|
encodeURIComponent(loadedProject?.file?.path || BROWSER_PATH)
|
||||||
const { RouteTelemetryCommand, RouteHomeCommand, RouteSettingsCommand } =
|
const { RouteTelemetryCommand, RouteHomeCommand, RouteSettingsCommand } =
|
||||||
createRouteCommands(navigate, location, filePath)
|
createRouteCommands(navigate, location, filePath)
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Remove commands',
|
type: 'Remove commands',
|
||||||
data: {
|
data: {
|
||||||
commands: [
|
commands: [
|
||||||
@ -314,12 +313,12 @@ export const SettingsAuthProviderBase = ({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
if (location.pathname === PATHS.HOME) {
|
if (location.pathname === PATHS.HOME) {
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Add commands',
|
type: 'Add commands',
|
||||||
data: { commands: [RouteTelemetryCommand, RouteSettingsCommand] },
|
data: { commands: [RouteTelemetryCommand, RouteSettingsCommand] },
|
||||||
})
|
})
|
||||||
} else if (location.pathname.includes(PATHS.FILE)) {
|
} else if (location.pathname.includes(PATHS.FILE)) {
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Add commands',
|
type: 'Add commands',
|
||||||
data: {
|
data: {
|
||||||
commands: [
|
commands: [
|
||||||
|
@ -17,10 +17,11 @@ import {
|
|||||||
import { useRouteLoaderData } from 'react-router-dom'
|
import { useRouteLoaderData } from 'react-router-dom'
|
||||||
import { PATHS } from 'lib/paths'
|
import { PATHS } from 'lib/paths'
|
||||||
import { IndexLoaderData } from 'lib/types'
|
import { IndexLoaderData } from 'lib/types'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import { err, reportRejection } from 'lib/trap'
|
import { err, reportRejection } from 'lib/trap'
|
||||||
import { getArtifactOfTypes } from 'lang/std/artifactGraph'
|
import { getArtifactOfTypes } from 'lang/std/artifactGraph'
|
||||||
import { ViewControlContextMenu } from './ViewControlMenu'
|
import { ViewControlContextMenu } from './ViewControlMenu'
|
||||||
|
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||||
|
import { useSelector } from '@xstate/react'
|
||||||
|
|
||||||
enum StreamState {
|
enum StreamState {
|
||||||
Playing = 'playing',
|
Playing = 'playing',
|
||||||
@ -35,7 +36,7 @@ export const Stream = () => {
|
|||||||
const videoRef = useRef<HTMLVideoElement>(null)
|
const videoRef = useRef<HTMLVideoElement>(null)
|
||||||
const { settings } = useSettingsAuthContext()
|
const { settings } = useSettingsAuthContext()
|
||||||
const { state, send } = useModelingContext()
|
const { state, send } = useModelingContext()
|
||||||
const { commandBarState } = useCommandsContext()
|
const commandBarState = useCommandBarState()
|
||||||
const { mediaStream } = useAppStream()
|
const { mediaStream } = useAppStream()
|
||||||
const { overallState, immediateState } = useNetworkContext()
|
const { overallState, immediateState } = useNetworkContext()
|
||||||
const [streamState, setStreamState] = useState(StreamState.Unset)
|
const [streamState, setStreamState] = useState(StreamState.Unset)
|
||||||
|
@ -28,7 +28,7 @@ import { base64Decode } from 'lang/wasm'
|
|||||||
import { sendTelemetry } from 'lib/textToCad'
|
import { sendTelemetry } from 'lib/textToCad'
|
||||||
import { Themes } from 'lib/theme'
|
import { Themes } from 'lib/theme'
|
||||||
import { ActionButton } from './ActionButton'
|
import { ActionButton } from './ActionButton'
|
||||||
import { commandBarMachine } from 'machines/commandBarMachine'
|
import { commandBarActor, commandBarMachine } from 'machines/commandBarMachine'
|
||||||
import { EventFrom } from 'xstate'
|
import { EventFrom } from 'xstate'
|
||||||
import { fileMachine } from 'machines/fileMachine'
|
import { fileMachine } from 'machines/fileMachine'
|
||||||
import { reportRejection } from 'lib/trap'
|
import { reportRejection } from 'lib/trap'
|
||||||
@ -43,15 +43,10 @@ export function ToastTextToCadError({
|
|||||||
toastId,
|
toastId,
|
||||||
message,
|
message,
|
||||||
prompt,
|
prompt,
|
||||||
commandBarSend,
|
|
||||||
}: {
|
}: {
|
||||||
toastId: string
|
toastId: string
|
||||||
message: string
|
message: string
|
||||||
prompt: string
|
prompt: string
|
||||||
commandBarSend: (
|
|
||||||
event: EventFrom<typeof commandBarMachine>,
|
|
||||||
data?: unknown
|
|
||||||
) => void
|
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col justify-between gap-6">
|
<div className="flex flex-col justify-between gap-6">
|
||||||
@ -81,7 +76,7 @@ export function ToastTextToCadError({
|
|||||||
}}
|
}}
|
||||||
name="Edit prompt"
|
name="Edit prompt"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: {
|
data: {
|
||||||
groupId: 'modeling',
|
groupId: 'modeling',
|
||||||
|
@ -8,7 +8,6 @@ import {
|
|||||||
} from 'react-router-dom'
|
} from 'react-router-dom'
|
||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
|
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
|
||||||
import { CommandBarProvider } from './CommandBar/CommandBarProvider'
|
|
||||||
|
|
||||||
type User = Models['User_type']
|
type User = Models['User_type']
|
||||||
|
|
||||||
@ -124,9 +123,7 @@ function TestWrap({ children }: { children: React.ReactNode }) {
|
|||||||
<Route
|
<Route
|
||||||
path="/file/:id"
|
path="/file/:id"
|
||||||
element={
|
element={
|
||||||
<CommandBarProvider>
|
<SettingsAuthProviderJest>{children}</SettingsAuthProviderJest>
|
||||||
<SettingsAuthProviderJest>{children}</SettingsAuthProviderJest>
|
|
||||||
</CommandBarProvider>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
@ -5,7 +5,6 @@ import { engineCommandManager, kclManager } from 'lib/singletons'
|
|||||||
import { modelingMachine, ModelingMachineEvent } from 'machines/modelingMachine'
|
import { modelingMachine, ModelingMachineEvent } from 'machines/modelingMachine'
|
||||||
import { Selections, Selection, processCodeMirrorRanges } from 'lib/selections'
|
import { Selections, Selection, processCodeMirrorRanges } from 'lib/selections'
|
||||||
import { undo, redo } from '@codemirror/commands'
|
import { undo, redo } from '@codemirror/commands'
|
||||||
import { CommandBarMachineEvent } from 'machines/commandBarMachine'
|
|
||||||
import { addLineHighlight, addLineHighlightEvent } from './highlightextension'
|
import { addLineHighlight, addLineHighlightEvent } from './highlightextension'
|
||||||
import {
|
import {
|
||||||
Diagnostic,
|
Diagnostic,
|
||||||
@ -52,9 +51,6 @@ export default class EditorManager {
|
|||||||
private _modelingSend: (eventInfo: ModelingMachineEvent) => void = () => {}
|
private _modelingSend: (eventInfo: ModelingMachineEvent) => void = () => {}
|
||||||
private _modelingState: StateFrom<typeof modelingMachine> | null = null
|
private _modelingState: StateFrom<typeof modelingMachine> | null = null
|
||||||
|
|
||||||
private _commandBarSend: (eventInfo: CommandBarMachineEvent) => void =
|
|
||||||
() => {}
|
|
||||||
|
|
||||||
private _convertToVariableEnabled: boolean = false
|
private _convertToVariableEnabled: boolean = false
|
||||||
private _convertToVariableCallback: () => void = () => {}
|
private _convertToVariableCallback: () => void = () => {}
|
||||||
|
|
||||||
@ -161,14 +157,6 @@ export default class EditorManager {
|
|||||||
this._modelingState = state
|
this._modelingState = state
|
||||||
}
|
}
|
||||||
|
|
||||||
setCommandBarSend(send: (eventInfo: CommandBarMachineEvent) => void) {
|
|
||||||
this._commandBarSend = send
|
|
||||||
}
|
|
||||||
|
|
||||||
commandBarSend(eventInfo: CommandBarMachineEvent): void {
|
|
||||||
return this._commandBarSend(eventInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
get highlightRange(): Array<[number, number]> {
|
get highlightRange(): Array<[number, number]> {
|
||||||
return this._highlightRange
|
return this._highlightRange
|
||||||
}
|
}
|
||||||
@ -315,6 +303,21 @@ export default class EditorManager {
|
|||||||
if (selections?.graphSelections?.length === 0) {
|
if (selections?.graphSelections?.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this._editorView) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const codeBaseSelections = this.createEditorSelection(selections)
|
||||||
|
this._editorView.dispatch({
|
||||||
|
selection: codeBaseSelections,
|
||||||
|
annotations: [
|
||||||
|
updateOutsideEditorEvent,
|
||||||
|
Transaction.addToHistory.of(false),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
createEditorSelection(selections: Selections) {
|
||||||
let codeBasedSelections = []
|
let codeBasedSelections = []
|
||||||
for (const selection of selections.graphSelections) {
|
for (const selection of selections.graphSelections) {
|
||||||
const safeEnd = Math.min(
|
const safeEnd = Math.min(
|
||||||
@ -331,18 +334,7 @@ export default class EditorManager {
|
|||||||
.range[1]
|
.range[1]
|
||||||
const safeEnd = Math.min(end, this._editorView?.state.doc.length || end)
|
const safeEnd = Math.min(end, this._editorView?.state.doc.length || end)
|
||||||
codeBasedSelections.push(EditorSelection.cursor(safeEnd))
|
codeBasedSelections.push(EditorSelection.cursor(safeEnd))
|
||||||
|
return EditorSelection.create(codeBasedSelections, 1)
|
||||||
if (!this._editorView) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this._editorView.dispatch({
|
|
||||||
selection: EditorSelection.create(codeBasedSelections, 1),
|
|
||||||
annotations: [
|
|
||||||
updateOutsideEditorEvent,
|
|
||||||
Transaction.addToHistory.of(false),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We will ONLY get here if the user called a select event.
|
// We will ONLY get here if the user called a select event.
|
||||||
|
@ -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])
|
||||||
|
}
|
@ -14,7 +14,7 @@ export const useProjectsLoader = (deps?: [number]) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Useless on web, until we get fake filesystems over there.
|
// Useless on web, until we get fake filesystems over there.
|
||||||
if (!isDesktop) return
|
if (!isDesktop()) return
|
||||||
|
|
||||||
if (deps && deps[0] === lastTs) return
|
if (deps && deps[0] === lastTs) return
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { AnyStateMachine, Actor, StateFrom, EventFrom } from 'xstate'
|
import { AnyStateMachine, Actor, StateFrom, EventFrom } from 'xstate'
|
||||||
import { createMachineCommand } from '../lib/createMachineCommand'
|
import { createMachineCommand } from '../lib/createMachineCommand'
|
||||||
import { useCommandsContext } from './useCommandsContext'
|
|
||||||
import { modelingMachine } from 'machines/modelingMachine'
|
import { modelingMachine } from 'machines/modelingMachine'
|
||||||
import { authMachine } from 'machines/authMachine'
|
import { authMachine } from 'machines/authMachine'
|
||||||
import { settingsMachine } from 'machines/settingsMachine'
|
import { settingsMachine } from 'machines/settingsMachine'
|
||||||
@ -15,6 +14,7 @@ import { useKclContext } from 'lang/KclProvider'
|
|||||||
import { useNetworkContext } from 'hooks/useNetworkContext'
|
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||||
import { useAppState } from 'AppState'
|
import { useAppState } from 'AppState'
|
||||||
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
|
|
||||||
// This might not be necessary, AnyStateMachine from xstate is working
|
// This might not be necessary, AnyStateMachine from xstate is working
|
||||||
export type AllMachines =
|
export type AllMachines =
|
||||||
@ -48,7 +48,6 @@ export default function useStateMachineCommands<
|
|||||||
allCommandsRequireNetwork = false,
|
allCommandsRequireNetwork = false,
|
||||||
onCancel,
|
onCancel,
|
||||||
}: UseStateMachineCommandsArgs<T, S>) {
|
}: UseStateMachineCommandsArgs<T, S>) {
|
||||||
const { commandBarSend } = useCommandsContext()
|
|
||||||
const { overallState } = useNetworkContext()
|
const { overallState } = useNetworkContext()
|
||||||
const { isExecuting } = useKclContext()
|
const { isExecuting } = useKclContext()
|
||||||
const { isStreamReady } = useAppState()
|
const { isStreamReady } = useAppState()
|
||||||
@ -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
|
.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 () => {
|
return () => {
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Remove commands',
|
type: 'Remove commands',
|
||||||
data: { commands: newCommands },
|
data: { commands: newCommands },
|
||||||
})
|
})
|
||||||
|
@ -24,7 +24,10 @@ describe('testing AST', () => {
|
|||||||
type: 'Literal',
|
type: 'Literal',
|
||||||
start: 0,
|
start: 0,
|
||||||
end: 1,
|
end: 1,
|
||||||
value: 5,
|
value: {
|
||||||
|
suffix: 'None',
|
||||||
|
value: 5,
|
||||||
|
},
|
||||||
raw: '5',
|
raw: '5',
|
||||||
},
|
},
|
||||||
operator: '+',
|
operator: '+',
|
||||||
@ -32,7 +35,10 @@ describe('testing AST', () => {
|
|||||||
type: 'Literal',
|
type: 'Literal',
|
||||||
start: 3,
|
start: 3,
|
||||||
end: 4,
|
end: 4,
|
||||||
value: 6,
|
value: {
|
||||||
|
suffix: 'None',
|
||||||
|
value: 6,
|
||||||
|
},
|
||||||
raw: '6',
|
raw: '6',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -54,6 +54,9 @@ const mySketch001 = startSketchOn('XY')
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
|
units: {
|
||||||
|
type: 'Mm',
|
||||||
|
},
|
||||||
__meta: [{ sourceRange: [46, 71, 0] }],
|
__meta: [{ sourceRange: [46, 71, 0] }],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -72,56 +75,65 @@ const mySketch001 = startSketchOn('XY')
|
|||||||
const sketch001 = execState.memory.get('mySketch001')
|
const sketch001 = execState.memory.get('mySketch001')
|
||||||
expect(sketch001).toEqual({
|
expect(sketch001).toEqual({
|
||||||
type: 'Solid',
|
type: 'Solid',
|
||||||
id: expect.any(String),
|
value: {
|
||||||
value: [
|
type: 'Solid',
|
||||||
{
|
|
||||||
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: {
|
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
__meta: expect.any(Array),
|
value: [
|
||||||
on: expect.any(Object),
|
|
||||||
start: expect.any(Object),
|
|
||||||
type: 'Sketch',
|
|
||||||
paths: [
|
|
||||||
{
|
{
|
||||||
type: 'ToPoint',
|
type: 'extrudePlane',
|
||||||
from: [0, 0],
|
faceId: expect.any(String),
|
||||||
to: [-1.59, -1.54],
|
|
||||||
tag: null,
|
tag: null,
|
||||||
__geoMeta: {
|
id: expect.any(String),
|
||||||
id: expect.any(String),
|
sourceRange: [77, 102, 0],
|
||||||
sourceRange: [77, 102, 0],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'ToPoint',
|
type: 'extrudePlane',
|
||||||
from: [-1.59, -1.54],
|
faceId: expect.any(String),
|
||||||
to: [0.46, -5.82],
|
|
||||||
tag: null,
|
tag: null,
|
||||||
__geoMeta: {
|
id: expect.any(String),
|
||||||
id: expect.any(String),
|
sourceRange: [108, 132, 0],
|
||||||
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 () => {
|
test('sketch extrude and sketch on one of the faces', async () => {
|
||||||
@ -154,187 +166,205 @@ const sk2 = startSketchOn('XY')
|
|||||||
expect(geos).toEqual([
|
expect(geos).toEqual([
|
||||||
{
|
{
|
||||||
type: 'Solid',
|
type: 'Solid',
|
||||||
id: expect.any(String),
|
value: {
|
||||||
value: [
|
type: 'Solid',
|
||||||
{
|
|
||||||
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: {
|
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
__meta: expect.any(Array),
|
value: [
|
||||||
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: [
|
|
||||||
{
|
{
|
||||||
type: 'ToPoint',
|
type: 'extrudePlane',
|
||||||
from: [0, 0],
|
faceId: expect.any(String),
|
||||||
to: [-2.5, 0],
|
|
||||||
tag: null,
|
tag: null,
|
||||||
__geoMeta: {
|
id: expect.any(String),
|
||||||
id: expect.any(String),
|
sourceRange: [69, 89, 0],
|
||||||
sourceRange: [69, 89, 0],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'ToPoint',
|
type: 'extrudePlane',
|
||||||
from: [-2.5, 0],
|
faceId: expect.any(String),
|
||||||
to: [0, 10],
|
|
||||||
tag: {
|
tag: {
|
||||||
end: 116,
|
end: 116,
|
||||||
start: 114,
|
start: 114,
|
||||||
type: 'TagDeclarator',
|
type: 'TagDeclarator',
|
||||||
value: 'p',
|
value: 'p',
|
||||||
},
|
},
|
||||||
__geoMeta: {
|
id: expect.any(String),
|
||||||
id: expect.any(String),
|
sourceRange: [95, 117, 0],
|
||||||
sourceRange: [95, 117, 0],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'ToPoint',
|
type: 'extrudePlane',
|
||||||
from: [0, 10],
|
faceId: expect.any(String),
|
||||||
to: [2.5, 0],
|
|
||||||
tag: null,
|
tag: null,
|
||||||
__geoMeta: {
|
id: expect.any(String),
|
||||||
id: expect.any(String),
|
sourceRange: [123, 142, 0],
|
||||||
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',
|
type: 'Solid',
|
||||||
id: expect.any(String),
|
value: {
|
||||||
value: [
|
type: 'Solid',
|
||||||
{
|
|
||||||
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: {
|
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
__meta: expect.any(Array),
|
value: [
|
||||||
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',
|
type: 'extrudePlane',
|
||||||
from: [0, 0],
|
faceId: expect.any(String),
|
||||||
to: [-2.5, 0],
|
|
||||||
tag: null,
|
tag: null,
|
||||||
__geoMeta: {
|
id: expect.any(String),
|
||||||
id: expect.any(String),
|
sourceRange: [373, 393, 0],
|
||||||
sourceRange: [373, 393, 0],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'ToPoint',
|
type: 'extrudePlane',
|
||||||
from: [-2.5, 0],
|
faceId: expect.any(String),
|
||||||
to: [0, 3],
|
|
||||||
tag: {
|
tag: {
|
||||||
end: 419,
|
end: 419,
|
||||||
start: 417,
|
start: 417,
|
||||||
type: 'TagDeclarator',
|
type: 'TagDeclarator',
|
||||||
value: 'o',
|
value: 'o',
|
||||||
},
|
},
|
||||||
__geoMeta: {
|
id: expect.any(String),
|
||||||
id: expect.any(String),
|
sourceRange: [399, 420, 0],
|
||||||
sourceRange: [399, 420, 0],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'ToPoint',
|
type: 'extrudePlane',
|
||||||
from: [0, 3],
|
faceId: expect.any(String),
|
||||||
to: [2.5, 0],
|
|
||||||
tag: null,
|
tag: null,
|
||||||
__geoMeta: {
|
id: expect.any(String),
|
||||||
id: expect.any(String),
|
sourceRange: [426, 445, 0],
|
||||||
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] }],
|
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
@ -221,6 +221,9 @@ const newVar = myVar + 1`
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
|
units: {
|
||||||
|
type: 'Mm',
|
||||||
|
},
|
||||||
__meta: [{ sourceRange: [39, 63, 0] }],
|
__meta: [{ sourceRange: [39, 63, 0] }],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -39,7 +39,7 @@ describe('Testing createLiteral', () => {
|
|||||||
it('should create a literal', () => {
|
it('should create a literal', () => {
|
||||||
const result = createLiteral(5)
|
const result = createLiteral(5)
|
||||||
expect(result.type).toBe('Literal')
|
expect(result.type).toBe('Literal')
|
||||||
expect(result.value).toBe(5)
|
expect((result as any).value.value).toBe(5)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
describe('Testing createIdentifier', () => {
|
describe('Testing createIdentifier', () => {
|
||||||
@ -56,7 +56,7 @@ describe('Testing createCallExpression', () => {
|
|||||||
expect(result.callee.type).toBe('Identifier')
|
expect(result.callee.type).toBe('Identifier')
|
||||||
expect(result.callee.name).toBe('myFunc')
|
expect(result.callee.name).toBe('myFunc')
|
||||||
expect(result.arguments[0].type).toBe('Literal')
|
expect(result.arguments[0].type).toBe('Literal')
|
||||||
expect((result.arguments[0] as any).value).toBe(5)
|
expect((result.arguments[0] as any).value.value).toBe(5)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
describe('Testing createObjectExpression', () => {
|
describe('Testing createObjectExpression', () => {
|
||||||
@ -68,7 +68,7 @@ describe('Testing createObjectExpression', () => {
|
|||||||
expect(result.properties[0].type).toBe('ObjectProperty')
|
expect(result.properties[0].type).toBe('ObjectProperty')
|
||||||
expect(result.properties[0].key.name).toBe('myProp')
|
expect(result.properties[0].key.name).toBe('myProp')
|
||||||
expect(result.properties[0].value.type).toBe('Literal')
|
expect(result.properties[0].value.type).toBe('Literal')
|
||||||
expect((result.properties[0].value as any).value).toBe(5)
|
expect((result.properties[0].value as any).value.value).toBe(5)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
describe('Testing createArrayExpression', () => {
|
describe('Testing createArrayExpression', () => {
|
||||||
@ -76,7 +76,7 @@ describe('Testing createArrayExpression', () => {
|
|||||||
const result = createArrayExpression([createLiteral(5)])
|
const result = createArrayExpression([createLiteral(5)])
|
||||||
expect(result.type).toBe('ArrayExpression')
|
expect(result.type).toBe('ArrayExpression')
|
||||||
expect(result.elements[0].type).toBe('Literal')
|
expect(result.elements[0].type).toBe('Literal')
|
||||||
expect((result.elements[0] as any).value).toBe(5)
|
expect((result.elements[0] as any).value.value).toBe(5)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
describe('Testing createPipeSubstitution', () => {
|
describe('Testing createPipeSubstitution', () => {
|
||||||
@ -93,7 +93,7 @@ describe('Testing createVariableDeclaration', () => {
|
|||||||
expect(result.declaration.id.type).toBe('Identifier')
|
expect(result.declaration.id.type).toBe('Identifier')
|
||||||
expect(result.declaration.id.name).toBe('myVar')
|
expect(result.declaration.id.name).toBe('myVar')
|
||||||
expect(result.declaration.init.type).toBe('Literal')
|
expect(result.declaration.init.type).toBe('Literal')
|
||||||
expect((result.declaration.init as any).value).toBe(5)
|
expect((result.declaration.init as any).value.value).toBe(5)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
describe('Testing createPipeExpression', () => {
|
describe('Testing createPipeExpression', () => {
|
||||||
@ -101,7 +101,7 @@ describe('Testing createPipeExpression', () => {
|
|||||||
const result = createPipeExpression([createLiteral(5)])
|
const result = createPipeExpression([createLiteral(5)])
|
||||||
expect(result.type).toBe('PipeExpression')
|
expect(result.type).toBe('PipeExpression')
|
||||||
expect(result.body[0].type).toBe('Literal')
|
expect(result.body[0].type).toBe('Literal')
|
||||||
expect((result.body[0] as any).value).toBe(5)
|
expect((result.body[0] as any).value.value).toBe(5)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -743,14 +743,18 @@ export function splitPathAtPipeExpression(pathToNode: PathToNode): {
|
|||||||
return splitPathAtPipeExpression(pathToNode.slice(0, -1))
|
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 {
|
return {
|
||||||
type: 'Literal',
|
type: 'Literal',
|
||||||
start: 0,
|
start: 0,
|
||||||
end: 0,
|
end: 0,
|
||||||
moduleId: 0,
|
moduleId: 0,
|
||||||
value,
|
value,
|
||||||
raw: `${value}`,
|
raw,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -660,7 +660,7 @@ myNestedVar = [
|
|||||||
enter: (node, path) => {
|
enter: (node, path) => {
|
||||||
if (
|
if (
|
||||||
node.type === 'Literal' &&
|
node.type === 'Literal' &&
|
||||||
String(node.value) === literalOfInterest
|
String((node as any).value.value) === literalOfInterest
|
||||||
) {
|
) {
|
||||||
pathToNode = path
|
pathToNode = path
|
||||||
} else if (
|
} else if (
|
||||||
|
@ -717,16 +717,6 @@ function isTypeInArrayExp(
|
|||||||
return node.elements.some((el) => isTypeInValue(el, syntaxType))
|
return node.elements.some((el) => isTypeInValue(el, syntaxType))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isValueZero(val?: Expr): boolean {
|
|
||||||
return (
|
|
||||||
(val?.type === 'Literal' && Number(val.value) === 0) ||
|
|
||||||
(val?.type === 'UnaryExpression' &&
|
|
||||||
val.operator === '-' &&
|
|
||||||
val.argument.type === 'Literal' &&
|
|
||||||
Number(val.argument.value) === 0)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isLinesParallelAndConstrained(
|
export function isLinesParallelAndConstrained(
|
||||||
ast: Program,
|
ast: Program,
|
||||||
artifactGraph: ArtifactGraph,
|
artifactGraph: ArtifactGraph,
|
||||||
|
@ -1014,6 +1014,11 @@ class EngineConnection extends EventTarget {
|
|||||||
this.pingPongSpan.pong = new Date()
|
this.pingPongSpan.pong = new Date()
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case 'modeling_session_data':
|
||||||
|
let api_call_id = resp.data?.session?.api_call_id
|
||||||
|
console.log(`API Call ID: ${api_call_id}`)
|
||||||
|
break
|
||||||
|
|
||||||
// Only fires on successful authentication.
|
// Only fires on successful authentication.
|
||||||
case 'ice_server_info':
|
case 'ice_server_info':
|
||||||
let ice_servers = resp.data?.ice_servers
|
let ice_servers = resp.data?.ice_servers
|
||||||
|
@ -20,12 +20,12 @@ import {
|
|||||||
sketchFromKclValue,
|
sketchFromKclValue,
|
||||||
Literal,
|
Literal,
|
||||||
SourceRange,
|
SourceRange,
|
||||||
|
LiteralValue,
|
||||||
} from '../wasm'
|
} from '../wasm'
|
||||||
import {
|
import {
|
||||||
getNodeFromPath,
|
getNodeFromPath,
|
||||||
getNodeFromPathCurry,
|
getNodeFromPathCurry,
|
||||||
getNodePathFromSourceRange,
|
getNodePathFromSourceRange,
|
||||||
isValueZero,
|
|
||||||
} from '../queryAst'
|
} from '../queryAst'
|
||||||
import {
|
import {
|
||||||
createArrayExpression,
|
createArrayExpression,
|
||||||
@ -79,11 +79,32 @@ export type ConstraintType =
|
|||||||
| 'setAngleBetween'
|
| 'setAngleBetween'
|
||||||
|
|
||||||
const REF_NUM_ERR = new Error('Referenced segment does not have a to value')
|
const REF_NUM_ERR = new Error('Referenced segment does not have a to value')
|
||||||
|
|
||||||
|
function asNum(val: LiteralValue): number | Error {
|
||||||
|
if (typeof val === 'object') return val.value
|
||||||
|
return REF_NUM_ERR
|
||||||
|
}
|
||||||
|
|
||||||
|
function forceNum(arg: Literal): number {
|
||||||
|
if (typeof arg.value === 'boolean' || typeof arg.value === 'string') {
|
||||||
|
return Number(arg.value)
|
||||||
|
} else {
|
||||||
|
return arg.value.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function isUndef(val: any): val is undefined {
|
function isUndef(val: any): val is undefined {
|
||||||
return typeof val === 'undefined'
|
return typeof val === 'undefined'
|
||||||
}
|
}
|
||||||
function isNum(val: any): val is number {
|
|
||||||
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(
|
function createCallWrapper(
|
||||||
@ -190,7 +211,7 @@ const xyLineSetLength =
|
|||||||
: referenceSeg
|
: referenceSeg
|
||||||
? segRef
|
? segRef
|
||||||
: args[0].expr
|
: args[0].expr
|
||||||
const literalARg = getArgLiteralVal(args[0].expr)
|
const literalARg = asNum(args[0].expr.value)
|
||||||
if (err(literalARg)) return literalARg
|
if (err(literalARg)) return literalARg
|
||||||
return createCallWrapper(xOrY, lineVal, tag, literalARg)
|
return createCallWrapper(xOrY, lineVal, tag, literalARg)
|
||||||
}
|
}
|
||||||
@ -211,13 +232,14 @@ const basicAngledLineCreateNode =
|
|||||||
referencedSegment: path,
|
referencedSegment: path,
|
||||||
}) => {
|
}) => {
|
||||||
const refAng = path ? getAngle(path?.from, path?.to) : 0
|
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 =
|
const nonForcedAng =
|
||||||
varValToUse === 'ang'
|
varValToUse === 'ang'
|
||||||
? inputs[0].expr
|
? inputs[0].expr
|
||||||
: referenceSeg === 'ang'
|
: referenceSeg === 'ang'
|
||||||
? getClosesAngleDirection(
|
? getClosesAngleDirection(
|
||||||
args[0].expr.value,
|
argValue,
|
||||||
refAng,
|
refAng,
|
||||||
createSegAngle(referenceSegName)
|
createSegAngle(referenceSegName)
|
||||||
)
|
)
|
||||||
@ -230,8 +252,8 @@ const basicAngledLineCreateNode =
|
|||||||
: args[1].expr
|
: args[1].expr
|
||||||
const shouldForceAng = valToForce === 'ang' && forceValueUsedInTransform
|
const shouldForceAng = valToForce === 'ang' && forceValueUsedInTransform
|
||||||
const shouldForceLen = valToForce === 'len' && forceValueUsedInTransform
|
const shouldForceLen = valToForce === 'len' && forceValueUsedInTransform
|
||||||
const literalArg = getArgLiteralVal(
|
const literalArg = asNum(
|
||||||
valToForce === 'ang' ? args[0].expr : args[1].expr
|
valToForce === 'ang' ? args[0].expr.value : args[1].expr.value
|
||||||
)
|
)
|
||||||
if (err(literalArg)) return literalArg
|
if (err(literalArg)) return literalArg
|
||||||
return createCallWrapper(
|
return createCallWrapper(
|
||||||
@ -283,7 +305,7 @@ const getMinAndSegAngVals = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getSignedLeg = (arg: Literal, legLenVal: BinaryPart) =>
|
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 getLegAng = (ang: number, legAngleVal: BinaryPart) => {
|
||||||
const normalisedAngle = ((ang % 360) + 360) % 360 // between 0 and 360
|
const normalisedAngle = ((ang % 360) + 360) % 360 // between 0 and 360
|
||||||
@ -322,8 +344,7 @@ const setHorzVertDistanceCreateNode =
|
|||||||
referencedSegment,
|
referencedSegment,
|
||||||
}) => {
|
}) => {
|
||||||
const refNum = referencedSegment?.to?.[index]
|
const refNum = referencedSegment?.to?.[index]
|
||||||
const literalArg = getArgLiteralVal(args?.[index].expr)
|
const literalArg = asNum(args?.[index].expr.value)
|
||||||
if (err(literalArg)) return literalArg
|
|
||||||
if (isUndef(refNum) || err(literalArg)) return REF_NUM_ERR
|
if (isUndef(refNum) || err(literalArg)) return REF_NUM_ERR
|
||||||
|
|
||||||
const valueUsedInTransform = roundOff(literalArg - refNum, 2)
|
const valueUsedInTransform = roundOff(literalArg - refNum, 2)
|
||||||
@ -352,7 +373,7 @@ const setHorzVertDistanceForAngleLineCreateNode =
|
|||||||
referencedSegment,
|
referencedSegment,
|
||||||
}) => {
|
}) => {
|
||||||
const refNum = referencedSegment?.to?.[index]
|
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
|
if (isUndef(refNum) || err(literalArg)) return REF_NUM_ERR
|
||||||
const valueUsedInTransform = roundOff(literalArg - refNum, 2)
|
const valueUsedInTransform = roundOff(literalArg - refNum, 2)
|
||||||
const binExp = createBinaryExpressionWithUnary([
|
const binExp = createBinaryExpressionWithUnary([
|
||||||
@ -374,8 +395,8 @@ const setAbsDistanceCreateNode =
|
|||||||
index = xOrY === 'x' ? 0 : 1
|
index = xOrY === 'x' ? 0 : 1
|
||||||
): CreateStdLibSketchCallExpr =>
|
): CreateStdLibSketchCallExpr =>
|
||||||
({ tag, forceValueUsedInTransform, rawArgs: args }) => {
|
({ tag, forceValueUsedInTransform, rawArgs: args }) => {
|
||||||
const literalArg = getArgLiteralVal(args?.[index].expr)
|
const literalArg = asNum(args?.[index].expr.value)
|
||||||
if (err(literalArg)) return REF_NUM_ERR
|
if (err(literalArg)) return literalArg
|
||||||
const valueUsedInTransform = roundOff(literalArg, 2)
|
const valueUsedInTransform = roundOff(literalArg, 2)
|
||||||
const val = forceValueUsedInTransform || createLiteral(valueUsedInTransform)
|
const val = forceValueUsedInTransform || createLiteral(valueUsedInTransform)
|
||||||
if (isXOrYLine) {
|
if (isXOrYLine) {
|
||||||
@ -396,8 +417,8 @@ const setAbsDistanceCreateNode =
|
|||||||
const setAbsDistanceForAngleLineCreateNode =
|
const setAbsDistanceForAngleLineCreateNode =
|
||||||
(xOrY: 'x' | 'y'): CreateStdLibSketchCallExpr =>
|
(xOrY: 'x' | 'y'): CreateStdLibSketchCallExpr =>
|
||||||
({ tag, forceValueUsedInTransform, inputs, rawArgs: args }) => {
|
({ tag, forceValueUsedInTransform, inputs, rawArgs: args }) => {
|
||||||
const literalArg = getArgLiteralVal(args?.[1].expr)
|
const literalArg = asNum(args?.[1].expr.value)
|
||||||
if (err(literalArg)) return REF_NUM_ERR
|
if (err(literalArg)) return literalArg
|
||||||
const valueUsedInTransform = roundOff(literalArg, 2)
|
const valueUsedInTransform = roundOff(literalArg, 2)
|
||||||
const val = forceValueUsedInTransform || createLiteral(valueUsedInTransform)
|
const val = forceValueUsedInTransform || createLiteral(valueUsedInTransform)
|
||||||
return createCallWrapper(
|
return createCallWrapper(
|
||||||
@ -419,7 +440,7 @@ const setHorVertDistanceForXYLines =
|
|||||||
}) => {
|
}) => {
|
||||||
const index = xOrY === 'x' ? 0 : 1
|
const index = xOrY === 'x' ? 0 : 1
|
||||||
const refNum = referencedSegment?.to?.[index]
|
const refNum = referencedSegment?.to?.[index]
|
||||||
const literalArg = getArgLiteralVal(args?.[index].expr)
|
const literalArg = asNum(args?.[index].expr.value)
|
||||||
if (isUndef(refNum) || err(literalArg)) return REF_NUM_ERR
|
if (isUndef(refNum) || err(literalArg)) return REF_NUM_ERR
|
||||||
const valueUsedInTransform = roundOff(literalArg - refNum, 2)
|
const valueUsedInTransform = roundOff(literalArg - refNum, 2)
|
||||||
const makeBinExp = createBinaryExpressionWithUnary([
|
const makeBinExp = createBinaryExpressionWithUnary([
|
||||||
@ -445,9 +466,9 @@ const setHorzVertDistanceConstraintLineCreateNode =
|
|||||||
])
|
])
|
||||||
|
|
||||||
const makeBinExp = (index: 0 | 1) => {
|
const makeBinExp = (index: 0 | 1) => {
|
||||||
const arg = getArgLiteralVal(args?.[index].expr)
|
const arg = asNum(args?.[index].expr.value)
|
||||||
const refNum = referencedSegment?.to?.[index]
|
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([
|
return createBinaryExpressionWithUnary([
|
||||||
createSegEnd(referenceSegName, isX),
|
createSegEnd(referenceSegName, isX),
|
||||||
createLiteral(roundOff(arg - refNum, 2)),
|
createLiteral(roundOff(arg - refNum, 2)),
|
||||||
@ -468,9 +489,9 @@ const setAngledIntersectLineForLines: CreateStdLibSketchCallExpr = ({
|
|||||||
forceValueUsedInTransform,
|
forceValueUsedInTransform,
|
||||||
rawArgs: args,
|
rawArgs: args,
|
||||||
}) => {
|
}) => {
|
||||||
const val = args[1].expr.value,
|
const val = asNum(args[1].expr.value),
|
||||||
angle = args[0].expr.value
|
angle = asNum(args[0].expr.value)
|
||||||
if (!isNum(val) || !isNum(angle)) return REF_NUM_ERR
|
if (err(val) || err(angle)) return REF_NUM_ERR
|
||||||
const valueUsedInTransform = roundOff(val, 2)
|
const valueUsedInTransform = roundOff(val, 2)
|
||||||
const varNamMap: { [key: number]: string } = {
|
const varNamMap: { [key: number]: string } = {
|
||||||
0: 'ZERO',
|
0: 'ZERO',
|
||||||
@ -498,8 +519,8 @@ const setAngledIntersectForAngledLines: CreateStdLibSketchCallExpr = ({
|
|||||||
inputs,
|
inputs,
|
||||||
rawArgs: args,
|
rawArgs: args,
|
||||||
}) => {
|
}) => {
|
||||||
const val = args[1].expr.value
|
const val = asNum(args[1].expr.value)
|
||||||
if (!isNum(val)) return REF_NUM_ERR
|
if (err(val)) return val
|
||||||
const valueUsedInTransform = roundOff(val, 2)
|
const valueUsedInTransform = roundOff(val, 2)
|
||||||
return intersectCallWrapper({
|
return intersectCallWrapper({
|
||||||
fnName: 'angledLineThatIntersects',
|
fnName: 'angledLineThatIntersects',
|
||||||
@ -524,8 +545,8 @@ const setAngleBetweenCreateNode =
|
|||||||
const refAngle = referencedSegment
|
const refAngle = referencedSegment
|
||||||
? getAngle(referencedSegment?.from, referencedSegment?.to)
|
? getAngle(referencedSegment?.from, referencedSegment?.to)
|
||||||
: 0
|
: 0
|
||||||
const val = args[0].expr.value
|
const val = asNum(args[0].expr.value)
|
||||||
if (!isNum(val)) return REF_NUM_ERR
|
if (err(val)) return val
|
||||||
let valueUsedInTransform = roundOff(normaliseAngle(val - refAngle))
|
let valueUsedInTransform = roundOff(normaliseAngle(val - refAngle))
|
||||||
let firstHalfValue = createSegAngle(referenceSegName)
|
let firstHalfValue = createSegAngle(referenceSegName)
|
||||||
if (Math.abs(valueUsedInTransform) > 90) {
|
if (Math.abs(valueUsedInTransform) > 90) {
|
||||||
@ -706,13 +727,11 @@ const transformMap: TransformMap = {
|
|||||||
createPipeSubstitution(),
|
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(
|
return createCallWrapper(
|
||||||
'angledLineToX',
|
'angledLineToX',
|
||||||
[
|
[getAngleLengthSign(val, angleToMatchLengthXCall), inputs[0].expr],
|
||||||
getAngleLengthSign(args[0].expr.value, angleToMatchLengthXCall),
|
|
||||||
inputs[0].expr,
|
|
||||||
],
|
|
||||||
tag
|
tag
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@ -739,13 +758,11 @@ const transformMap: TransformMap = {
|
|||||||
createPipeSubstitution(),
|
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(
|
return createCallWrapper(
|
||||||
'angledLineToY',
|
'angledLineToY',
|
||||||
[
|
[getAngleLengthSign(val, angleToMatchLengthYCall), inputs[1].expr],
|
||||||
getAngleLengthSign(args[0].expr.value, angleToMatchLengthYCall),
|
|
||||||
inputs[1].expr,
|
|
||||||
],
|
|
||||||
tag
|
tag
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@ -763,7 +780,7 @@ const transformMap: TransformMap = {
|
|||||||
forceValueUsedInTransform,
|
forceValueUsedInTransform,
|
||||||
rawArgs: args,
|
rawArgs: args,
|
||||||
}) => {
|
}) => {
|
||||||
const val = getArgLiteralVal(args[0].expr)
|
const val = asNum(args[0].expr.value)
|
||||||
if (err(val)) return val
|
if (err(val)) return val
|
||||||
return createCallWrapper(
|
return createCallWrapper(
|
||||||
'angledLineToY',
|
'angledLineToY',
|
||||||
@ -844,7 +861,7 @@ const transformMap: TransformMap = {
|
|||||||
tooltip: 'yLine',
|
tooltip: 'yLine',
|
||||||
createNode: ({ inputs, tag, rawArgs: args }) => {
|
createNode: ({ inputs, tag, rawArgs: args }) => {
|
||||||
const expr = inputs[1].expr
|
const expr = inputs[1].expr
|
||||||
if (Number(args[0].expr.value) >= 0)
|
if (forceNum(args[0].expr) >= 0)
|
||||||
return createCallWrapper('yLine', expr, tag)
|
return createCallWrapper('yLine', expr, tag)
|
||||||
if (isExprBinaryPart(expr))
|
if (isExprBinaryPart(expr))
|
||||||
return createCallWrapper('yLine', createUnaryExpression(expr), tag)
|
return createCallWrapper('yLine', createUnaryExpression(expr), tag)
|
||||||
@ -856,7 +873,7 @@ const transformMap: TransformMap = {
|
|||||||
tooltip: 'xLine',
|
tooltip: 'xLine',
|
||||||
createNode: ({ inputs, tag, rawArgs: args }) => {
|
createNode: ({ inputs, tag, rawArgs: args }) => {
|
||||||
const expr = inputs[1].expr
|
const expr = inputs[1].expr
|
||||||
if (Number(args[0].expr.value) >= 0)
|
if (forceNum(args[0].expr) >= 0)
|
||||||
return createCallWrapper('xLine', expr, tag)
|
return createCallWrapper('xLine', expr, tag)
|
||||||
if (isExprBinaryPart(expr))
|
if (isExprBinaryPart(expr))
|
||||||
return createCallWrapper('xLine', createUnaryExpression(expr), tag)
|
return createCallWrapper('xLine', createUnaryExpression(expr), tag)
|
||||||
@ -900,10 +917,11 @@ const transformMap: TransformMap = {
|
|||||||
referenceSegName,
|
referenceSegName,
|
||||||
getInputOfType(inputs, 'xRelative').expr
|
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(
|
return createCallWrapper(
|
||||||
'angledLineOfXLength',
|
'angledLineOfXLength',
|
||||||
[getLegAng(args[0].expr.value, legAngle), minVal],
|
[getLegAng(val, legAngle), minVal],
|
||||||
tag
|
tag
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@ -912,7 +930,7 @@ const transformMap: TransformMap = {
|
|||||||
tooltip: 'xLine',
|
tooltip: 'xLine',
|
||||||
createNode: ({ inputs, tag, rawArgs: args }) => {
|
createNode: ({ inputs, tag, rawArgs: args }) => {
|
||||||
const expr = inputs[1].expr
|
const expr = inputs[1].expr
|
||||||
if (Number(args[0].expr.value) >= 0)
|
if (forceNum(args[0].expr) >= 0)
|
||||||
return createCallWrapper('xLine', expr, tag)
|
return createCallWrapper('xLine', expr, tag)
|
||||||
if (isExprBinaryPart(expr))
|
if (isExprBinaryPart(expr))
|
||||||
return createCallWrapper('xLine', createUnaryExpression(expr), tag)
|
return createCallWrapper('xLine', createUnaryExpression(expr), tag)
|
||||||
@ -953,10 +971,11 @@ const transformMap: TransformMap = {
|
|||||||
inputs[1].expr,
|
inputs[1].expr,
|
||||||
'legAngY'
|
'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(
|
return createCallWrapper(
|
||||||
'angledLineOfXLength',
|
'angledLineOfXLength',
|
||||||
[getLegAng(args[0].expr.value, legAngle), minVal],
|
[getLegAng(val, legAngle), minVal],
|
||||||
tag
|
tag
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@ -965,7 +984,7 @@ const transformMap: TransformMap = {
|
|||||||
tooltip: 'yLine',
|
tooltip: 'yLine',
|
||||||
createNode: ({ inputs, tag, rawArgs: args }) => {
|
createNode: ({ inputs, tag, rawArgs: args }) => {
|
||||||
const expr = inputs[1].expr
|
const expr = inputs[1].expr
|
||||||
if (Number(args[0].expr.value) >= 0)
|
if (forceNum(args[0].expr) >= 0)
|
||||||
return createCallWrapper('yLine', expr, tag)
|
return createCallWrapper('yLine', expr, tag)
|
||||||
if (isExprBinaryPart(expr))
|
if (isExprBinaryPart(expr))
|
||||||
return createCallWrapper('yLine', createUnaryExpression(expr), tag)
|
return createCallWrapper('yLine', createUnaryExpression(expr), tag)
|
||||||
@ -1005,13 +1024,11 @@ const transformMap: TransformMap = {
|
|||||||
createPipeSubstitution(),
|
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(
|
return createCallWrapper(
|
||||||
'angledLineToX',
|
'angledLineToX',
|
||||||
[
|
[getAngleLengthSign(val, angleToMatchLengthXCall), inputs[1].expr],
|
||||||
getAngleLengthSign(args[0].expr.value, angleToMatchLengthXCall),
|
|
||||||
inputs[1].expr,
|
|
||||||
],
|
|
||||||
tag
|
tag
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@ -1057,13 +1074,11 @@ const transformMap: TransformMap = {
|
|||||||
createPipeSubstitution(),
|
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(
|
return createCallWrapper(
|
||||||
'angledLineToY',
|
'angledLineToY',
|
||||||
[
|
[getAngleLengthSign(val, angleToMatchLengthXCall), inputs[1].expr],
|
||||||
getAngleLengthSign(args[0].expr.value, angleToMatchLengthXCall),
|
|
||||||
inputs[1].expr,
|
|
||||||
],
|
|
||||||
tag
|
tag
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@ -1080,7 +1095,7 @@ const transformMap: TransformMap = {
|
|||||||
equalLength: {
|
equalLength: {
|
||||||
tooltip: 'xLine',
|
tooltip: 'xLine',
|
||||||
createNode: ({ referenceSegName, tag, rawArgs: args }) => {
|
createNode: ({ referenceSegName, tag, rawArgs: args }) => {
|
||||||
const argVal = getArgLiteralVal(args[0].expr)
|
const argVal = asNum(args[0].expr.value)
|
||||||
if (err(argVal)) return argVal
|
if (err(argVal)) return argVal
|
||||||
const segLen = createSegLen(referenceSegName)
|
const segLen = createSegLen(referenceSegName)
|
||||||
if (argVal > 0) return createCallWrapper('xLine', segLen, tag, argVal)
|
if (argVal > 0) return createCallWrapper('xLine', segLen, tag, argVal)
|
||||||
@ -1118,7 +1133,7 @@ const transformMap: TransformMap = {
|
|||||||
equalLength: {
|
equalLength: {
|
||||||
tooltip: 'yLine',
|
tooltip: 'yLine',
|
||||||
createNode: ({ referenceSegName, tag, rawArgs: args }) => {
|
createNode: ({ referenceSegName, tag, rawArgs: args }) => {
|
||||||
const argVal = getArgLiteralVal(args[0].expr)
|
const argVal = asNum(args[0].expr.value)
|
||||||
if (err(argVal)) return argVal
|
if (err(argVal)) return argVal
|
||||||
let segLen = createSegLen(referenceSegName)
|
let segLen = createSegLen(referenceSegName)
|
||||||
if (argVal < 0) segLen = createUnaryExpression(segLen)
|
if (argVal < 0) segLen = createUnaryExpression(segLen)
|
||||||
@ -1714,7 +1729,7 @@ export function transformAstSketchLines({
|
|||||||
let kclVal = programMemory.get(varName)
|
let kclVal = programMemory.get(varName)
|
||||||
let sketch
|
let sketch
|
||||||
if (kclVal?.type === 'Solid') {
|
if (kclVal?.type === 'Solid') {
|
||||||
sketch = kclVal.sketch
|
sketch = kclVal.value.sketch
|
||||||
} else {
|
} else {
|
||||||
sketch = sketchFromKclValue(kclVal, varName)
|
sketch = sketchFromKclValue(kclVal, varName)
|
||||||
if (err(sketch)) {
|
if (err(sketch)) {
|
||||||
@ -1823,11 +1838,6 @@ function createLastSeg(isX: boolean): Node<CallExpression> {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
function getArgLiteralVal(arg: Literal): number | Error {
|
|
||||||
if (!isNum(arg.value)) return REF_NUM_ERR
|
|
||||||
return arg.value
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ConstraintLevel = 'free' | 'partial' | 'full'
|
export type ConstraintLevel = 'free' | 'partial' | 'full'
|
||||||
|
|
||||||
export function getConstraintLevelFromSourceRange(
|
export function getConstraintLevelFromSourceRange(
|
||||||
|
@ -539,7 +539,8 @@ export function sketchFromKclValueOptional(
|
|||||||
): Sketch | Reason {
|
): Sketch | Reason {
|
||||||
if (obj?.value?.type === 'Sketch') return obj.value
|
if (obj?.value?.type === 'Sketch') return obj.value
|
||||||
if (obj?.value?.type === 'Solid') return obj.value.sketch
|
if (obj?.value?.type === 'Solid') return obj.value.sketch
|
||||||
if (obj?.type === 'Solid') return obj.sketch
|
if (obj?.type === 'Sketch') return obj.value
|
||||||
|
if (obj?.type === 'Solid') return obj.value.sketch
|
||||||
if (!varName) {
|
if (!varName) {
|
||||||
varName = 'a KCL value'
|
varName = 'a KCL value'
|
||||||
}
|
}
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
29
src/lib/base64.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* Converts a string to a base64 string, preserving the UTF-8 encoding
|
||||||
|
*/
|
||||||
|
export function stringToBase64(str: string) {
|
||||||
|
return bytesToBase64(new TextEncoder().encode(str))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a base64 string to a string, preserving the UTF-8 encoding
|
||||||
|
*/
|
||||||
|
export function base64ToString(base64: string) {
|
||||||
|
return new TextDecoder().decode(base64ToBytes(base64))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* From the MDN Web Docs
|
||||||
|
* https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
|
||||||
|
*/
|
||||||
|
function base64ToBytes(base64: string) {
|
||||||
|
const binString = atob(base64)
|
||||||
|
return Uint8Array.from(binString, (m) => m.codePointAt(0)!)
|
||||||
|
}
|
||||||
|
|
||||||
|
function bytesToBase64(bytes: Uint8Array) {
|
||||||
|
const binString = Array.from(bytes, (byte) =>
|
||||||
|
String.fromCodePoint(byte)
|
||||||
|
).join('')
|
||||||
|
return btoa(binString)
|
||||||
|
}
|
@ -13,6 +13,7 @@ import {
|
|||||||
loftValidator,
|
loftValidator,
|
||||||
revolveAxisValidator,
|
revolveAxisValidator,
|
||||||
shellValidator,
|
shellValidator,
|
||||||
|
sweepValidator,
|
||||||
} from './validators'
|
} from './validators'
|
||||||
|
|
||||||
type OutputFormat = Models['OutputFormat_type']
|
type OutputFormat = Models['OutputFormat_type']
|
||||||
@ -42,8 +43,8 @@ export type ModelingCommandSchema = {
|
|||||||
distance: KclCommandValue
|
distance: KclCommandValue
|
||||||
}
|
}
|
||||||
Sweep: {
|
Sweep: {
|
||||||
path: Selections
|
target: Selections
|
||||||
profile: Selections
|
trajectory: Selections
|
||||||
}
|
}
|
||||||
Loft: {
|
Loft: {
|
||||||
selection: Selections
|
selection: Selections
|
||||||
@ -308,25 +309,24 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
|||||||
'Create a 3D body by moving a sketch region along an arbitrary path.',
|
'Create a 3D body by moving a sketch region along an arbitrary path.',
|
||||||
icon: 'sweep',
|
icon: 'sweep',
|
||||||
status: 'development',
|
status: 'development',
|
||||||
needsReview: true,
|
needsReview: false,
|
||||||
args: {
|
args: {
|
||||||
profile: {
|
target: {
|
||||||
inputType: 'selection',
|
inputType: 'selection',
|
||||||
selectionTypes: ['solid2d'],
|
selectionTypes: ['solid2d'],
|
||||||
required: true,
|
required: true,
|
||||||
skip: true,
|
skip: true,
|
||||||
multiple: false,
|
multiple: false,
|
||||||
// TODO: add dry-run validation
|
|
||||||
warningMessage:
|
warningMessage:
|
||||||
'The sweep workflow is new and under tested. Please break it and report issues.',
|
'The sweep workflow is new and under tested. Please break it and report issues.',
|
||||||
},
|
},
|
||||||
path: {
|
trajectory: {
|
||||||
inputType: 'selection',
|
inputType: 'selection',
|
||||||
selectionTypes: ['segment', 'path'],
|
selectionTypes: ['segment', 'path'],
|
||||||
required: true,
|
required: true,
|
||||||
skip: true,
|
skip: false,
|
||||||
multiple: false,
|
multiple: false,
|
||||||
// TODO: add dry-run validation
|
validation: sweepValidator,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
|
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
|
||||||
import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarning'
|
import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarning'
|
||||||
import { StateMachineCommandSetConfig } from 'lib/commandTypes'
|
import { StateMachineCommandSetConfig } from 'lib/commandTypes'
|
||||||
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
|
import { baseUnitLabels, baseUnitsUnion } from 'lib/settings/settingsTypes'
|
||||||
import { projectsMachine } from 'machines/projectsMachine'
|
import { projectsMachine } from 'machines/projectsMachine'
|
||||||
|
|
||||||
export type ProjectsCommandSchema = {
|
export type ProjectsCommandSchema = {
|
||||||
@ -17,6 +20,13 @@ export type ProjectsCommandSchema = {
|
|||||||
oldName: string
|
oldName: string
|
||||||
newName: string
|
newName: string
|
||||||
}
|
}
|
||||||
|
'Import file from URL': {
|
||||||
|
name: string
|
||||||
|
code?: string
|
||||||
|
units: UnitLength_type
|
||||||
|
method: 'newProject' | 'existingProject'
|
||||||
|
projectName?: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const projectsCommandBarConfig: StateMachineCommandSetConfig<
|
export const projectsCommandBarConfig: StateMachineCommandSetConfig<
|
||||||
@ -26,6 +36,7 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
|
|||||||
'Open project': {
|
'Open project': {
|
||||||
icon: 'arrowRight',
|
icon: 'arrowRight',
|
||||||
description: 'Open a project',
|
description: 'Open a project',
|
||||||
|
status: isDesktop() ? 'active' : 'inactive',
|
||||||
args: {
|
args: {
|
||||||
name: {
|
name: {
|
||||||
inputType: 'options',
|
inputType: 'options',
|
||||||
@ -42,6 +53,7 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
|
|||||||
'Create project': {
|
'Create project': {
|
||||||
icon: 'folderPlus',
|
icon: 'folderPlus',
|
||||||
description: 'Create a project',
|
description: 'Create a project',
|
||||||
|
status: isDesktop() ? 'active' : 'inactive',
|
||||||
args: {
|
args: {
|
||||||
name: {
|
name: {
|
||||||
inputType: 'string',
|
inputType: 'string',
|
||||||
@ -53,6 +65,7 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
|
|||||||
'Delete project': {
|
'Delete project': {
|
||||||
icon: 'close',
|
icon: 'close',
|
||||||
description: 'Delete a project',
|
description: 'Delete a project',
|
||||||
|
status: isDesktop() ? 'active' : 'inactive',
|
||||||
needsReview: true,
|
needsReview: true,
|
||||||
reviewMessage: ({ argumentsToSubmit }) =>
|
reviewMessage: ({ argumentsToSubmit }) =>
|
||||||
CommandBarOverwriteWarning({
|
CommandBarOverwriteWarning({
|
||||||
@ -75,6 +88,7 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
|
|||||||
icon: 'folder',
|
icon: 'folder',
|
||||||
description: 'Rename a project',
|
description: 'Rename a project',
|
||||||
needsReview: true,
|
needsReview: true,
|
||||||
|
status: isDesktop() ? 'active' : 'inactive',
|
||||||
args: {
|
args: {
|
||||||
oldName: {
|
oldName: {
|
||||||
inputType: 'options',
|
inputType: 'options',
|
||||||
@ -92,4 +106,80 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'Import file from URL': {
|
||||||
|
icon: 'file',
|
||||||
|
description: 'Create a file',
|
||||||
|
needsReview: true,
|
||||||
|
status: 'active',
|
||||||
|
args: {
|
||||||
|
method: {
|
||||||
|
inputType: 'options',
|
||||||
|
required: true,
|
||||||
|
skip: true,
|
||||||
|
options: isDesktop()
|
||||||
|
? [
|
||||||
|
{ name: 'New project', value: 'newProject' },
|
||||||
|
{ name: 'Existing project', value: 'existingProject' },
|
||||||
|
]
|
||||||
|
: [{ name: 'Overwrite', value: 'existingProject' }],
|
||||||
|
valueSummary(value) {
|
||||||
|
return isDesktop()
|
||||||
|
? value === 'newProject'
|
||||||
|
? 'New project'
|
||||||
|
: 'Existing project'
|
||||||
|
: 'Overwrite'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// TODO: We can't get the currently-opened project to auto-populate here because
|
||||||
|
// it's not available on projectMachine, but lower in fileMachine. Unify these.
|
||||||
|
projectName: {
|
||||||
|
inputType: 'options',
|
||||||
|
required: (commandsContext) =>
|
||||||
|
isDesktop() &&
|
||||||
|
commandsContext.argumentsToSubmit.method === 'existingProject',
|
||||||
|
skip: true,
|
||||||
|
options: (_, context) =>
|
||||||
|
context?.projects.map((p) => ({
|
||||||
|
name: p.name!,
|
||||||
|
value: p.name!,
|
||||||
|
})) || [],
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
inputType: 'string',
|
||||||
|
required: isDesktop(),
|
||||||
|
skip: true,
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
inputType: 'text',
|
||||||
|
required: true,
|
||||||
|
skip: true,
|
||||||
|
valueSummary(value) {
|
||||||
|
const lineCount = value?.trim().split('\n').length
|
||||||
|
return `${lineCount} line${lineCount === 1 ? '' : 's'}`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
units: {
|
||||||
|
inputType: 'options',
|
||||||
|
required: false,
|
||||||
|
skip: true,
|
||||||
|
options: baseUnitsUnion.map((unit) => ({
|
||||||
|
name: baseUnitLabels[unit],
|
||||||
|
value: unit,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
reviewMessage(commandBarContext) {
|
||||||
|
return isDesktop()
|
||||||
|
? `Will add the contents from URL to a new ${
|
||||||
|
commandBarContext.argumentsToSubmit.method === 'newProject'
|
||||||
|
? 'project with file main.kcl'
|
||||||
|
: `file within the project "${commandBarContext.argumentsToSubmit.projectName}"`
|
||||||
|
} named "${
|
||||||
|
commandBarContext.argumentsToSubmit.name
|
||||||
|
}", and set default units to "${
|
||||||
|
commandBarContext.argumentsToSubmit.units
|
||||||
|
}".`
|
||||||
|
: `Will overwrite the contents of the current file with the contents from the URL.`
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
@ -207,3 +207,64 @@ export const shellValidator = async ({
|
|||||||
|
|
||||||
return 'Unable to shell with the provided selection'
|
return 'Unable to shell with the provided selection'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const sweepValidator = async ({
|
||||||
|
context,
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
context: CommandBarContext
|
||||||
|
data: { trajectory: Selections }
|
||||||
|
}): Promise<boolean | string> => {
|
||||||
|
if (!isSelections(data.trajectory)) {
|
||||||
|
console.log('Unable to sweep, selections are missing')
|
||||||
|
return 'Unable to sweep, selections are missing'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the parent path from the segment selection directly
|
||||||
|
const trajectoryArtifact = data.trajectory.graphSelections[0].artifact
|
||||||
|
if (!trajectoryArtifact) {
|
||||||
|
return "Unable to sweep, couldn't find the trajectory artifact"
|
||||||
|
}
|
||||||
|
if (trajectoryArtifact.type !== 'segment') {
|
||||||
|
return "Unable to sweep, couldn't find the target from a non-segment selection"
|
||||||
|
}
|
||||||
|
const trajectory = trajectoryArtifact.pathId
|
||||||
|
|
||||||
|
// Get the former arg in the command bar flow, and retrieve the path from the solid2d directly
|
||||||
|
const targetArg = context.argumentsToSubmit['target'] as Selections
|
||||||
|
const targetArtifact = targetArg.graphSelections[0].artifact
|
||||||
|
if (!targetArtifact) {
|
||||||
|
return "Unable to sweep, couldn't find the profile artifact"
|
||||||
|
}
|
||||||
|
if (targetArtifact.type !== 'solid2d') {
|
||||||
|
return "Unable to sweep, couldn't find the target from a non-solid2d selection"
|
||||||
|
}
|
||||||
|
const target = targetArtifact.pathId
|
||||||
|
|
||||||
|
const sweepCommand = async () => {
|
||||||
|
// TODO: second look on defaults here
|
||||||
|
const DEFAULT_TOLERANCE: Models['LengthUnit_type'] = 1e-7
|
||||||
|
const DEFAULT_SECTIONAL = false
|
||||||
|
const cmdArgs = {
|
||||||
|
target,
|
||||||
|
trajectory,
|
||||||
|
sectional: DEFAULT_SECTIONAL,
|
||||||
|
tolerance: DEFAULT_TOLERANCE,
|
||||||
|
}
|
||||||
|
return await engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'sweep',
|
||||||
|
...cmdArgs,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const attemptSweep = await dryRunWrapper(sweepCommand)
|
||||||
|
if (attemptSweep?.success) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Unable to sweep with the provided selection'
|
||||||
|
}
|
||||||
|
@ -69,6 +69,7 @@ export const KCL_DEFAULT_DEGREE = `360`
|
|||||||
export const TEST_SETTINGS_FILE_KEY = 'playwright-test-settings'
|
export const TEST_SETTINGS_FILE_KEY = 'playwright-test-settings'
|
||||||
|
|
||||||
export const DEFAULT_HOST = 'https://api.zoo.dev'
|
export const DEFAULT_HOST = 'https://api.zoo.dev'
|
||||||
|
export const PROD_APP_URL = 'https://app.zoo.dev'
|
||||||
export const SETTINGS_FILE_NAME = 'settings.toml'
|
export const SETTINGS_FILE_NAME = 'settings.toml'
|
||||||
export const TOKEN_FILE_NAME = 'token.txt'
|
export const TOKEN_FILE_NAME = 'token.txt'
|
||||||
export const PROJECT_SETTINGS_FILE_NAME = 'project.toml'
|
export const PROJECT_SETTINGS_FILE_NAME = 'project.toml'
|
||||||
@ -110,6 +111,9 @@ export const KCL_SAMPLES_MANIFEST_URLS = {
|
|||||||
localFallback: '/kcl-samples-manifest-fallback.json',
|
localFallback: '/kcl-samples-manifest-fallback.json',
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
/** URL parameter to create a file */
|
||||||
|
export const CREATE_FILE_URL_PARAM = 'create-file'
|
||||||
|
|
||||||
/** Toast id for the app auto-updater toast */
|
/** Toast id for the app auto-updater toast */
|
||||||
export const AUTO_UPDATER_TOAST_ID = 'auto-updater-toast'
|
export const AUTO_UPDATER_TOAST_ID = 'auto-updater-toast'
|
||||||
|
|
||||||
@ -139,3 +143,12 @@ export const VIEW_NAMES_SEMANTIC = {
|
|||||||
} as const
|
} as const
|
||||||
/** The modeling sidebar buttons' IDs get a suffix to prevent collisions */
|
/** The modeling sidebar buttons' IDs get a suffix to prevent collisions */
|
||||||
export const SIDEBAR_BUTTON_SUFFIX = '-pane-button'
|
export const SIDEBAR_BUTTON_SUFFIX = '-pane-button'
|
||||||
|
|
||||||
|
/** Custom URL protocol our desktop registers */
|
||||||
|
export const ZOO_STUDIO_PROTOCOL = 'zoo-studio:'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A query parameter that triggers a modal
|
||||||
|
* to "open in desktop app" when present in the URL
|
||||||
|
*/
|
||||||
|
export const ASK_TO_OPEN_QUERY_PARAM = 'ask-open-desktop'
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarning'
|
import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarning'
|
||||||
import { Command, CommandArgumentOption } from './commandTypes'
|
import { Command, CommandArgumentOption } from './commandTypes'
|
||||||
import { kclManager } from './singletons'
|
import { codeManager, kclManager } from './singletons'
|
||||||
import { isDesktop } from './isDesktop'
|
import { isDesktop } from './isDesktop'
|
||||||
import { FILE_EXT, PROJECT_SETTINGS_FILE_NAME } from './constants'
|
import { FILE_EXT, PROJECT_SETTINGS_FILE_NAME } from './constants'
|
||||||
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
|
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
|
||||||
import { parseProjectSettings } from 'lang/wasm'
|
import { parseProjectSettings } from 'lang/wasm'
|
||||||
import { err, reportRejection } from './trap'
|
import { err, reportRejection } from './trap'
|
||||||
import { projectConfigurationToSettingsPayload } from './settings/settingsUtils'
|
import { projectConfigurationToSettingsPayload } from './settings/settingsUtils'
|
||||||
|
import { copyFileShareLink } from './links'
|
||||||
|
import { IndexLoaderData } from './types'
|
||||||
|
|
||||||
interface OnSubmitProps {
|
interface OnSubmitProps {
|
||||||
sampleName: string
|
sampleName: string
|
||||||
@ -15,10 +17,21 @@ interface OnSubmitProps {
|
|||||||
method: 'overwrite' | 'newFile'
|
method: 'overwrite' | 'newFile'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function kclCommands(
|
interface KclCommandConfig {
|
||||||
onSubmit: (p: OnSubmitProps) => Promise<void>,
|
// TODO: find a different approach that doesn't require
|
||||||
providedOptions: CommandArgumentOption<string>[]
|
// special props for a single command
|
||||||
): Command[] {
|
specialPropsForSampleCommand: {
|
||||||
|
onSubmit: (p: OnSubmitProps) => Promise<void>
|
||||||
|
providedOptions: CommandArgumentOption<string>[]
|
||||||
|
}
|
||||||
|
projectData: IndexLoaderData
|
||||||
|
authToken: string
|
||||||
|
settings: {
|
||||||
|
defaultUnit: UnitLength_type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function kclCommands(commandProps: KclCommandConfig): Command[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'format-code',
|
name: 'format-code',
|
||||||
@ -107,7 +120,9 @@ export function kclCommands(
|
|||||||
)
|
)
|
||||||
.then((props) => {
|
.then((props) => {
|
||||||
if (props?.code) {
|
if (props?.code) {
|
||||||
onSubmit(props).catch(reportError)
|
commandProps.specialPropsForSampleCommand
|
||||||
|
.onSubmit(props)
|
||||||
|
.catch(reportError)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(reportError)
|
.catch(reportError)
|
||||||
@ -149,9 +164,25 @@ export function kclCommands(
|
|||||||
}
|
}
|
||||||
return value
|
return value
|
||||||
},
|
},
|
||||||
options: providedOptions,
|
options: commandProps.specialPropsForSampleCommand.providedOptions,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// name: 'share-file-link',
|
||||||
|
// displayName: 'Share file',
|
||||||
|
// description: 'Create a link that contains a copy of the current file.',
|
||||||
|
// groupId: 'code',
|
||||||
|
// needsReview: false,
|
||||||
|
// icon: 'link',
|
||||||
|
// onSubmit: () => {
|
||||||
|
// copyFileShareLink({
|
||||||
|
// token: commandProps.authToken,
|
||||||
|
// code: codeManager.code,
|
||||||
|
// name: commandProps.projectData.project?.name || '',
|
||||||
|
// units: commandProps.settings.defaultUnit,
|
||||||
|
// }).catch(reportRejection)
|
||||||
|
// },
|
||||||
|
// },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
16
src/lib/links.test.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { createCreateFileUrl } from './links'
|
||||||
|
|
||||||
|
describe(`link creation tests`, () => {
|
||||||
|
test(`createCreateFileUrl happy path`, async () => {
|
||||||
|
const code = `extrusionDistance = 12`
|
||||||
|
const name = `test`
|
||||||
|
const units = `mm`
|
||||||
|
|
||||||
|
// Converted with external online tools
|
||||||
|
const expectedEncodedCode = `ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg%3D%3D`
|
||||||
|
const expectedLink = `http://localhost:3000/?create-file=true&name=test&units=mm&code=${expectedEncodedCode}&ask-open-desktop=true`
|
||||||
|
|
||||||
|
const result = createCreateFileUrl({ code, name, units })
|
||||||
|
expect(result.toString()).toBe(expectedLink)
|
||||||
|
})
|
||||||
|
})
|
100
src/lib/links.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
|
||||||
|
import {
|
||||||
|
ASK_TO_OPEN_QUERY_PARAM,
|
||||||
|
CREATE_FILE_URL_PARAM,
|
||||||
|
PROD_APP_URL,
|
||||||
|
} from './constants'
|
||||||
|
import { stringToBase64 } from './base64'
|
||||||
|
import { DEV, VITE_KC_API_BASE_URL } from 'env'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
import { err } from './trap'
|
||||||
|
export interface FileLinkParams {
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
units: UnitLength_type
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function copyFileShareLink(
|
||||||
|
args: FileLinkParams & { token: string }
|
||||||
|
) {
|
||||||
|
const token = args.token
|
||||||
|
if (!token) {
|
||||||
|
toast.error('You need to be signed in to share a file.', {
|
||||||
|
duration: 5000,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const shareUrl = createCreateFileUrl(args)
|
||||||
|
const shortlink = await createShortlink(token, shareUrl.toString())
|
||||||
|
|
||||||
|
if (err(shortlink)) {
|
||||||
|
toast.error(shortlink.message, {
|
||||||
|
duration: 5000,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await globalThis.navigator.clipboard.writeText(shortlink.url)
|
||||||
|
toast.success(
|
||||||
|
'Link copied to clipboard. Anyone who clicks this link will get a copy of this file. Share carefully!',
|
||||||
|
{
|
||||||
|
duration: 5000,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a URL with the necessary query parameters to trigger
|
||||||
|
* the "Import file from URL" command in the app.
|
||||||
|
*
|
||||||
|
* With the additional step of asking the user if they want to
|
||||||
|
* open the URL in the desktop app.
|
||||||
|
*/
|
||||||
|
export function createCreateFileUrl({ code, name, units }: FileLinkParams) {
|
||||||
|
// Use the dev server if we are in development mode
|
||||||
|
let origin = DEV ? 'http://localhost:3000' : PROD_APP_URL
|
||||||
|
const searchParams = new URLSearchParams({
|
||||||
|
[CREATE_FILE_URL_PARAM]: String(true),
|
||||||
|
name,
|
||||||
|
units,
|
||||||
|
code: stringToBase64(code),
|
||||||
|
[ASK_TO_OPEN_QUERY_PARAM]: String(true),
|
||||||
|
})
|
||||||
|
const createFileUrl = new URL(`?${searchParams.toString()}`, origin)
|
||||||
|
|
||||||
|
return createFileUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a file's code, name, and units, creates shareable link to the
|
||||||
|
* web app with a query parameter that triggers a modal to "open in desktop app".
|
||||||
|
* That modal is defined in the `OpenInDesktopAppHandler` component.
|
||||||
|
* TODO: update the return type to use TS library after its updated
|
||||||
|
*/
|
||||||
|
export async function createShortlink(
|
||||||
|
token: string,
|
||||||
|
url: string
|
||||||
|
): Promise<Error | { key: string; url: string }> {
|
||||||
|
/**
|
||||||
|
* We don't use our `withBaseURL` function here because
|
||||||
|
* there is no URL shortener service in the dev API.
|
||||||
|
*/
|
||||||
|
const response = await fetch(`${VITE_KC_API_BASE_URL}/user/shortlinks`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
url,
|
||||||
|
// In future we can support org-scoped and password-protected shortlinks here
|
||||||
|
// https://zoo.dev/docs/api/shortlinks/create-a-shortlink-for-a-user?lang=typescript
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json()
|
||||||
|
return new Error(`Failed to create shortlink: ${error.message}`)
|
||||||
|
} else {
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
|
}
|
@ -114,7 +114,7 @@ export const fileLoader: LoaderFunction = async (
|
|||||||
return redirect(
|
return redirect(
|
||||||
`${PATHS.FILE}/${encodeURIComponent(
|
`${PATHS.FILE}/${encodeURIComponent(
|
||||||
isDesktop() ? fallbackFile : params.id + '/' + PROJECT_ENTRYPOINT
|
isDesktop() ? fallbackFile : params.id + '/' + PROJECT_ENTRYPOINT
|
||||||
)}`
|
)}${new URL(routerData.request.url).search || ''}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,11 +188,14 @@ export const fileLoader: LoaderFunction = async (
|
|||||||
|
|
||||||
// Loads the settings and by extension the projects in the default directory
|
// Loads the settings and by extension the projects in the default directory
|
||||||
// and returns them to the Home route, along with any errors that occurred
|
// and returns them to the Home route, along with any errors that occurred
|
||||||
export const homeLoader: LoaderFunction = async (): Promise<
|
export const homeLoader: LoaderFunction = async ({
|
||||||
HomeLoaderData | Response
|
request,
|
||||||
> => {
|
}): Promise<HomeLoaderData | Response> => {
|
||||||
|
const url = new URL(request.url)
|
||||||
if (!isDesktop()) {
|
if (!isDesktop()) {
|
||||||
return redirect(PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME)
|
return redirect(
|
||||||
|
PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME + (url.search || '')
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
@ -68,10 +68,6 @@ interface TextToKclProps {
|
|||||||
data?: unknown
|
data?: unknown
|
||||||
) => unknown
|
) => unknown
|
||||||
navigate: NavigateFunction
|
navigate: NavigateFunction
|
||||||
commandBarSend: (
|
|
||||||
type: EventFrom<typeof commandBarMachine>,
|
|
||||||
data?: unknown
|
|
||||||
) => unknown
|
|
||||||
context: ContextFrom<typeof fileMachine>
|
context: ContextFrom<typeof fileMachine>
|
||||||
token?: string
|
token?: string
|
||||||
settings: {
|
settings: {
|
||||||
@ -84,7 +80,6 @@ export async function submitAndAwaitTextToKcl({
|
|||||||
trimmedPrompt,
|
trimmedPrompt,
|
||||||
fileMachineSend,
|
fileMachineSend,
|
||||||
navigate,
|
navigate,
|
||||||
commandBarSend,
|
|
||||||
context,
|
context,
|
||||||
token,
|
token,
|
||||||
settings,
|
settings,
|
||||||
@ -96,7 +91,6 @@ export async function submitAndAwaitTextToKcl({
|
|||||||
ToastTextToCadError({
|
ToastTextToCadError({
|
||||||
toastId,
|
toastId,
|
||||||
message,
|
message,
|
||||||
commandBarSend,
|
|
||||||
prompt: trimmedPrompt,
|
prompt: trimmedPrompt,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
@ -195,7 +189,7 @@ export async function submitAndAwaitTextToKcl({
|
|||||||
.toLowerCase()}${FILE_EXT}`
|
.toLowerCase()}${FILE_EXT}`
|
||||||
|
|
||||||
if (isDesktop()) {
|
if (isDesktop()) {
|
||||||
// We have to pre-emptively run our unique file name logic,
|
// We have to preemptively run our unique file name logic,
|
||||||
// so that we can pass the unique file name to the toast,
|
// so that we can pass the unique file name to the toast,
|
||||||
// and by extension the file-deletion-on-reject logic.
|
// and by extension the file-deletion-on-reject logic.
|
||||||
newFileName = getNextFileName({
|
newFileName = getNextFileName({
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { CustomIconName } from 'components/CustomIcon'
|
import { CustomIconName } from 'components/CustomIcon'
|
||||||
import { DEV } from 'env'
|
import { DEV } from 'env'
|
||||||
import { commandBarMachine } from 'machines/commandBarMachine'
|
import { commandBarActor, commandBarMachine } from 'machines/commandBarMachine'
|
||||||
import {
|
import {
|
||||||
canRectangleOrCircleTool,
|
canRectangleOrCircleTool,
|
||||||
isClosedSketch,
|
isClosedSketch,
|
||||||
@ -21,7 +21,6 @@ type ToolbarMode = {
|
|||||||
export interface ToolbarItemCallbackProps {
|
export interface ToolbarItemCallbackProps {
|
||||||
modelingState: StateFrom<typeof modelingMachine>
|
modelingState: StateFrom<typeof modelingMachine>
|
||||||
modelingSend: (event: EventFrom<typeof modelingMachine>) => void
|
modelingSend: (event: EventFrom<typeof modelingMachine>) => void
|
||||||
commandBarSend: (event: EventFrom<typeof commandBarMachine>) => void
|
|
||||||
sketchPathId: string | false
|
sketchPathId: string | false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,8 +83,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
'break',
|
'break',
|
||||||
{
|
{
|
||||||
id: 'extrude',
|
id: 'extrude',
|
||||||
onClick: ({ commandBarSend }) =>
|
onClick: () =>
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: { name: 'Extrude', groupId: 'modeling' },
|
data: { name: 'Extrude', groupId: 'modeling' },
|
||||||
}),
|
}),
|
||||||
@ -98,8 +97,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'revolve',
|
id: 'revolve',
|
||||||
onClick: ({ commandBarSend }) =>
|
onClick: () =>
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: { name: 'Revolve', groupId: 'modeling' },
|
data: { name: 'Revolve', groupId: 'modeling' },
|
||||||
}),
|
}),
|
||||||
@ -119,8 +118,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'sweep',
|
id: 'sweep',
|
||||||
onClick: ({ commandBarSend }) =>
|
onClick: () =>
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: { name: 'Sweep', groupId: 'modeling' },
|
data: { name: 'Sweep', groupId: 'modeling' },
|
||||||
}),
|
}),
|
||||||
@ -139,8 +138,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'loft',
|
id: 'loft',
|
||||||
onClick: ({ commandBarSend }) =>
|
onClick: () =>
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: { name: 'Loft', groupId: 'modeling' },
|
data: { name: 'Loft', groupId: 'modeling' },
|
||||||
}),
|
}),
|
||||||
@ -160,8 +159,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
'break',
|
'break',
|
||||||
{
|
{
|
||||||
id: 'fillet3d',
|
id: 'fillet3d',
|
||||||
onClick: ({ commandBarSend }) =>
|
onClick: () =>
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: { name: 'Fillet', groupId: 'modeling' },
|
data: { name: 'Fillet', groupId: 'modeling' },
|
||||||
}),
|
}),
|
||||||
@ -174,8 +173,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'chamfer3d',
|
id: 'chamfer3d',
|
||||||
onClick: ({ commandBarSend }) =>
|
onClick: () =>
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: { name: 'Chamfer', groupId: 'modeling' },
|
data: { name: 'Chamfer', groupId: 'modeling' },
|
||||||
}),
|
}),
|
||||||
@ -188,8 +187,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'shell',
|
id: 'shell',
|
||||||
onClick: ({ commandBarSend }) => {
|
onClick: () => {
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: { name: 'Shell', groupId: 'modeling' },
|
data: { name: 'Shell', groupId: 'modeling' },
|
||||||
})
|
})
|
||||||
@ -269,8 +268,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
id: 'plane-offset',
|
id: 'plane-offset',
|
||||||
onClick: ({ commandBarSend }) => {
|
onClick: () => {
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: { name: 'Offset plane', groupId: 'modeling' },
|
data: { name: 'Offset plane', groupId: 'modeling' },
|
||||||
})
|
})
|
||||||
@ -280,7 +279,12 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
status: 'available',
|
status: 'available',
|
||||||
title: 'Offset plane',
|
title: 'Offset plane',
|
||||||
description: 'Create a plane parallel to an existing plane.',
|
description: 'Create a plane parallel to an existing plane.',
|
||||||
links: [],
|
links: [
|
||||||
|
{
|
||||||
|
label: 'KCL docs',
|
||||||
|
url: 'https://zoo.dev/docs/kcl/offsetPlane',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'plane-points',
|
id: 'plane-points',
|
||||||
@ -296,8 +300,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
id: 'text-to-cad',
|
id: 'text-to-cad',
|
||||||
onClick: ({ commandBarSend }) =>
|
onClick: () =>
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: { name: 'Text-to-CAD', groupId: 'modeling' },
|
data: { name: 'Text-to-CAD', groupId: 'modeling' },
|
||||||
}),
|
}),
|
||||||
@ -305,12 +309,17 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
status: 'available',
|
status: 'available',
|
||||||
title: 'Text-to-CAD',
|
title: 'Text-to-CAD',
|
||||||
description: 'Generate geometry from a text prompt.',
|
description: 'Generate geometry from a text prompt.',
|
||||||
links: [],
|
links: [
|
||||||
|
{
|
||||||
|
label: 'API docs',
|
||||||
|
url: 'https://zoo.dev/docs/api/ml/generate-a-cad-model-from-text',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'prompt-to-edit',
|
id: 'prompt-to-edit',
|
||||||
onClick: ({ commandBarSend }) =>
|
onClick: () =>
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: { name: 'Prompt-to-edit', groupId: 'modeling' },
|
data: { name: 'Prompt-to-edit', groupId: 'modeling' },
|
||||||
}),
|
}),
|
||||||
@ -583,8 +592,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
{
|
{
|
||||||
id: 'constraint-length',
|
id: 'constraint-length',
|
||||||
disabled: (state) => !state.matches({ Sketch: 'SketchIdle' }),
|
disabled: (state) => !state.matches({ Sketch: 'SketchIdle' }),
|
||||||
onClick: ({ commandBarSend }) =>
|
onClick: () =>
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: {
|
data: {
|
||||||
name: 'Constrain length',
|
name: 'Constrain length',
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { assign, fromPromise, setup } from 'xstate'
|
import { assign, createActor, fromPromise, setup, SnapshotFrom } from 'xstate'
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandArgument,
|
CommandArgument,
|
||||||
@ -9,6 +9,7 @@ import { Selections__old } from 'lib/selections'
|
|||||||
import { getCommandArgumentKclValuesOnly } from 'lib/commandUtils'
|
import { getCommandArgumentKclValuesOnly } from 'lib/commandUtils'
|
||||||
import { MachineManager } from 'components/MachineManagerProvider'
|
import { MachineManager } from 'components/MachineManagerProvider'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
|
import { useSelector } from '@xstate/react'
|
||||||
|
|
||||||
export type CommandBarContext = {
|
export type CommandBarContext = {
|
||||||
commands: Command[]
|
commands: Command[]
|
||||||
@ -247,8 +248,17 @@ export const commandBarMachine = setup({
|
|||||||
guards: {
|
guards: {
|
||||||
'Command needs review': ({ context }) =>
|
'Command needs review': ({ context }) =>
|
||||||
context.selectedCommand?.needsReview || false,
|
context.selectedCommand?.needsReview || false,
|
||||||
'Command has no arguments': () => false,
|
'Command has no arguments': ({ context }) => {
|
||||||
'All arguments are skippable': () => false,
|
return (
|
||||||
|
!context.selectedCommand?.args ||
|
||||||
|
Object.keys(context.selectedCommand?.args).length === 0
|
||||||
|
)
|
||||||
|
},
|
||||||
|
'All arguments are skippable': ({ context }) => {
|
||||||
|
return Object.values(context.selectedCommand!.args!).every(
|
||||||
|
(argConfig) => argConfig.skip
|
||||||
|
)
|
||||||
|
},
|
||||||
'Has selected command': ({ context }) => !!context.selectedCommand,
|
'Has selected command': ({ context }) => !!context.selectedCommand,
|
||||||
},
|
},
|
||||||
actors: {
|
actors: {
|
||||||
@ -620,3 +630,12 @@ function sortCommands(a: Command, b: Command) {
|
|||||||
if (a.groupId === 'settings' && !(b.groupId === 'settings')) return 1
|
if (a.groupId === 'settings' && !(b.groupId === 'settings')) return 1
|
||||||
return a.name.localeCompare(b.name)
|
return a.name.localeCompare(b.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const commandBarActor = createActor(commandBarMachine).start()
|
||||||
|
|
||||||
|
/** Basic state snapshot selector */
|
||||||
|
const cmdBarStateSelector = (state: SnapshotFrom<typeof commandBarActor>) =>
|
||||||
|
state
|
||||||
|
export const useCommandBarState = () => {
|
||||||
|
return useSelector(commandBarActor, cmdBarStateSelector)
|
||||||
|
}
|
||||||
|
@ -1561,40 +1561,40 @@ export const modelingMachine = setup({
|
|||||||
if (!input) return new Error('No input provided')
|
if (!input) return new Error('No input provided')
|
||||||
// Extract inputs
|
// Extract inputs
|
||||||
const ast = kclManager.ast
|
const ast = kclManager.ast
|
||||||
const { profile, path } = input
|
const { target, trajectory } = input
|
||||||
|
|
||||||
// Find the profile declaration
|
// Find the profile declaration
|
||||||
const profileNodePath = getNodePathFromSourceRange(
|
const targetNodePath = getNodePathFromSourceRange(
|
||||||
ast,
|
ast,
|
||||||
profile.graphSelections[0].codeRef.range
|
target.graphSelections[0].codeRef.range
|
||||||
)
|
)
|
||||||
const profileNode = getNodeFromPath<VariableDeclarator>(
|
const targetNode = getNodeFromPath<VariableDeclarator>(
|
||||||
ast,
|
ast,
|
||||||
profileNodePath,
|
targetNodePath,
|
||||||
'VariableDeclarator'
|
'VariableDeclarator'
|
||||||
)
|
)
|
||||||
if (err(profileNode)) {
|
if (err(targetNode)) {
|
||||||
return new Error("Couldn't parse profile selection")
|
return new Error("Couldn't parse profile selection")
|
||||||
}
|
}
|
||||||
const profileDeclarator = profileNode.node
|
const targetDeclarator = targetNode.node
|
||||||
|
|
||||||
// Find the path declaration
|
// Find the path declaration
|
||||||
const pathNodePath = getNodePathFromSourceRange(
|
const trajectoryNodePath = getNodePathFromSourceRange(
|
||||||
ast,
|
ast,
|
||||||
path.graphSelections[0].codeRef.range
|
trajectory.graphSelections[0].codeRef.range
|
||||||
)
|
)
|
||||||
const pathNode = getNodeFromPath<VariableDeclarator>(
|
const trajectoryNode = getNodeFromPath<VariableDeclarator>(
|
||||||
ast,
|
ast,
|
||||||
pathNodePath,
|
trajectoryNodePath,
|
||||||
'VariableDeclarator'
|
'VariableDeclarator'
|
||||||
)
|
)
|
||||||
if (err(pathNode)) {
|
if (err(trajectoryNode)) {
|
||||||
return new Error("Couldn't parse path selection")
|
return new Error("Couldn't parse path selection")
|
||||||
}
|
}
|
||||||
const pathDeclarator = pathNode.node
|
const trajectoryDeclarator = trajectoryNode.node
|
||||||
|
|
||||||
// Perform the sweep
|
// Perform the sweep
|
||||||
const sweepRes = addSweep(ast, profileDeclarator, pathDeclarator)
|
const sweepRes = addSweep(ast, targetDeclarator, trajectoryDeclarator)
|
||||||
const updateAstResult = await kclManager.updateAst(
|
const updateAstResult = await kclManager.updateAst(
|
||||||
sweepRes.modifiedAst,
|
sweepRes.modifiedAst,
|
||||||
true,
|
true,
|
||||||
|
@ -25,6 +25,10 @@ export const projectsMachine = setup({
|
|||||||
type: 'Delete project'
|
type: 'Delete project'
|
||||||
data: ProjectsCommandSchema['Delete project']
|
data: ProjectsCommandSchema['Delete project']
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: 'Import file from URL'
|
||||||
|
data: ProjectsCommandSchema['Import file from URL']
|
||||||
|
}
|
||||||
| { type: 'navigate'; data: { name: string } }
|
| { type: 'navigate'; data: { name: string } }
|
||||||
| {
|
| {
|
||||||
type: 'xstate.done.actor.read-projects'
|
type: 'xstate.done.actor.read-projects'
|
||||||
@ -42,6 +46,10 @@ export const projectsMachine = setup({
|
|||||||
type: 'xstate.done.actor.rename-project'
|
type: 'xstate.done.actor.rename-project'
|
||||||
output: { message: string; oldName: string; newName: string }
|
output: { message: string; oldName: string; newName: string }
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: 'xstate.done.actor.create-file'
|
||||||
|
output: { message: string; projectName: string; fileName: string }
|
||||||
|
}
|
||||||
| { type: 'assign'; data: { [key: string]: any } },
|
| { type: 'assign'; data: { [key: string]: any } },
|
||||||
input: {} as {
|
input: {} as {
|
||||||
projects: Project[]
|
projects: Project[]
|
||||||
@ -60,6 +68,7 @@ export const projectsMachine = setup({
|
|||||||
toastError: () => {},
|
toastError: () => {},
|
||||||
navigateToProject: () => {},
|
navigateToProject: () => {},
|
||||||
navigateToProjectIfNeeded: () => {},
|
navigateToProjectIfNeeded: () => {},
|
||||||
|
navigateToFile: () => {},
|
||||||
},
|
},
|
||||||
actors: {
|
actors: {
|
||||||
readProjects: fromPromise(() => Promise.resolve([] as Project[])),
|
readProjects: fromPromise(() => Promise.resolve([] as Project[])),
|
||||||
@ -90,12 +99,22 @@ export const projectsMachine = setup({
|
|||||||
name: '',
|
name: '',
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
|
createFile: fromPromise(
|
||||||
|
(_: {
|
||||||
|
input: ProjectsCommandSchema['Import file from URL'] & {
|
||||||
|
projects: Project[]
|
||||||
|
}
|
||||||
|
}) => Promise.resolve({ message: '', projectName: '', fileName: '' })
|
||||||
|
),
|
||||||
},
|
},
|
||||||
guards: {
|
guards: {
|
||||||
'Has at least 1 project': () => false,
|
'Has at least 1 project': ({ event }) => {
|
||||||
|
if (event.type !== 'xstate.done.actor.read-projects') return false
|
||||||
|
return event.output.length ? event.output.length >= 1 : false
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}).createMachine({
|
}).createMachine({
|
||||||
/** @xstate-layout N4IgpgJg5mDOIC5QAkD2BbMACdBDAxgBYCWAdmAMS6yzFSkDaADALqKgAOqtALsaqXYgAHogAsAJgA0IAJ6IAjBIkA2AHQAOCUw0qNkjQE4xYjQF8zMtJhwES5NcmpZSqLBwBOqAFZh8PWAoAJTBcCHcvX39YZjYkEC5efkF40QQFFQBmAHY1Qwz9QwBWU0NMrRl5BGyxBTUVJlVM01rtBXNLEGtsPCIyMEdnVwifPwCKAGEPUJ5sT1H-WKFE4j4BITSMzMzNIsyJDSZMhSYmFWzKxAkTNSYxIuU9zOKT7IsrDB67fsHYEajxiEwv8xjFWMtuKtkhsrpJboZsioincFMdTIjLggVGJ1GImMVMg0JISjEV3l1PrY+g4nH95gDAiFSLgbPSxkt4is1ilQGlrhJ4YjkbU0RoMXJEIYkWoikV8hJinjdOdyd0qfYBrSQdFJtNcLNtTwOZxIdyYQh+YKkSjReKqqYBQoEQpsgiSi9MqrKb0Nb9DYEACJgAA2YANbMW4M5puhqVhAvxQptCnRKkxCleuw0Gm2+2aRVRXpsPp+Woj4wA8hwwKRDcaEjH1nGLXDE9aRSmxWmJVipWocmdsbL8kxC501SWHFMZmQoIaKBABAMyAA3VAAawG+D1swAtOX61zY7zENlEWoMkoatcdPoipjCWIZRIirozsPiYYi19qQNp-rZ3nMAPC8Dw1A4YN9QAM1QDx0DUbcZjAfdInZKMTSSJsT2qc9Lwka8lTvTFTB2WUMiyQwc3yPRv3VH4mRZQDywXJc1FXDcBmmZlMBQhYjXQhtMJ5ERT1wlQr0kQj7kxKiZTxM5s2zbQEVoycBgY9AmNQ-wKGA0DwMgngYLgtQuJZZCDwEo8sJEnD1Dwgjb2kntig0GUkWVfZDEMfFVO+Bwg1DPhSDnZjFwcdjNzUCAQzDCztP4uIMKhGy0jPezxPwySnPvHsTjlPIhyOHRsnwlM-N-NRArDLS+N0kDYIM6DYPgmKgvivjD0bYS+VbBF21RTs7UUUcimfRpXyYRF8LFCrfSBCBaoZFiItINcor1CBeIZLqhPNdKL0yxzs2cqoNDqdpSo0EoSoLOb6NCRaQv9FblzWjjTMe7bQQYBQksElKesUMRjFubRMllCHsm2a5MVldRDBfFpmj0IpxPuhwFqW0F6v0iDmpMzbvuiXbAfNFNQcaI5IaKaH9jETFsUMNRVDxOU5TxBULE6VwYvgeIJ38sAIT25td27KpdzG7yZeho5slHVmMc1IY3HLfnkrNZt2hOW5smRCQM2uhQHkZ6VtkRBFnmu0qFGVv11ZFsnm0kdN8QFJVjlUPZ9co+3-2C0KEqdrXsJMJ8ryc86iUyYiCvxFNzldb3jHtjTsf8EPj1ssQchZlR8jR5EmDRrIGZcuF7maF1aZtslx29IWqtiwPDSz1LxDxepnlOfYDghp03eyOpngzXuYdHNPHozgJ26B9IXR2cTvP0BVkSRXKqgLtyq8OfQcXOwlubMIA */
|
/** @xstate-layout N4IgpgJg5mDOIC5QAkD2BbMACdBDAxgBYCWAdmAMS6yzFSkDaADALqKgAOqtALsaqXYgAHogAsAJgA0IAJ6IAjAHYAbADoArBJVMFTCQA4mTAMwmxAXwsy0mHARLkKASXRcATjywAzYgBtsb3cMLABVACUAGWY2JBAuXn5BONEESRl5BAUFFQM1HQUxJSYATmyFAyUTKxsMbDwiMjA1ZGosUlQsDmCAKzB8HlgKcLBcCC7e-sGYoQTiPgEhVIUy9SKSiQ0NEwkclRyMxGKJNRKlMRVDU23zpRqQW3qHJpa2jonUPoGhgGF3UZ42G6nymMzicwWyVAyzKJzECg0OiYGjERhK+kOWUMSlOGiUlTEJlMxRKBnuj3sjXIr1gHy+g2Go3GwPpsDBnG48ySS0QGj0+Q0JTOGkqBg2JhKmNRJy2OyUEn0Kml5LqlMczVatJZUyGI1IuDs2oG7PinMhPIQfKYAqFShF+PFkrkRwkYjU8OUShKKhMKg0pUs1geqoa6ppdJ1FD+AKBk2NrFmZu5KV5-L9tvtYokEsxelRagUCoMiMumyUdpVdlDL01Ee+FAAImAAoC6zwTRDk9DU9b08LRY7MWd1AZcoS-aoLiYFJWnlSNW0jQyAPIcMCkNsdpOLFOWtOC-sO7NOzLbGWFbY6acmFESWdql7R3B8UhQNsUCACZpkABuqAA1s0+D-M+YAALRLluiQ7t2WRMGIbryjeBgGBIEglNsCi5sY6hMOchQqEoCLFCh97VtST4vm+S4UGA7jBO4agcH4z7eKg7joGowExhBcbtgm4LblCIiKPBiHZiKqHoZhuaFhoBbGMYBhiPBCgStUQYUuRzR6gaZDUXxH5fmov4Ac0-z6pgvEgvGsQctBwnLGJahIZJaEYdOmKEfJuQSoRRKSRcZHPNSunoPp750QxTEsTwbEcWoFkGuBkECfZXIwSJcEIS5Ekoe5MnOggXqIaUqFiiUhIInemkhiFzRNi2EU0Z+1KmYBagQM2YCAtZ9JQRljmiTlrn5dJnlFShOLwcpZjKBs+KBrUVb1WojU9c1hlRexMWsexnFdS2KV8QN5q7laNqHlmOaTcpmjoWc8L6HoYrBfOagjGMm02QyrXfqQf4dSBEB9Tqp1dllebichUkeVhRXTiU+QecUKhCps4pvWGn0QN9rJGW1ANmYlTKg98DAKHZpoORanoyqh8oImU3pKJifJ5Ohdr7CYqjIvKWMvDjeORttjHMXtCXA2T0xpdTg20+W9MSIzgorIRXlpr6ORbPs+jwQLFEgVRPj+JQf0mUTHXcaBYG+AE4OZcsxbuts5hbCK+zFmImJgSc5zPV6BQVD6RQG80lERXblCi7tcX7VxRvgVHDtDQgaE4vK2gXKiTA6ItubmJo8IoqOylERUVhBh0XXwHEWn1YmNO7mBKg+2KpzK2IpIBUwBhqUtwYre9tbvEutfpWdsFFnkPpVJnQqz15Ozur36FoypREbGH4Zj438u7tO1qIu5qK5PBGyYjebpEkS44e9oVTbxHr5tnvk9ZeXajTuW+zaHNuzYRUmoeCEp-RKlUHJbeYVhYDDfhDVIxR5IGGnISQk6E+6EkxMUBQX9c66EMMg3Q5Zt7rWNkuOBjsjilDUAqQsZQzzgOkJNUk7o7SmF-tiXOUCmQwMGBQ1OF4TgVGzFUG8yIxQGDZiYPI4oqh+mPsrDSy05xhmfm+KO-CLT+iRucEUuwFQqWzMWXM2YTAuTtL6ZBylkRcMrkAA */
|
||||||
id: 'Home machine',
|
id: 'Home machine',
|
||||||
|
|
||||||
initial: 'Reading projects',
|
initial: 'Reading projects',
|
||||||
@ -111,6 +130,8 @@ export const projectsMachine = setup({
|
|||||||
})),
|
})),
|
||||||
target: '.Reading projects',
|
target: '.Reading projects',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'Import file from URL': '.Creating file',
|
||||||
},
|
},
|
||||||
states: {
|
states: {
|
||||||
'Has no projects': {
|
'Has no projects': {
|
||||||
@ -155,7 +176,10 @@ export const projectsMachine = setup({
|
|||||||
id: 'create-project',
|
id: 'create-project',
|
||||||
src: 'createProject',
|
src: 'createProject',
|
||||||
input: ({ event, context }) => {
|
input: ({ event, context }) => {
|
||||||
if (event.type !== 'Create project') {
|
if (
|
||||||
|
event.type !== 'Create project' &&
|
||||||
|
event.type !== 'Import file from URL'
|
||||||
|
) {
|
||||||
return {
|
return {
|
||||||
name: '',
|
name: '',
|
||||||
projects: context.projects,
|
projects: context.projects,
|
||||||
@ -272,5 +296,39 @@ export const projectsMachine = setup({
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'Creating file': {
|
||||||
|
invoke: {
|
||||||
|
id: 'create-file',
|
||||||
|
src: 'createFile',
|
||||||
|
input: ({ event, context }) => {
|
||||||
|
if (event.type !== 'Import file from URL') {
|
||||||
|
return {
|
||||||
|
code: '',
|
||||||
|
name: '',
|
||||||
|
units: 'mm',
|
||||||
|
method: 'existingProject',
|
||||||
|
projects: context.projects,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
code: event.data.code || '',
|
||||||
|
name: event.data.name,
|
||||||
|
units: event.data.units,
|
||||||
|
method: event.data.method,
|
||||||
|
projectName: event.data.projectName,
|
||||||
|
projects: context.projects,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDone: {
|
||||||
|
target: 'Reading projects',
|
||||||
|
actions: ['navigateToFile', 'toastSuccess'],
|
||||||
|
},
|
||||||
|
onError: {
|
||||||
|
target: 'Reading projects',
|
||||||
|
actions: 'toastError',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
73
src/main.ts
@ -21,6 +21,7 @@ import minimist from 'minimist'
|
|||||||
import getCurrentProjectFile from 'lib/getCurrentProjectFile'
|
import getCurrentProjectFile from 'lib/getCurrentProjectFile'
|
||||||
import os from 'node:os'
|
import os from 'node:os'
|
||||||
import { reportRejection } from 'lib/trap'
|
import { reportRejection } from 'lib/trap'
|
||||||
|
import { ZOO_STUDIO_PROTOCOL } from 'lib/constants'
|
||||||
import argvFromYargs from './commandLineArgs'
|
import argvFromYargs from './commandLineArgs'
|
||||||
|
|
||||||
import * as packageJSON from '../package.json'
|
import * as packageJSON from '../package.json'
|
||||||
@ -48,9 +49,7 @@ process.env.VITE_KC_SITE_BASE_URL ??= 'https://zoo.dev'
|
|||||||
process.env.VITE_KC_SKIP_AUTH ??= 'false'
|
process.env.VITE_KC_SKIP_AUTH ??= 'false'
|
||||||
process.env.VITE_KC_CONNECTION_TIMEOUT_MS ??= '15000'
|
process.env.VITE_KC_CONNECTION_TIMEOUT_MS ??= '15000'
|
||||||
|
|
||||||
const ZOO_STUDIO_PROTOCOL = 'zoo-studio'
|
/// Register our application to handle all "zoo-studio:" protocols.
|
||||||
|
|
||||||
/// Register our application to handle all "electron-fiddle://" protocols.
|
|
||||||
if (process.defaultApp) {
|
if (process.defaultApp) {
|
||||||
if (process.argv.length >= 2) {
|
if (process.argv.length >= 2) {
|
||||||
app.setAsDefaultProtocolClient(ZOO_STUDIO_PROTOCOL, process.execPath, [
|
app.setAsDefaultProtocolClient(ZOO_STUDIO_PROTOCOL, process.execPath, [
|
||||||
@ -65,7 +64,7 @@ if (process.defaultApp) {
|
|||||||
// Must be done before ready event.
|
// Must be done before ready event.
|
||||||
registerStartupListeners()
|
registerStartupListeners()
|
||||||
|
|
||||||
const createWindow = (filePath?: string, reuse?: boolean): BrowserWindow => {
|
const createWindow = (pathToOpen?: string, reuse?: boolean): BrowserWindow => {
|
||||||
let newWindow
|
let newWindow
|
||||||
|
|
||||||
if (reuse) {
|
if (reuse) {
|
||||||
@ -90,32 +89,54 @@ const createWindow = (filePath?: string, reuse?: boolean): BrowserWindow => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pathIsCustomProtocolLink =
|
||||||
|
pathToOpen?.startsWith(ZOO_STUDIO_PROTOCOL) ?? false
|
||||||
|
|
||||||
// and load the index.html of the app.
|
// and load the index.html of the app.
|
||||||
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
|
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
|
||||||
newWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL).catch(reportRejection)
|
const filteredPath = pathToOpen
|
||||||
|
? decodeURI(pathToOpen.replace(ZOO_STUDIO_PROTOCOL, ''))
|
||||||
|
: ''
|
||||||
|
const fullHashBasedUrl = `${MAIN_WINDOW_VITE_DEV_SERVER_URL}/#/${filteredPath}`
|
||||||
|
newWindow.loadURL(fullHashBasedUrl).catch(reportRejection)
|
||||||
} else {
|
} else {
|
||||||
getProjectPathAtStartup(filePath)
|
if (pathIsCustomProtocolLink && pathToOpen) {
|
||||||
.then(async (projectPath) => {
|
// We're trying to open a custom protocol link
|
||||||
const startIndex = path.join(
|
const filteredPath = pathToOpen
|
||||||
__dirname,
|
? decodeURI(pathToOpen.replace(ZOO_STUDIO_PROTOCOL, ''))
|
||||||
`../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`
|
: ''
|
||||||
)
|
const startIndex = path.join(
|
||||||
|
__dirname,
|
||||||
if (projectPath === null) {
|
`../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`
|
||||||
await newWindow.loadFile(startIndex)
|
)
|
||||||
return
|
newWindow
|
||||||
}
|
.loadFile(startIndex, {
|
||||||
|
hash: filteredPath,
|
||||||
console.log('Loading file', projectPath)
|
|
||||||
|
|
||||||
const fullUrl = `/file/${encodeURIComponent(projectPath)}`
|
|
||||||
console.log('Full URL', fullUrl)
|
|
||||||
|
|
||||||
await newWindow.loadFile(startIndex, {
|
|
||||||
hash: fullUrl,
|
|
||||||
})
|
})
|
||||||
})
|
.catch(reportRejection)
|
||||||
.catch(reportRejection)
|
} else {
|
||||||
|
// otherwise we're trying to open a local file from the command line
|
||||||
|
getProjectPathAtStartup(pathToOpen)
|
||||||
|
.then(async (projectPath) => {
|
||||||
|
const startIndex = path.join(
|
||||||
|
__dirname,
|
||||||
|
`../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (projectPath === null) {
|
||||||
|
await newWindow.loadFile(startIndex)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullUrl = `/file/${encodeURIComponent(projectPath)}`
|
||||||
|
console.log('Full URL', fullUrl)
|
||||||
|
|
||||||
|
await newWindow.loadFile(startIndex, {
|
||||||
|
hash: fullUrl,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(reportRejection)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open the DevTools.
|
// Open the DevTools.
|
||||||
|
@ -24,16 +24,28 @@ import { markOnce } from 'lib/performance'
|
|||||||
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
||||||
import { useProjectsLoader } from 'hooks/useProjectsLoader'
|
import { useProjectsLoader } from 'hooks/useProjectsLoader'
|
||||||
import { useProjectsContext } from 'hooks/useProjectsContext'
|
import { useProjectsContext } from 'hooks/useProjectsContext'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
|
import { useCreateFileLinkQuery } from 'hooks/useCreateFileLinkQueryWatcher'
|
||||||
|
|
||||||
// This route only opens in the desktop context for now,
|
// This route only opens in the desktop context for now,
|
||||||
// as defined in Router.tsx, so we can use the desktop APIs and types.
|
// as defined in Router.tsx, so we can use the desktop APIs and types.
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
const { state, send } = useProjectsContext()
|
const { state, send } = useProjectsContext()
|
||||||
const { commandBarSend } = useCommandsContext()
|
|
||||||
const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0)
|
const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0)
|
||||||
const { projectsDir } = useProjectsLoader([projectsLoaderTrigger])
|
const { projectsDir } = useProjectsLoader([projectsLoaderTrigger])
|
||||||
|
|
||||||
|
// 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.HOME + 'SETTINGS')
|
useRefreshSettings(PATHS.HOME + 'SETTINGS')
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const {
|
const {
|
||||||
@ -128,7 +140,7 @@ const Home = () => {
|
|||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: {
|
data: {
|
||||||
groupId: 'projects',
|
groupId: 'projects',
|
||||||
|
7
src/wasm-lib/Cargo.lock
generated
@ -1382,12 +1382,6 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "iai"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "71a816c97c42258aa5834d07590b718b4c9a598944cd39a52dc25b351185d678"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iana-time-zone"
|
name = "iana-time-zone"
|
||||||
version = "0.1.61"
|
version = "0.1.61"
|
||||||
@ -1739,7 +1733,6 @@ dependencies = [
|
|||||||
"gltf-json",
|
"gltf-json",
|
||||||
"handlebars",
|
"handlebars",
|
||||||
"http 1.2.0",
|
"http 1.2.0",
|
||||||
"iai",
|
|
||||||
"image",
|
"image",
|
||||||
"indexmap 2.7.0",
|
"indexmap 2.7.0",
|
||||||
"insta",
|
"insta",
|
||||||
|
@ -113,7 +113,6 @@ base64 = "0.22.1"
|
|||||||
criterion = { version = "0.5.1", features = ["async_tokio"] }
|
criterion = { version = "0.5.1", features = ["async_tokio"] }
|
||||||
expectorate = "1.1.0"
|
expectorate = "1.1.0"
|
||||||
handlebars = "6.3.0"
|
handlebars = "6.3.0"
|
||||||
iai = "0.1"
|
|
||||||
image = { version = "0.25.5", default-features = false, features = ["png"] }
|
image = { version = "0.25.5", default-features = false, features = ["png"] }
|
||||||
insta = { version = "1.41.1", features = ["json", "filters", "redactions"] }
|
insta = { version = "1.41.1", features = ["json", "filters", "redactions"] }
|
||||||
itertools = "0.13.0"
|
itertools = "0.13.0"
|
||||||
@ -129,10 +128,6 @@ workspace = true
|
|||||||
name = "compiler_benchmark_criterion"
|
name = "compiler_benchmark_criterion"
|
||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
[[bench]]
|
|
||||||
name = "compiler_benchmark_iai"
|
|
||||||
harness = false
|
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "digest_benchmark"
|
name = "digest_benchmark"
|
||||||
harness = false
|
harness = false
|
||||||
@ -142,15 +137,7 @@ name = "lsp_semantic_tokens_benchmark_criterion"
|
|||||||
harness = false
|
harness = false
|
||||||
required-features = ["lsp-test-util"]
|
required-features = ["lsp-test-util"]
|
||||||
|
|
||||||
[[bench]]
|
|
||||||
name = "lsp_semantic_tokens_benchmark_iai"
|
|
||||||
harness = false
|
|
||||||
required-features = ["lsp-test-util"]
|
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "executor_benchmark_criterion"
|
name = "executor_benchmark_criterion"
|
||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
[[bench]]
|
|
||||||
name = "executor_benchmark_iai"
|
|
||||||
harness = false
|
|
||||||
|
@ -1,35 +0,0 @@
|
|||||||
use iai::black_box;
|
|
||||||
|
|
||||||
pub fn parse(program: &str) {
|
|
||||||
black_box(kcl_lib::Program::parse(program).unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_kitt() {
|
|
||||||
parse(KITT_PROGRAM)
|
|
||||||
}
|
|
||||||
fn parse_pipes() {
|
|
||||||
parse(PIPES_PROGRAM)
|
|
||||||
}
|
|
||||||
fn parse_cube() {
|
|
||||||
parse(CUBE_PROGRAM)
|
|
||||||
}
|
|
||||||
fn parse_math() {
|
|
||||||
parse(MATH_PROGRAM)
|
|
||||||
}
|
|
||||||
fn parse_lsystem() {
|
|
||||||
parse(LSYSTEM_PROGRAM)
|
|
||||||
}
|
|
||||||
|
|
||||||
iai::main! {
|
|
||||||
parse_kitt,
|
|
||||||
parse_pipes,
|
|
||||||
parse_cube,
|
|
||||||
parse_math,
|
|
||||||
parse_lsystem,
|
|
||||||
}
|
|
||||||
|
|
||||||
const KITT_PROGRAM: &str = include_str!("../../tests/executor/inputs/kittycad_svg.kcl");
|
|
||||||
const PIPES_PROGRAM: &str = include_str!("../../tests/executor/inputs/pipes_on_pipes.kcl");
|
|
||||||
const CUBE_PROGRAM: &str = include_str!("../../tests/executor/inputs/cube.kcl");
|
|
||||||
const MATH_PROGRAM: &str = include_str!("../../tests/executor/inputs/math.kcl");
|
|
||||||
const LSYSTEM_PROGRAM: &str = include_str!("../../tests/executor/inputs/lsystem.kcl");
|
|
@ -1,27 +0,0 @@
|
|||||||
use iai::black_box;
|
|
||||||
|
|
||||||
async fn execute_server_rack_heavy() {
|
|
||||||
let code = SERVER_RACK_HEAVY_PROGRAM;
|
|
||||||
black_box(
|
|
||||||
kcl_lib::test_server::execute_and_snapshot(code, kcl_lib::UnitLength::Mm, None)
|
|
||||||
.await
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn execute_server_rack_lite() {
|
|
||||||
let code = SERVER_RACK_LITE_PROGRAM;
|
|
||||||
black_box(
|
|
||||||
kcl_lib::test_server::execute_and_snapshot(code, kcl_lib::UnitLength::Mm, None)
|
|
||||||
.await
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
iai::main! {
|
|
||||||
execute_server_rack_lite,
|
|
||||||
execute_server_rack_heavy,
|
|
||||||
}
|
|
||||||
|
|
||||||
const SERVER_RACK_HEAVY_PROGRAM: &str = include_str!("../../tests/executor/inputs/server-rack-heavy.kcl");
|
|
||||||
const SERVER_RACK_LITE_PROGRAM: &str = include_str!("../../tests/executor/inputs/server-rack-lite.kcl");
|
|
@ -1,45 +0,0 @@
|
|||||||
use iai::black_box;
|
|
||||||
use kcl_lib::kcl_lsp_server;
|
|
||||||
use tower_lsp::LanguageServer;
|
|
||||||
|
|
||||||
async fn kcl_lsp_semantic_tokens(code: &str) {
|
|
||||||
let server = kcl_lsp_server(false).await.unwrap();
|
|
||||||
|
|
||||||
// Send open file.
|
|
||||||
server
|
|
||||||
.did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams {
|
|
||||||
text_document: tower_lsp::lsp_types::TextDocumentItem {
|
|
||||||
uri: "file:///test.kcl".try_into().unwrap(),
|
|
||||||
language_id: "kcl".to_string(),
|
|
||||||
version: 1,
|
|
||||||
text: code.to_string(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// Send semantic tokens request.
|
|
||||||
black_box(
|
|
||||||
server
|
|
||||||
.semantic_tokens_full(tower_lsp::lsp_types::SemanticTokensParams {
|
|
||||||
text_document: tower_lsp::lsp_types::TextDocumentIdentifier {
|
|
||||||
uri: "file:///test.kcl".try_into().unwrap(),
|
|
||||||
},
|
|
||||||
partial_result_params: Default::default(),
|
|
||||||
work_done_progress_params: Default::default(),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn semantic_tokens_global_tags() {
|
|
||||||
let code = GLOBAL_TAGS_FILE;
|
|
||||||
kcl_lsp_semantic_tokens(code).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
iai::main! {
|
|
||||||
semantic_tokens_global_tags,
|
|
||||||
}
|
|
||||||
|
|
||||||
const GLOBAL_TAGS_FILE: &str = include_str!("../../tests/executor/inputs/global-tags.kcl");
|
|
@ -370,8 +370,6 @@ impl From<KclError> for pyo3::PyErr {
|
|||||||
pub struct CompilationError {
|
pub struct CompilationError {
|
||||||
#[serde(rename = "sourceRange")]
|
#[serde(rename = "sourceRange")]
|
||||||
pub source_range: SourceRange,
|
pub source_range: SourceRange,
|
||||||
#[serde(rename = "contextRange")]
|
|
||||||
pub context_range: Option<SourceRange>,
|
|
||||||
pub message: String,
|
pub message: String,
|
||||||
pub suggestion: Option<Suggestion>,
|
pub suggestion: Option<Suggestion>,
|
||||||
pub severity: Severity,
|
pub severity: Severity,
|
||||||
@ -382,7 +380,6 @@ impl CompilationError {
|
|||||||
pub(crate) fn err(source_range: SourceRange, message: impl ToString) -> CompilationError {
|
pub(crate) fn err(source_range: SourceRange, message: impl ToString) -> CompilationError {
|
||||||
CompilationError {
|
CompilationError {
|
||||||
source_range,
|
source_range,
|
||||||
context_range: None,
|
|
||||||
message: message.to_string(),
|
message: message.to_string(),
|
||||||
suggestion: None,
|
suggestion: None,
|
||||||
severity: Severity::Error,
|
severity: Severity::Error,
|
||||||
@ -393,7 +390,6 @@ impl CompilationError {
|
|||||||
pub(crate) fn fatal(source_range: SourceRange, message: impl ToString) -> CompilationError {
|
pub(crate) fn fatal(source_range: SourceRange, message: impl ToString) -> CompilationError {
|
||||||
CompilationError {
|
CompilationError {
|
||||||
source_range,
|
source_range,
|
||||||
context_range: None,
|
|
||||||
message: message.to_string(),
|
message: message.to_string(),
|
||||||
suggestion: None,
|
suggestion: None,
|
||||||
severity: Severity::Fatal,
|
severity: Severity::Fatal,
|
||||||
@ -402,22 +398,18 @@ impl CompilationError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn with_suggestion(
|
pub(crate) fn with_suggestion(
|
||||||
source_range: SourceRange,
|
self,
|
||||||
context_range: Option<SourceRange>,
|
suggestion_title: impl ToString,
|
||||||
message: impl ToString,
|
suggestion_insert: impl ToString,
|
||||||
suggestion: Option<(impl ToString, impl ToString)>,
|
|
||||||
tag: Tag,
|
tag: Tag,
|
||||||
) -> CompilationError {
|
) -> CompilationError {
|
||||||
CompilationError {
|
CompilationError {
|
||||||
source_range,
|
suggestion: Some(Suggestion {
|
||||||
context_range,
|
title: suggestion_title.to_string(),
|
||||||
message: message.to_string(),
|
insert: suggestion_insert.to_string(),
|
||||||
suggestion: suggestion.map(|(t, i)| Suggestion {
|
|
||||||
title: t.to_string(),
|
|
||||||
insert: i.to_string(),
|
|
||||||
}),
|
}),
|
||||||
severity: Severity::Error,
|
|
||||||
tag,
|
tag,
|
||||||
|
..self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|