Compare commits
126 Commits
nightly-v2
...
pierremtb/
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c78dbd4c8 | |||
| 059593372a | |||
| 1ba8c5af00 | |||
| 410b4e81eb | |||
| 30275d86cc | |||
| 39c40b2cde | |||
| 10789d9c3c | |||
| 67cc4f5835 | |||
| 2692f2b73a | |||
| 965cb18059 | |||
| a022b8ef6c | |||
| 907102a8fa | |||
| 4d24bf7c94 | |||
| 9a537da183 | |||
| 353eca110e | |||
| df81b76b8b | |||
| ac3f7ab712 | |||
| dac91d3b79 | |||
| 0698432abf | |||
| fb56820811 | |||
| fb37bb83a8 | |||
| f90811695d | |||
| 0592d3b5da | |||
| 31e4d60045 | |||
| 5c1dfe0c8e | |||
| f06873a0e2 | |||
| 09025179f9 | |||
| 521a593451 | |||
| 87c4e6c74e | |||
| 82cd106898 | |||
| e14cc4ace3 | |||
| 2a2a31d0ef | |||
| f2669223c5 | |||
| c3bc1fad6d | |||
| 96ff1dd55b | |||
| 82bd04631a | |||
| abec2d6d66 | |||
| 6089b1932a | |||
| 074fd2b5c7 | |||
| b2485b804c | |||
| e753082653 | |||
| 634745bb81 | |||
| e3660c75fc | |||
| ef61d10615 | |||
| c208e16c76 | |||
| 585ca7e80f | |||
| f7bae1d221 | |||
| 339de00e68 | |||
| 4f02e45da3 | |||
| 1908383f0e | |||
| 68204bb23d | |||
| 5438a987ab | |||
| fa3f934948 | |||
| 08e714080e | |||
| df01c233e4 | |||
| b30a37a0b3 | |||
| 82aefec34d | |||
| 679b65f643 | |||
| d64270d494 | |||
| c06b2b4029 | |||
| 8b8a2bc4e2 | |||
| af702ae1b2 | |||
| 83e72dafa3 | |||
| e417e60053 | |||
| ebc6b6460d | |||
| 91f0cfe467 | |||
| a2ff0aeceb | |||
| f05acf92cc | |||
| 670faac1e8 | |||
| ca09224c92 | |||
| 5cbd11cec8 | |||
| 28eb99f655 | |||
| c29be6e341 | |||
| 2193d563c5 | |||
| 570d159c29 | |||
| 713886b274 | |||
| 2aa4a01cb7 | |||
| 2048c26b9f | |||
| cbb8df5904 | |||
| bb67a9e9cf | |||
| b84d5951b7 | |||
| 1e5954e5ed | |||
| d58a147b7d | |||
| 96b06247a4 | |||
| 36d49b1bcb | |||
| 4748c2d1e0 | |||
| 698ce671df | |||
| a2330a0dbc | |||
| c882e34ea9 | |||
| 1ce3d8ccd0 | |||
| 15bedd56f4 | |||
| 746ebf80d1 | |||
| 02b249bd31 | |||
| 524fcb03ad | |||
| 3a9e0c72a8 | |||
| 5dc983ad7b | |||
| 81411033d7 | |||
| 30a24c8ae6 | |||
| 403cee5f16 | |||
| 14eeafb70a | |||
| f4ecd16ffa | |||
| 48380be480 | |||
| 80e32b337f | |||
| 9378d9862b | |||
| 1f515b712b | |||
| 372f2eebcc | |||
| e22a9edde8 | |||
| 75e3f843eb | |||
| f0136a5939 | |||
| 3d2e48732c | |||
| 7545b61b49 | |||
| d1be6d7b64 | |||
| 8ab24ceee7 | |||
| f163870b86 | |||
| 3fc707a2a4 | |||
| 238163d7db | |||
| bfccb79c1c | |||
| fe6d1f8119 | |||
| f496d94258 | |||
| 5d8f3f988a | |||
| 4f06524776 | |||
| d7fe827a9e | |||
| 049e487ac4 | |||
| 5bd89047b2 | |||
| 5822321f35 | |||
| 401dcf8152 |
@ -1,3 +1,3 @@
|
||||
[codespell]
|
||||
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,atleast,ue,afterall
|
||||
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,atleast,ue,afterall,ser
|
||||
skip: **/target,node_modules,build,dist,./out,**/Cargo.lock,./docs/kcl/*.md,.yarn.lock,**/yarn.lock,./openapi/*.json,./packages/codemirror-lang-kcl/test/all.test.ts,tsconfig.tsbuildinfo
|
||||
|
||||
1
.github/workflows/build-apps.yml
vendored
1
.github/workflows/build-apps.yml
vendored
@ -5,6 +5,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- pierremtb/4088/create-file-url
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
schedule:
|
||||
|
||||
44
.github/workflows/cargo-bench.yml
vendored
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}}
|
||||
|
||||
20587
docs/kcl/std.json
20587
docs/kcl/std.json
File diff suppressed because it is too large
Load Diff
28
docs/kcl/types/Face.md
Normal file
28
docs/kcl/types/Face.md
Normal file
@ -0,0 +1,28 @@
|
||||
---
|
||||
title: "Face"
|
||||
excerpt: "A face."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
A face.
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `id` |`string`| The id of the face. | No |
|
||||
| `value` |`string`| The tag of the face. | No |
|
||||
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face’s X axis be? | No |
|
||||
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face’s Y axis be? | No |
|
||||
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
|
||||
| `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No |
|
||||
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A face. | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||
|
||||
|
||||
@ -20,6 +20,7 @@ A helix.
|
||||
| `revolutions` |`number`| Number of revolutions. | No |
|
||||
| `angleStart` |`number`| Start angle (in degrees). | No |
|
||||
| `ccw` |`boolean`| Is the helix rotation counter clockwise? | No |
|
||||
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A helix. | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||
|
||||
|
||||
|
||||
@ -20,6 +20,7 @@ A helix.
|
||||
| `revolutions` |`number`| Number of revolutions. | No |
|
||||
| `angleStart` |`number`| Start angle (in degrees). | No |
|
||||
| `ccw` |`boolean`| Is the helix rotation counter clockwise? | No |
|
||||
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A helix. | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||
|
||||
|
||||
|
||||
@ -168,7 +168,6 @@ Any KCL value.
|
||||
|
||||
|
||||
----
|
||||
A plane.
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
@ -181,17 +180,10 @@ A plane.
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: [`Plane`](/docs/kcl/types/Plane)| | No |
|
||||
| `id` |`string`| The id of the plane. | No |
|
||||
| `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| Any KCL value. | No |
|
||||
| `origin` |[`Point3d`](/docs/kcl/types/Point3d)| Origin of the plane. | No |
|
||||
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s X axis be? | No |
|
||||
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s Y axis be? | No |
|
||||
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||
| `value` |[`Plane`](/docs/kcl/types/Plane)| Any KCL value. | No |
|
||||
|
||||
|
||||
----
|
||||
A face.
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
@ -203,14 +195,8 @@ A face.
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `Face`| | No |
|
||||
| `id` |`string`| The id of the face. | No |
|
||||
| `value` |`string`| The tag of the face. | No |
|
||||
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face’s X axis be? | No |
|
||||
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face’s Y axis be? | No |
|
||||
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
|
||||
| `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||
| `type` |enum: [`Face`](/docs/kcl/types/Face)| | No |
|
||||
| `value` |[`Face`](/docs/kcl/types/Face)| Any KCL value. | No |
|
||||
|
||||
|
||||
----
|
||||
@ -246,7 +232,6 @@ A face.
|
||||
|
||||
|
||||
----
|
||||
An solid is a collection of extrude surfaces.
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
@ -259,14 +244,7 @@ An solid is a collection of extrude surfaces.
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: [`Solid`](/docs/kcl/types/Solid)| | No |
|
||||
| `id` |`string`| The id of the solid. | No |
|
||||
| `value` |`[` [`ExtrudeSurface`](/docs/kcl/types/ExtrudeSurface) `]`| The extrude surfaces. | No |
|
||||
| `sketch` |[`Sketch`](/docs/kcl/types/Sketch)| The sketch. | No |
|
||||
| `height` |`number`| The height of the solid. | No |
|
||||
| `startCapId` |`string`| The id of the extrusion start cap | No |
|
||||
| `endCapId` |`string`| The id of the extrusion end cap | No |
|
||||
| `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |
|
||||
| `value` |[`Solid`](/docs/kcl/types/Solid)| Any KCL value. | No |
|
||||
|
||||
|
||||
----
|
||||
@ -286,7 +264,6 @@ An solid is a collection of extrude surfaces.
|
||||
|
||||
|
||||
----
|
||||
A helix.
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
@ -299,11 +276,7 @@ A helix.
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: [`Helix`](/docs/kcl/types/Helix)| | No |
|
||||
| `value` |`string`| The id of the helix. | No |
|
||||
| `revolutions` |`number`| Number of revolutions. | No |
|
||||
| `angleStart` |`number`| Start angle (in degrees). | No |
|
||||
| `ccw` |`boolean`| Is the helix rotation counter clockwise? | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||
| `value` |[`Helix`](/docs/kcl/types/Helix)| Any KCL value. | No |
|
||||
|
||||
|
||||
----
|
||||
|
||||
@ -22,6 +22,7 @@ A plane.
|
||||
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s X axis be? | No |
|
||||
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s Y axis be? | No |
|
||||
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
|
||||
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A plane. | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||
|
||||
|
||||
|
||||
@ -21,6 +21,7 @@ A sketch is a collection of paths.
|
||||
| `on` |[`SketchSurface`](/docs/kcl/types/SketchSurface)| What the sketch is on (can be a plane or a face). | No |
|
||||
| `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No |
|
||||
| `tags` |`object`| Tag identifiers that have been declared in this sketch. | No |
|
||||
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch is a collection of paths. | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |
|
||||
|
||||
|
||||
|
||||
@ -30,6 +30,7 @@ A sketch is a collection of paths.
|
||||
| `on` |[`SketchSurface`](/docs/kcl/types/SketchSurface)| What the sketch is on (can be a plane or a face). | No |
|
||||
| `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No |
|
||||
| `tags` |`object`| Tag identifiers that have been declared in this sketch. | No |
|
||||
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch or a group of sketches. | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |
|
||||
|
||||
|
||||
|
||||
@ -31,6 +31,7 @@ A plane.
|
||||
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s X axis be? | No |
|
||||
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s Y axis be? | No |
|
||||
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
|
||||
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch type. | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||
|
||||
|
||||
@ -54,6 +55,7 @@ A face.
|
||||
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face’s Y axis be? | No |
|
||||
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
|
||||
| `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No |
|
||||
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch type. | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||
|
||||
|
||||
|
||||
@ -23,6 +23,7 @@ An solid is a collection of extrude surfaces.
|
||||
| `startCapId` |`string`| The id of the extrusion start cap | No |
|
||||
| `endCapId` |`string`| The id of the extrusion end cap | No |
|
||||
| `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No |
|
||||
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| An solid is a collection of extrude surfaces. | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |
|
||||
|
||||
|
||||
|
||||
@ -32,6 +32,7 @@ An solid is a collection of extrude surfaces.
|
||||
| `startCapId` |`string`| The id of the extrusion start cap | No |
|
||||
| `endCapId` |`string`| The id of the extrusion end cap | No |
|
||||
| `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No |
|
||||
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A solid or a group of solids. | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |
|
||||
|
||||
|
||||
|
||||
107
docs/kcl/types/UnitLen.md
Normal file
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 |
|
||||
|
||||
|
||||
----
|
||||
|
||||
|
||||
|
||||
|
||||
@ -280,7 +280,7 @@ test(
|
||||
|
||||
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
|
||||
await expect(page.getByText('router-template-slate')).toBeVisible()
|
||||
await expect(page.getByText('New Project')).toBeVisible()
|
||||
await expect(page.getByText('Create project')).toBeVisible()
|
||||
})
|
||||
|
||||
await test.step('Opening the router-template project should load', async () => {
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { test, expect } from './zoo-test'
|
||||
|
||||
import { getUtils } from './test-utils'
|
||||
import * as fsp from 'fs/promises'
|
||||
import { executorInputPath, getUtils } from './test-utils'
|
||||
import { KCL_DEFAULT_LENGTH } from 'lib/constants'
|
||||
import path from 'path'
|
||||
|
||||
test.describe('Command bar tests', () => {
|
||||
test('Extrude from command bar selects extrude line after', async ({
|
||||
@ -45,46 +46,6 @@ test.describe('Command bar tests', () => {
|
||||
)
|
||||
})
|
||||
|
||||
// TODO: fix this test after the electron migration
|
||||
test.fixme('Fillet from command bar', async ({ page, homePage }) => {
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-5, -5], %)
|
||||
|> line([0, 10], %)
|
||||
|> line([10, 0], %)
|
||||
|> line([0, -10], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
extrude001 = extrude(-10, sketch001)`
|
||||
)
|
||||
})
|
||||
|
||||
const u = await getUtils(page)
|
||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||
await homePage.goToModelingScene()
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.closeDebugPanel()
|
||||
|
||||
const selectSegment = () => page.getByText(`line([0, -10], %)`).click()
|
||||
|
||||
await selectSegment()
|
||||
await page.waitForTimeout(100)
|
||||
await page.getByRole('button', { name: 'Fillet' }).click()
|
||||
await page.waitForTimeout(100)
|
||||
await page.keyboard.press('Enter') // skip selection
|
||||
await page.waitForTimeout(100)
|
||||
await page.keyboard.press('Enter') // accept default radius
|
||||
await page.waitForTimeout(100)
|
||||
await page.keyboard.press('Enter') // submit
|
||||
await page.waitForTimeout(100)
|
||||
await expect(page.locator('.cm-activeLine')).toContainText(
|
||||
`fillet({ radius = ${KCL_DEFAULT_LENGTH}, tags = [seg01] }, %)`
|
||||
)
|
||||
})
|
||||
|
||||
test('Command bar can change a setting, and switch back and forth between arguments', async ({
|
||||
page,
|
||||
homePage,
|
||||
@ -345,4 +306,132 @@ test.describe('Command bar tests', () => {
|
||||
await arcToolCommand.click()
|
||||
await expect(arcToolButton).toHaveAttribute('aria-pressed', 'true')
|
||||
})
|
||||
|
||||
test(`Reacts to query param to open "import from URL" command`, async ({
|
||||
page,
|
||||
cmdBar,
|
||||
editor,
|
||||
homePage,
|
||||
}) => {
|
||||
await test.step(`Prepare and navigate to home page with query params`, async () => {
|
||||
const targetURL = `?create-file&name=test&units=mm&code=ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg%3D%3D&ask-open-desktop`
|
||||
await homePage.expectState({
|
||||
projectCards: [],
|
||||
sortBy: 'last-modified-desc',
|
||||
})
|
||||
await page.goto(page.url() + targetURL)
|
||||
expect(page.url()).toContain(targetURL)
|
||||
})
|
||||
|
||||
await test.step(`Submit the command`, async () => {
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
commandName: 'Import file from URL',
|
||||
currentArgKey: 'method',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Method: '',
|
||||
Name: 'test',
|
||||
Code: '1 line',
|
||||
},
|
||||
highlightedHeaderArg: 'method',
|
||||
})
|
||||
await cmdBar.selectOption({ name: 'New Project' }).click()
|
||||
await cmdBar.expectState({
|
||||
stage: 'review',
|
||||
commandName: 'Import file from URL',
|
||||
headerArguments: {
|
||||
Method: 'New project',
|
||||
Name: 'test',
|
||||
Code: '1 line',
|
||||
},
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
})
|
||||
|
||||
await test.step(`Ensure we created the project and are in the modeling scene`, async () => {
|
||||
await editor.expectEditor.toContain('extrusionDistance = 12')
|
||||
})
|
||||
})
|
||||
|
||||
test(`"import from URL" can add to existing project`, async ({
|
||||
page,
|
||||
cmdBar,
|
||||
editor,
|
||||
homePage,
|
||||
toolbar,
|
||||
context,
|
||||
}) => {
|
||||
await context.folderSetupFn(async (dir) => {
|
||||
const testProjectDir = path.join(dir, 'testProjectDir')
|
||||
await Promise.all([fsp.mkdir(testProjectDir, { recursive: true })])
|
||||
await Promise.all([
|
||||
fsp.copyFile(
|
||||
executorInputPath('cylinder.kcl'),
|
||||
path.join(testProjectDir, 'main.kcl')
|
||||
),
|
||||
])
|
||||
})
|
||||
await test.step(`Prepare and navigate to home page with query params`, async () => {
|
||||
const targetURL = `?create-file&name=test&units=mm&code=ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg%3D%3D&ask-open-desktop`
|
||||
await homePage.expectState({
|
||||
projectCards: [
|
||||
{
|
||||
fileCount: 1,
|
||||
title: 'testProjectDir',
|
||||
},
|
||||
],
|
||||
sortBy: 'last-modified-desc',
|
||||
})
|
||||
await page.goto(page.url() + targetURL)
|
||||
expect(page.url()).toContain(targetURL)
|
||||
})
|
||||
|
||||
await test.step(`Submit the command`, async () => {
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
commandName: 'Import file from URL',
|
||||
currentArgKey: 'method',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Method: '',
|
||||
Name: 'test',
|
||||
Code: '1 line',
|
||||
},
|
||||
highlightedHeaderArg: 'method',
|
||||
})
|
||||
await cmdBar.selectOption({ name: 'Existing Project' }).click()
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
commandName: 'Import file from URL',
|
||||
currentArgKey: 'projectName',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Method: 'Existing project',
|
||||
Name: 'test',
|
||||
ProjectName: '',
|
||||
Code: '1 line',
|
||||
},
|
||||
highlightedHeaderArg: 'projectName',
|
||||
})
|
||||
await cmdBar.selectOption({ name: 'testProjectDir' }).click()
|
||||
await cmdBar.expectState({
|
||||
stage: 'review',
|
||||
commandName: 'Import file from URL',
|
||||
headerArguments: {
|
||||
Method: 'Existing project',
|
||||
ProjectName: 'testProjectDir',
|
||||
Name: 'test',
|
||||
Code: '1 line',
|
||||
},
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
})
|
||||
|
||||
await test.step(`Ensure we created the project and are in the modeling scene`, async () => {
|
||||
await editor.expectEditor.toContain('extrusionDistance = 12')
|
||||
await toolbar.openPane('files')
|
||||
await toolbar.expectFileTreeState(['main.kcl', 'test.kcl'])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -135,4 +135,27 @@ export class CmdBarFixture {
|
||||
await promptEditCommand.first().click()
|
||||
}
|
||||
}
|
||||
|
||||
get cmdSearchInput() {
|
||||
return this.page.getByTestId('cmd-bar-search')
|
||||
}
|
||||
|
||||
get argumentInput() {
|
||||
return this.page.getByTestId('cmd-bar-arg-value')
|
||||
}
|
||||
|
||||
get cmdOptions() {
|
||||
return this.page.getByTestId('cmd-bar-option')
|
||||
}
|
||||
|
||||
chooseCommand = async (commandName: string) => {
|
||||
await this.cmdOptions.getByText(commandName).click()
|
||||
}
|
||||
|
||||
/**
|
||||
* Select an option from the command bar
|
||||
*/
|
||||
selectOption = (options: Parameters<typeof this.page.getByRole>[1]) => {
|
||||
return this.page.getByRole('option', options)
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,7 +103,7 @@ export class HomePageFixture {
|
||||
.toEqual(expectedState)
|
||||
}
|
||||
|
||||
createAndGoToProject = async (projectTitle: string) => {
|
||||
createAndGoToProject = async (projectTitle = 'project-$nnn') => {
|
||||
await expect(this.projectSection).not.toHaveText('Loading your Projects...')
|
||||
await this.projectButtonNew.click()
|
||||
await this.projectTextName.click()
|
||||
|
||||
@ -15,6 +15,7 @@ export class ToolbarFixture {
|
||||
extrudeButton!: Locator
|
||||
loftButton!: Locator
|
||||
sweepButton!: Locator
|
||||
filletButton!: Locator
|
||||
chamferButton!: Locator
|
||||
shellButton!: Locator
|
||||
offsetPlaneButton!: Locator
|
||||
@ -43,6 +44,7 @@ export class ToolbarFixture {
|
||||
this.extrudeButton = page.getByTestId('extrude')
|
||||
this.loftButton = page.getByTestId('loft')
|
||||
this.sweepButton = page.getByTestId('sweep')
|
||||
this.filletButton = page.getByTestId('fillet3d')
|
||||
this.chamferButton = page.getByTestId('chamfer3d')
|
||||
this.shellButton = page.getByTestId('shell')
|
||||
this.offsetPlaneButton = page.getByTestId('plane-offset')
|
||||
@ -61,6 +63,10 @@ export class ToolbarFixture {
|
||||
this.exeIndicator = page.getByTestId('model-state-indicator-execution-done')
|
||||
}
|
||||
|
||||
get logoLink() {
|
||||
return this.page.getByTestId('app-logo')
|
||||
}
|
||||
|
||||
startSketchPlaneSelection = async () =>
|
||||
doAndWaitForImageDiff(this.page, () => this.startSketchBtn.click(), 500)
|
||||
|
||||
|
||||
@ -1020,6 +1020,221 @@ sketch002 = startSketchOn('XZ')
|
||||
})
|
||||
})
|
||||
|
||||
test(`Fillet point-and-click`, async ({
|
||||
context,
|
||||
page,
|
||||
homePage,
|
||||
scene,
|
||||
editor,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
// Code samples
|
||||
const initialCode = `sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-12, -6], %)
|
||||
|> line([0, 12], %)
|
||||
|> line([24, 0], %)
|
||||
|> line([0, -12], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
extrude001 = extrude(-12, sketch001)
|
||||
`
|
||||
const firstFilletDeclaration = 'fillet({ radius = 5, tags = [seg01] }, %)'
|
||||
const secondFilletDeclaration =
|
||||
'fillet({ radius = 5, tags = [getOppositeEdge(seg01)] }, %)'
|
||||
|
||||
// Locators
|
||||
const firstEdgeLocation = { x: 600, y: 193 }
|
||||
const secondEdgeLocation = { x: 600, y: 383 }
|
||||
const bodyLocation = { x: 630, y: 290 }
|
||||
const [clickOnFirstEdge] = scene.makeMouseHelpers(
|
||||
firstEdgeLocation.x,
|
||||
firstEdgeLocation.y
|
||||
)
|
||||
const [clickOnSecondEdge] = scene.makeMouseHelpers(
|
||||
secondEdgeLocation.x,
|
||||
secondEdgeLocation.y
|
||||
)
|
||||
|
||||
// Colors
|
||||
const edgeColorWhite: [number, number, number] = [248, 248, 248]
|
||||
const edgeColorYellow: [number, number, number] = [251, 251, 40] // Mac:B=67 Ubuntu:B=12
|
||||
const bodyColor: [number, number, number] = [155, 155, 155]
|
||||
const filletColor: [number, number, number] = [127, 127, 127]
|
||||
const backgroundColor: [number, number, number] = [30, 30, 30]
|
||||
const lowTolerance = 20
|
||||
const highTolerance = 40
|
||||
|
||||
// Setup
|
||||
await test.step(`Initial test setup`, async () => {
|
||||
await context.addInitScript((initialCode) => {
|
||||
localStorage.setItem('persistCode', initialCode)
|
||||
}, initialCode)
|
||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||
await homePage.goToModelingScene()
|
||||
|
||||
// verify modeling scene is loaded
|
||||
await scene.expectPixelColor(
|
||||
backgroundColor,
|
||||
secondEdgeLocation,
|
||||
lowTolerance
|
||||
)
|
||||
|
||||
// wait for stream to load
|
||||
await scene.expectPixelColor(bodyColor, bodyLocation, highTolerance)
|
||||
})
|
||||
|
||||
// Test 1: Command bar flow with preselected edges
|
||||
await test.step(`Select first edge`, async () => {
|
||||
await scene.expectPixelColor(
|
||||
edgeColorWhite,
|
||||
firstEdgeLocation,
|
||||
lowTolerance
|
||||
)
|
||||
await clickOnFirstEdge()
|
||||
await scene.expectPixelColor(
|
||||
edgeColorYellow,
|
||||
firstEdgeLocation,
|
||||
highTolerance // Ubuntu color mismatch can require high tolerance
|
||||
)
|
||||
})
|
||||
|
||||
await test.step(`Apply fillet to the preselected edge`, async () => {
|
||||
await page.waitForTimeout(100)
|
||||
await toolbar.filletButton.click()
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Fillet',
|
||||
highlightedHeaderArg: 'selection',
|
||||
currentArgKey: 'selection',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Selection: '',
|
||||
Radius: '',
|
||||
},
|
||||
stage: 'arguments',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Fillet',
|
||||
highlightedHeaderArg: 'radius',
|
||||
currentArgKey: 'radius',
|
||||
currentArgValue: '5',
|
||||
headerArguments: {
|
||||
Selection: '1 face',
|
||||
Radius: '',
|
||||
},
|
||||
stage: 'arguments',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Fillet',
|
||||
headerArguments: {
|
||||
Selection: '1 face',
|
||||
Radius: '5',
|
||||
},
|
||||
stage: 'review',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
})
|
||||
|
||||
await test.step(`Confirm code is added to the editor`, async () => {
|
||||
await editor.expectEditor.toContain(firstFilletDeclaration)
|
||||
await editor.expectState({
|
||||
diagnostics: [],
|
||||
activeLines: ['|>fillet({radius=5,tags=[seg01]},%)'],
|
||||
highlightedCode: '',
|
||||
})
|
||||
})
|
||||
|
||||
await test.step(`Confirm scene has changed`, async () => {
|
||||
await scene.expectPixelColor(filletColor, firstEdgeLocation, lowTolerance)
|
||||
})
|
||||
|
||||
// Test 2: Command bar flow without preselected edges
|
||||
await test.step(`Open fillet UI without selecting edges`, async () => {
|
||||
await page.waitForTimeout(100)
|
||||
await toolbar.filletButton.click()
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
currentArgKey: 'selection',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Selection: '',
|
||||
Radius: '',
|
||||
},
|
||||
highlightedHeaderArg: 'selection',
|
||||
commandName: 'Fillet',
|
||||
})
|
||||
})
|
||||
|
||||
await test.step(`Select second edge`, async () => {
|
||||
await scene.expectPixelColor(
|
||||
edgeColorWhite,
|
||||
secondEdgeLocation,
|
||||
lowTolerance
|
||||
)
|
||||
await clickOnSecondEdge()
|
||||
await scene.expectPixelColor(
|
||||
edgeColorYellow,
|
||||
secondEdgeLocation,
|
||||
highTolerance // Ubuntu color mismatch can require high tolerance
|
||||
)
|
||||
})
|
||||
|
||||
await test.step(`Apply fillet to the second edge`, async () => {
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Fillet',
|
||||
highlightedHeaderArg: 'selection',
|
||||
currentArgKey: 'selection',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Selection: '',
|
||||
Radius: '',
|
||||
},
|
||||
stage: 'arguments',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Fillet',
|
||||
highlightedHeaderArg: 'radius',
|
||||
currentArgKey: 'radius',
|
||||
currentArgValue: '5',
|
||||
headerArguments: {
|
||||
Selection: '1 sweepEdge',
|
||||
Radius: '',
|
||||
},
|
||||
stage: 'arguments',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Fillet',
|
||||
headerArguments: {
|
||||
Selection: '1 sweepEdge',
|
||||
Radius: '5',
|
||||
},
|
||||
stage: 'review',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
})
|
||||
|
||||
await test.step(`Confirm code is added to the editor`, async () => {
|
||||
await editor.expectEditor.toContain(secondFilletDeclaration)
|
||||
await editor.expectState({
|
||||
diagnostics: [],
|
||||
activeLines: ['radius=5,'],
|
||||
highlightedCode: '',
|
||||
})
|
||||
})
|
||||
|
||||
await test.step(`Confirm scene has changed`, async () => {
|
||||
await scene.expectPixelColor(
|
||||
backgroundColor,
|
||||
secondEdgeLocation,
|
||||
lowTolerance
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test(`Chamfer point-and-click`, async ({
|
||||
context,
|
||||
page,
|
||||
@ -1029,9 +1244,6 @@ test(`Chamfer point-and-click`, async ({
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
// TODO: fix this test on windows after the electron migration
|
||||
test.skip(process.platform === 'win32', 'Skip on windows')
|
||||
|
||||
// Code samples
|
||||
const initialCode = `sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-12, -6], %)
|
||||
@ -1069,13 +1281,13 @@ extrude001 = extrude(-12, sketch001)
|
||||
const highTolerance = 40
|
||||
|
||||
// Setup
|
||||
await context.addInitScript((initialCode) => {
|
||||
localStorage.setItem('persistCode', initialCode)
|
||||
}, initialCode)
|
||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||
await homePage.goToModelingScene()
|
||||
await test.step(`Initial test setup`, async () => {
|
||||
await context.addInitScript((initialCode) => {
|
||||
localStorage.setItem('persistCode', initialCode)
|
||||
}, initialCode)
|
||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||
await homePage.goToModelingScene()
|
||||
|
||||
await test.step(`Verify scene is loaded`, async () => {
|
||||
// verify modeling scene is loaded
|
||||
await scene.expectPixelColor(
|
||||
backgroundColor,
|
||||
@ -1103,6 +1315,7 @@ extrude001 = extrude(-12, sketch001)
|
||||
})
|
||||
|
||||
await test.step(`Apply chamfer to the preselected edge`, async () => {
|
||||
await page.waitForTimeout(100)
|
||||
await toolbar.chamferButton.click()
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Chamfer',
|
||||
@ -1154,6 +1367,7 @@ extrude001 = extrude(-12, sketch001)
|
||||
|
||||
// Test 2: Command bar flow without preselected edges
|
||||
await test.step(`Open chamfer UI without selecting edges`, async () => {
|
||||
await page.waitForTimeout(100)
|
||||
await toolbar.chamferButton.click()
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
@ -1289,6 +1503,7 @@ shellPointAndClickCapCases.forEach(({ shouldPreselect }) => {
|
||||
await clickOnCap()
|
||||
await page.waitForTimeout(500)
|
||||
await cmdBar.progressCmdBar()
|
||||
await page.waitForTimeout(500)
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
stage: 'review',
|
||||
@ -1309,6 +1524,7 @@ shellPointAndClickCapCases.forEach(({ shouldPreselect }) => {
|
||||
await test.step(`Go through the command bar flow with a preselected face (cap)`, async () => {
|
||||
await toolbar.shellButton.click()
|
||||
await cmdBar.progressCmdBar()
|
||||
await page.waitForTimeout(500)
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
stage: 'review',
|
||||
@ -1390,6 +1606,7 @@ extrude001 = extrude(40, sketch001)
|
||||
await page.waitForTimeout(500)
|
||||
await page.keyboard.up('Shift')
|
||||
await cmdBar.progressCmdBar()
|
||||
await page.waitForTimeout(500)
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
stage: 'review',
|
||||
@ -1513,3 +1730,61 @@ shellSketchOnFacesCases.forEach((initialCode, index) => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test(`Shell dry-run validation rejects sweeps`, async ({
|
||||
context,
|
||||
page,
|
||||
homePage,
|
||||
scene,
|
||||
editor,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
const initialCode = `sketch001 = startSketchOn('YZ')
|
||||
|> circle({
|
||||
center = [0, 0],
|
||||
radius = 500
|
||||
}, %)
|
||||
sketch002 = startSketchOn('XZ')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> xLine(-2000, %)
|
||||
sweep001 = sweep({ path = sketch002 }, sketch001)
|
||||
`
|
||||
await context.addInitScript((initialCode) => {
|
||||
localStorage.setItem('persistCode', initialCode)
|
||||
}, initialCode)
|
||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||
await homePage.goToModelingScene()
|
||||
await scene.waitForExecutionDone()
|
||||
|
||||
// One dumb hardcoded screen pixel value
|
||||
const testPoint = { x: 500, y: 250 }
|
||||
const [clickOnSweep] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
|
||||
|
||||
await test.step(`Confirm sweep exists`, async () => {
|
||||
await toolbar.closePane('code')
|
||||
await scene.expectPixelColor([231, 231, 231], testPoint, 15)
|
||||
})
|
||||
|
||||
await test.step(`Go through the Shell flow and fail validation with a toast`, async () => {
|
||||
await toolbar.shellButton.click()
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
currentArgKey: 'selection',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Selection: '',
|
||||
Thickness: '',
|
||||
},
|
||||
highlightedHeaderArg: 'selection',
|
||||
commandName: 'Shell',
|
||||
})
|
||||
await clickOnSweep()
|
||||
await page.waitForTimeout(500)
|
||||
await cmdBar.progressCmdBar()
|
||||
await expect(
|
||||
page.getByText('Unable to shell with the provided selection')
|
||||
).toBeVisible()
|
||||
await page.waitForTimeout(1000)
|
||||
})
|
||||
})
|
||||
|
||||
@ -172,7 +172,7 @@ test(
|
||||
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
|
||||
await expect(page.getByText('broken-code')).toBeVisible()
|
||||
await expect(page.getByText('bracket')).toBeVisible()
|
||||
await expect(page.getByText('New Project')).toBeVisible()
|
||||
await expect(page.getByText('Create project')).toBeVisible()
|
||||
})
|
||||
await test.step('opening broken code project should clear the scene and show the error', async () => {
|
||||
// Go back home.
|
||||
@ -253,7 +253,7 @@ test(
|
||||
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
|
||||
await expect(page.getByText('empty')).toBeVisible()
|
||||
await expect(page.getByText('bracket')).toBeVisible()
|
||||
await expect(page.getByText('New Project')).toBeVisible()
|
||||
await expect(page.getByText('Create project')).toBeVisible()
|
||||
})
|
||||
await test.step('opening empty code project should clear the scene', async () => {
|
||||
// Go back home.
|
||||
@ -985,6 +985,126 @@ test.describe(`Project management commands`, () => {
|
||||
})
|
||||
}
|
||||
)
|
||||
test(`Create a new project with a colliding name`, async ({
|
||||
context,
|
||||
homePage,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
const projectName = 'test-project'
|
||||
await test.step(`Setup`, async () => {
|
||||
await context.folderSetupFn(async (dir) => {
|
||||
const projectDir = path.join(dir, projectName)
|
||||
await Promise.all([fsp.mkdir(projectDir, { recursive: true })])
|
||||
await Promise.all([
|
||||
fsp.copyFile(
|
||||
executorInputPath('router-template-slate.kcl'),
|
||||
path.join(projectDir, 'main.kcl')
|
||||
),
|
||||
])
|
||||
})
|
||||
await homePage.expectState({
|
||||
projectCards: [
|
||||
{
|
||||
title: projectName,
|
||||
fileCount: 1,
|
||||
},
|
||||
],
|
||||
sortBy: 'last-modified-desc',
|
||||
})
|
||||
})
|
||||
|
||||
await test.step('Create a new project with the same name', async () => {
|
||||
await cmdBar.openCmdBar()
|
||||
await cmdBar.chooseCommand('create project')
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
commandName: 'Create project',
|
||||
currentArgKey: 'name',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Name: '',
|
||||
},
|
||||
highlightedHeaderArg: 'name',
|
||||
})
|
||||
await cmdBar.argumentInput.fill(projectName)
|
||||
await cmdBar.progressCmdBar()
|
||||
})
|
||||
|
||||
await test.step(`Check the project was created with a non-colliding name`, async () => {
|
||||
await toolbar.logoLink.click()
|
||||
await homePage.expectState({
|
||||
projectCards: [
|
||||
{
|
||||
title: projectName + '-1',
|
||||
fileCount: 1,
|
||||
},
|
||||
{
|
||||
title: projectName,
|
||||
fileCount: 1,
|
||||
},
|
||||
],
|
||||
sortBy: 'last-modified-desc',
|
||||
})
|
||||
})
|
||||
|
||||
await test.step('Create another project with the same name', async () => {
|
||||
await cmdBar.openCmdBar()
|
||||
await cmdBar.chooseCommand('create project')
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
commandName: 'Create project',
|
||||
currentArgKey: 'name',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Name: '',
|
||||
},
|
||||
highlightedHeaderArg: 'name',
|
||||
})
|
||||
await cmdBar.argumentInput.fill(projectName)
|
||||
await cmdBar.progressCmdBar()
|
||||
})
|
||||
|
||||
await test.step(`Check the second project was created with a non-colliding name`, async () => {
|
||||
await toolbar.logoLink.click()
|
||||
await homePage.expectState({
|
||||
projectCards: [
|
||||
{
|
||||
title: projectName + '-2',
|
||||
fileCount: 1,
|
||||
},
|
||||
{
|
||||
title: projectName + '-1',
|
||||
fileCount: 1,
|
||||
},
|
||||
{
|
||||
title: projectName,
|
||||
fileCount: 1,
|
||||
},
|
||||
],
|
||||
sortBy: 'last-modified-desc',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test(`Create a few projects using the default project name`, async ({
|
||||
homePage,
|
||||
toolbar,
|
||||
}) => {
|
||||
for (let i = 0; i < 12; i++) {
|
||||
await test.step(`Create project ${i}`, async () => {
|
||||
await homePage.expectState({
|
||||
projectCards: Array.from({ length: i }, (_, i) => ({
|
||||
title: `project-${i.toString().padStart(3, '0')}`,
|
||||
fileCount: 1,
|
||||
})).toReversed(),
|
||||
sortBy: 'last-modified-desc',
|
||||
})
|
||||
await homePage.createAndGoToProject()
|
||||
await toolbar.logoLink.click()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
test(
|
||||
@ -1391,7 +1511,7 @@ extrude001 = extrude(200, sketch001)`)
|
||||
await page.getByTestId('app-logo').click()
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'New project' })
|
||||
page.getByRole('button', { name: 'Create project' })
|
||||
).toBeVisible()
|
||||
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
@ -1465,7 +1585,7 @@ test(
|
||||
|
||||
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
|
||||
await expect(page.getByText('router-template-slate')).toBeVisible()
|
||||
await expect(page.getByText('New Project')).toBeVisible()
|
||||
await expect(page.getByText('Create project')).toBeVisible()
|
||||
})
|
||||
|
||||
await test.step('Opening the router-template project should load the stream', async () => {
|
||||
@ -1494,7 +1614,7 @@ test(
|
||||
|
||||
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
|
||||
await expect(page.getByText('router-template-slate')).toBeVisible()
|
||||
await expect(page.getByText('New Project')).toBeVisible()
|
||||
await expect(page.getByText('Create project')).toBeVisible()
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 60 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 44 KiB |
@ -1078,7 +1078,7 @@ export async function createProject({
|
||||
returnHome?: boolean
|
||||
}) {
|
||||
await test.step(`Create project and navigate to it`, async () => {
|
||||
await page.getByRole('button', { name: 'New project' }).click()
|
||||
await page.getByRole('button', { name: 'Create project' }).click()
|
||||
await page.getByRole('textbox', { name: 'Name' }).fill(name)
|
||||
await page.getByRole('button', { name: 'Continue' }).click()
|
||||
|
||||
|
||||
@ -154,7 +154,6 @@
|
||||
"@playwright/test": "^1.49.0",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^15.0.2",
|
||||
"@types/d3-force": "^3.0.10",
|
||||
"@types/diff": "^6.0.0",
|
||||
"@types/electron": "^1.6.10",
|
||||
"@types/isomorphic-fetch": "^0.0.39",
|
||||
@ -175,7 +174,6 @@
|
||||
"@vitest/web-worker": "^1.5.0",
|
||||
"@xstate/cli": "^0.5.17",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"d3-force": "^3.0.0",
|
||||
"electron": "32.1.2",
|
||||
"electron-builder": "24.13.3",
|
||||
"electron-notarize": "1.2.2",
|
||||
|
||||
16
src/App.tsx
16
src/App.tsx
@ -22,6 +22,8 @@ import Gizmo from 'components/Gizmo'
|
||||
import { CoreDumpManager } from 'lib/coredump'
|
||||
import { UnitsMenu } from 'components/UnitsMenu'
|
||||
import { CameraProjectionToggle } from 'components/CameraProjectionToggle'
|
||||
import { useCreateFileLinkQuery } from 'hooks/useCreateFileLinkQueryWatcher'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { maybeWriteToDisk } from 'lib/telemetry'
|
||||
maybeWriteToDisk()
|
||||
.then(() => {})
|
||||
@ -29,6 +31,20 @@ maybeWriteToDisk()
|
||||
|
||||
export function App() {
|
||||
const { project, file } = useLoaderData() as IndexLoaderData
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
|
||||
// Keep a lookout for a URL query string that invokes the 'import file from URL' command
|
||||
useCreateFileLinkQuery((argDefaultValues) => {
|
||||
commandBarSend({
|
||||
type: 'Find and select command',
|
||||
data: {
|
||||
groupId: 'projects',
|
||||
name: 'Import file from URL',
|
||||
argDefaultValues,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
useRefreshSettings(PATHS.FILE + 'SETTINGS')
|
||||
const navigate = useNavigate()
|
||||
const filePath = useAbsoluteFilePath()
|
||||
|
||||
@ -35,7 +35,7 @@ import { CommandBarProvider } from 'components/CommandBar/CommandBarProvider'
|
||||
import SettingsAuthProvider from 'components/SettingsAuthProvider'
|
||||
import LspProvider from 'components/LspProvider'
|
||||
import { KclContextProvider } from 'lang/KclProvider'
|
||||
import { BROWSER_PROJECT_NAME } from 'lib/constants'
|
||||
import { ASK_TO_OPEN_QUERY_PARAM, BROWSER_PROJECT_NAME } from 'lib/constants'
|
||||
import { CoreDumpManager } from 'lib/coredump'
|
||||
import { codeManager, engineCommandManager } from 'lib/singletons'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
@ -47,6 +47,7 @@ import { AppStateProvider } from 'AppState'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { RouteProvider } from 'components/RouteProvider'
|
||||
import { ProjectsContextProvider } from 'components/ProjectsContextProvider'
|
||||
import { OpenInDesktopAppHandler } from 'components/OpenInDesktopAppHandler'
|
||||
|
||||
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
|
||||
|
||||
@ -58,33 +59,44 @@ const router = createRouter([
|
||||
/* Make sure auth is the outermost provider or else we will have
|
||||
* inefficient re-renders, use the react profiler to see. */
|
||||
element: (
|
||||
<CommandBarProvider>
|
||||
<RouteProvider>
|
||||
<SettingsAuthProvider>
|
||||
<LspProvider>
|
||||
<ProjectsContextProvider>
|
||||
<KclContextProvider>
|
||||
<AppStateProvider>
|
||||
<MachineManagerProvider>
|
||||
<Outlet />
|
||||
</MachineManagerProvider>
|
||||
</AppStateProvider>
|
||||
</KclContextProvider>
|
||||
</ProjectsContextProvider>
|
||||
</LspProvider>
|
||||
</SettingsAuthProvider>
|
||||
</RouteProvider>
|
||||
</CommandBarProvider>
|
||||
<OpenInDesktopAppHandler>
|
||||
<CommandBarProvider>
|
||||
<RouteProvider>
|
||||
<SettingsAuthProvider>
|
||||
<LspProvider>
|
||||
<ProjectsContextProvider>
|
||||
<KclContextProvider>
|
||||
<AppStateProvider>
|
||||
<MachineManagerProvider>
|
||||
<Outlet />
|
||||
</MachineManagerProvider>
|
||||
</AppStateProvider>
|
||||
</KclContextProvider>
|
||||
</ProjectsContextProvider>
|
||||
</LspProvider>
|
||||
</SettingsAuthProvider>
|
||||
</RouteProvider>
|
||||
</CommandBarProvider>
|
||||
</OpenInDesktopAppHandler>
|
||||
),
|
||||
errorElement: <ErrorPage />,
|
||||
children: [
|
||||
{
|
||||
path: PATHS.INDEX,
|
||||
loader: async () => {
|
||||
loader: async ({ request }) => {
|
||||
const onDesktop = isDesktop()
|
||||
return onDesktop
|
||||
? redirect(PATHS.HOME)
|
||||
: redirect(PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME)
|
||||
const url = new URL(request.url)
|
||||
if (onDesktop) {
|
||||
return redirect(PATHS.HOME + (url.search || ''))
|
||||
} else {
|
||||
const searchParams = new URLSearchParams(url.search)
|
||||
if (!searchParams.has(ASK_TO_OPEN_QUERY_PARAM)) {
|
||||
return redirect(
|
||||
PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME + (url.search || '')
|
||||
)
|
||||
}
|
||||
}
|
||||
return null
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
215
src/Toolbar.tsx
215
src/Toolbar.tsx
@ -1,4 +1,4 @@
|
||||
import { useRef, useMemo, memo } from 'react'
|
||||
import { useRef, useMemo, memo, useCallback, useState } from 'react'
|
||||
import { isCursorInSketchCommandRange } from 'lang/util'
|
||||
import { engineCommandManager, kclManager } from 'lib/singletons'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
@ -34,8 +34,7 @@ export function Toolbar({
|
||||
const bgClassName = '!bg-transparent'
|
||||
const buttonBgClassName =
|
||||
'bg-chalkboard-transparent dark:bg-transparent disabled:bg-transparent dark:disabled:bg-transparent enabled:hover:bg-chalkboard-10 dark:enabled:hover:bg-chalkboard-100 pressed:!bg-primary pressed:enabled:hover:!text-chalkboard-10'
|
||||
const buttonBorderClassName =
|
||||
'!border-transparent hover:!border-chalkboard-20 dark:enabled:hover:!border-primary pressed:!border-primary ui-open:!border-primary'
|
||||
const buttonBorderClassName = '!border-transparent'
|
||||
|
||||
const sketchPathId = useMemo(() => {
|
||||
if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast))
|
||||
@ -50,6 +49,7 @@ export function Toolbar({
|
||||
const { overallState } = useNetworkContext()
|
||||
const { isExecuting } = useKclContext()
|
||||
const { isStreamReady } = useAppState()
|
||||
const [showRichContent, setShowRichContent] = useState(false)
|
||||
|
||||
const disableAllButtons =
|
||||
(overallState !== NetworkHealthState.Ok &&
|
||||
@ -77,6 +77,40 @@ export function Toolbar({
|
||||
[state, send, commandBarSend, sketchPathId]
|
||||
)
|
||||
|
||||
const tooltipContentClassName = !showRichContent
|
||||
? ''
|
||||
: '!text-left text-wrap !text-xs !p-0 !pb-2 flex gap-2 !max-w-none !w-72 flex-col items-stretch'
|
||||
const richContentTimeout = useRef<number | null>(null)
|
||||
const richContentClearTimeout = useRef<number | null>(null)
|
||||
// On mouse enter, show rich content after a 1s delay
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
// Cancel the clear timeout if it's already set
|
||||
if (richContentClearTimeout.current) {
|
||||
clearTimeout(richContentClearTimeout.current)
|
||||
}
|
||||
// Start our own timeout to show the rich content
|
||||
richContentTimeout.current = window.setTimeout(() => {
|
||||
setShowRichContent(true)
|
||||
if (richContentClearTimeout.current) {
|
||||
clearTimeout(richContentClearTimeout.current)
|
||||
}
|
||||
}, 1000)
|
||||
}, [setShowRichContent])
|
||||
// On mouse leave, clear the timeout and hide rich content
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
// Clear the timeout to show rich content
|
||||
if (richContentTimeout.current) {
|
||||
clearTimeout(richContentTimeout.current)
|
||||
}
|
||||
// Start a timeout to hide the rich content
|
||||
richContentClearTimeout.current = window.setTimeout(() => {
|
||||
setShowRichContent(false)
|
||||
if (richContentClearTimeout.current) {
|
||||
clearTimeout(richContentClearTimeout.current)
|
||||
}
|
||||
}, 500)
|
||||
}, [setShowRichContent])
|
||||
|
||||
/**
|
||||
* Resolve all the callbacks and values for the current mode,
|
||||
* so we don't need to worry about the other modes
|
||||
@ -174,43 +208,64 @@ export function Toolbar({
|
||||
status: itemConfig.status,
|
||||
}))}
|
||||
>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
id={maybeIconConfig[0].id}
|
||||
data-testid={maybeIconConfig[0].id}
|
||||
iconStart={{
|
||||
icon: maybeIconConfig[0].icon,
|
||||
className: iconClassName,
|
||||
bgClassName: bgClassName,
|
||||
}}
|
||||
className={
|
||||
'!border-transparent !px-0 pressed:!text-chalkboard-10 pressed:enabled:hovered:!text-chalkboard-10 ' +
|
||||
buttonBgClassName
|
||||
}
|
||||
aria-pressed={maybeIconConfig[0].isActive}
|
||||
disabled={
|
||||
disableAllButtons ||
|
||||
maybeIconConfig[0].status !== 'available' ||
|
||||
maybeIconConfig[0].disabled
|
||||
}
|
||||
name={maybeIconConfig[0].title}
|
||||
// aria-description is still in ARIA 1.3 draft.
|
||||
// eslint-disable-next-line jsx-a11y/aria-props
|
||||
aria-description={maybeIconConfig[0].description}
|
||||
onClick={() =>
|
||||
maybeIconConfig[0].onClick(configCallbackProps)
|
||||
}
|
||||
<div
|
||||
className="contents"
|
||||
// Mouse events do not fire on disabled buttons
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<span
|
||||
className={!maybeIconConfig[0].showTitle ? 'sr-only' : ''}
|
||||
<ActionButton
|
||||
Element="button"
|
||||
id={maybeIconConfig[0].id}
|
||||
data-testid={maybeIconConfig[0].id}
|
||||
iconStart={{
|
||||
icon: maybeIconConfig[0].icon,
|
||||
className: iconClassName,
|
||||
bgClassName: bgClassName,
|
||||
}}
|
||||
className={
|
||||
'!border-transparent !px-0 pressed:!text-chalkboard-10 pressed:enabled:hovered:!text-chalkboard-10 ' +
|
||||
buttonBgClassName
|
||||
}
|
||||
aria-pressed={maybeIconConfig[0].isActive}
|
||||
disabled={
|
||||
disableAllButtons ||
|
||||
maybeIconConfig[0].status !== 'available' ||
|
||||
maybeIconConfig[0].disabled
|
||||
}
|
||||
name={maybeIconConfig[0].title}
|
||||
// aria-description is still in ARIA 1.3 draft.
|
||||
// eslint-disable-next-line jsx-a11y/aria-props
|
||||
aria-description={maybeIconConfig[0].description}
|
||||
onClick={() =>
|
||||
maybeIconConfig[0].onClick(configCallbackProps)
|
||||
}
|
||||
>
|
||||
{maybeIconConfig[0].title}
|
||||
</span>
|
||||
</ActionButton>
|
||||
<ToolbarItemTooltip
|
||||
itemConfig={maybeIconConfig[0]}
|
||||
configCallbackProps={configCallbackProps}
|
||||
/>
|
||||
<span
|
||||
className={!maybeIconConfig[0].showTitle ? 'sr-only' : ''}
|
||||
>
|
||||
{maybeIconConfig[0].title}
|
||||
</span>
|
||||
<ToolbarItemTooltip
|
||||
itemConfig={maybeIconConfig[0]}
|
||||
configCallbackProps={configCallbackProps}
|
||||
wrapperClassName="ui-open:!hidden"
|
||||
contentClassName={tooltipContentClassName}
|
||||
>
|
||||
{showRichContent ? (
|
||||
<ToolbarItemTooltipRichContent
|
||||
itemConfig={maybeIconConfig[0]}
|
||||
/>
|
||||
) : (
|
||||
<ToolbarItemTooltipShortContent
|
||||
status={maybeIconConfig[0].status}
|
||||
title={maybeIconConfig[0].title}
|
||||
hotkey={maybeIconConfig[0].hotkey}
|
||||
/>
|
||||
)}
|
||||
</ToolbarItemTooltip>
|
||||
</ActionButton>
|
||||
</div>
|
||||
</ActionButtonDropdown>
|
||||
)
|
||||
}
|
||||
@ -218,7 +273,13 @@ export function Toolbar({
|
||||
|
||||
// A single button
|
||||
return (
|
||||
<div className="relative" key={itemConfig.id}>
|
||||
<div
|
||||
className="relative"
|
||||
key={itemConfig.id}
|
||||
// Mouse events do not fire on disabled buttons
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
key={itemConfig.id}
|
||||
@ -255,7 +316,18 @@ export function Toolbar({
|
||||
<ToolbarItemTooltip
|
||||
itemConfig={itemConfig}
|
||||
configCallbackProps={configCallbackProps}
|
||||
/>
|
||||
contentClassName={tooltipContentClassName}
|
||||
>
|
||||
{showRichContent ? (
|
||||
<ToolbarItemTooltipRichContent itemConfig={itemConfig} />
|
||||
) : (
|
||||
<ToolbarItemTooltipShortContent
|
||||
status={itemConfig.status}
|
||||
title={itemConfig.title}
|
||||
hotkey={itemConfig.hotkey}
|
||||
/>
|
||||
)}
|
||||
</ToolbarItemTooltip>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
@ -269,6 +341,12 @@ export function Toolbar({
|
||||
)
|
||||
}
|
||||
|
||||
interface ToolbarItemContentsProps extends React.PropsWithChildren {
|
||||
itemConfig: ToolbarItemResolved
|
||||
configCallbackProps: ToolbarItemCallbackProps
|
||||
wrapperClassName?: string
|
||||
contentClassName?: string
|
||||
}
|
||||
/**
|
||||
* The single button and dropdown button share content, so we extract it here
|
||||
* It contains a tooltip with the title, description, and links
|
||||
@ -277,12 +355,10 @@ export function Toolbar({
|
||||
const ToolbarItemTooltip = memo(function ToolbarItemContents({
|
||||
itemConfig,
|
||||
configCallbackProps,
|
||||
}: {
|
||||
itemConfig: ToolbarItemResolved
|
||||
configCallbackProps: ToolbarItemCallbackProps
|
||||
}) {
|
||||
const { state } = useModelingContext()
|
||||
|
||||
wrapperClassName = '',
|
||||
contentClassName = '',
|
||||
children,
|
||||
}: ToolbarItemContentsProps) {
|
||||
useHotkeys(
|
||||
itemConfig.hotkey || '',
|
||||
() => {
|
||||
@ -305,11 +381,50 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
|
||||
? ({ '-webkit-app-region': 'no-drag' } as React.CSSProperties)
|
||||
: {}
|
||||
}
|
||||
hoverOnly
|
||||
position="bottom"
|
||||
wrapperClassName="!p-4 !pointer-events-auto"
|
||||
contentClassName="!text-left text-wrap !text-xs !p-0 !pb-2 flex gap-2 !max-w-none !w-72 flex-col items-stretch"
|
||||
wrapperClassName={'!p-4 !pointer-events-auto ' + wrapperClassName}
|
||||
contentClassName={contentClassName}
|
||||
delay={0}
|
||||
>
|
||||
{children}
|
||||
</Tooltip>
|
||||
)
|
||||
})
|
||||
|
||||
const ToolbarItemTooltipShortContent = ({
|
||||
status,
|
||||
title,
|
||||
hotkey,
|
||||
}: {
|
||||
status: string
|
||||
title: string
|
||||
hotkey?: string | string[]
|
||||
}) => (
|
||||
<span
|
||||
className={`text-sm ${
|
||||
status !== 'available' ? 'text-chalkboard-70 dark:text-chalkboard-40' : ''
|
||||
}`}
|
||||
>
|
||||
{title}
|
||||
{hotkey && (
|
||||
<kbd className="inline-block ml-2 flex-none hotkey">{hotkey}</kbd>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
|
||||
const ToolbarItemTooltipRichContent = ({
|
||||
itemConfig,
|
||||
}: {
|
||||
itemConfig: ToolbarItemResolved
|
||||
}) => {
|
||||
const { state } = useModelingContext()
|
||||
return (
|
||||
<>
|
||||
<div className="rounded-top flex items-center gap-2 pt-3 pb-2 px-2 bg-chalkboard-20/50 dark:bg-chalkboard-80/50">
|
||||
{itemConfig.icon && (
|
||||
<CustomIcon className="w-5 h-5" name={itemConfig.icon} />
|
||||
)}
|
||||
<span
|
||||
className={`text-sm flex-1 ${
|
||||
itemConfig.status !== 'available'
|
||||
@ -378,6 +493,6 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</Tooltip>
|
||||
</>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@ -25,13 +25,13 @@ import {
|
||||
CallExpression,
|
||||
PathToNode,
|
||||
Program,
|
||||
SourceRange,
|
||||
Expr,
|
||||
parse,
|
||||
recast,
|
||||
defaultSourceRange,
|
||||
resultIsOk,
|
||||
ProgramMemory,
|
||||
topLevelRange,
|
||||
} from 'lang/wasm'
|
||||
import { CustomIcon, CustomIconName } from 'components/CustomIcon'
|
||||
import { ConstrainInfo } from 'lang/std/stdTypes'
|
||||
@ -600,8 +600,8 @@ const ConstraintSymbol = ({
|
||||
if (err(_node)) return
|
||||
const node = _node.node
|
||||
|
||||
const range: SourceRange = node
|
||||
? [node.start, node.end, true]
|
||||
const range = node
|
||||
? topLevelRange(node.start, node.end)
|
||||
: defaultSourceRange()
|
||||
|
||||
if (_type === 'intersectionTag') return null
|
||||
|
||||
@ -59,6 +59,7 @@ import {
|
||||
sourceRangeFromRust,
|
||||
resultIsOk,
|
||||
SourceRange,
|
||||
topLevelRange,
|
||||
} from 'lang/wasm'
|
||||
import { calculate_circle_from_3_points } from '../wasm-lib/pkg/wasm_lib'
|
||||
import {
|
||||
@ -628,7 +629,7 @@ export class SceneEntities {
|
||||
|
||||
const startRange = _node1.node.start
|
||||
const endRange = _node1.node.end
|
||||
const sourceRange: SourceRange = [startRange, endRange, true]
|
||||
const sourceRange = topLevelRange(startRange, endRange)
|
||||
const selection: Selections = computeSelectionFromSourceRangeAndAST(
|
||||
sourceRange,
|
||||
maybeModdedAst
|
||||
@ -1397,23 +1398,23 @@ export class SceneEntities {
|
||||
|
||||
const arg0 = arg(kclCircle3PointArgs[0])
|
||||
if (!arg0) return kclManager.ast
|
||||
arg0[0].value = points[0].x
|
||||
arg0[0].value = { value: points[0].x, suffix: 'None' }
|
||||
arg0[0].raw = points[0].x.toString()
|
||||
arg0[1].value = points[0].y
|
||||
arg0[1].value = { value: points[0].y, suffix: 'None' }
|
||||
arg0[1].raw = points[0].y.toString()
|
||||
|
||||
const arg1 = arg(kclCircle3PointArgs[1])
|
||||
if (!arg1) return kclManager.ast
|
||||
arg1[0].value = points[1].x
|
||||
arg1[0].value = { value: points[1].x, suffix: 'None' }
|
||||
arg1[0].raw = points[1].x.toString()
|
||||
arg1[1].value = points[1].y
|
||||
arg1[1].value = { value: points[1].y, suffix: 'None' }
|
||||
arg1[1].raw = points[1].y.toString()
|
||||
|
||||
const arg2 = arg(kclCircle3PointArgs[2])
|
||||
if (!arg2) return kclManager.ast
|
||||
arg2[0].value = points[2].x
|
||||
arg2[0].value = { value: points[2].x, suffix: 'None' }
|
||||
arg2[0].raw = points[2].x.toString()
|
||||
arg2[1].value = points[2].y
|
||||
arg2[1].value = { value: points[2].y, suffix: 'None' }
|
||||
arg2[1].raw = points[2].y.toString()
|
||||
|
||||
const astSnapshot = structuredClone(kclManager.ast)
|
||||
@ -2012,7 +2013,7 @@ export class SceneEntities {
|
||||
kclManager.programMemory,
|
||||
{
|
||||
type: 'sourceRange',
|
||||
sourceRange: [node.start, node.end, true],
|
||||
sourceRange: topLevelRange(node.start, node.end),
|
||||
},
|
||||
getChangeSketchInput()
|
||||
)
|
||||
@ -2050,8 +2051,8 @@ export class SceneEntities {
|
||||
)
|
||||
if (!(sk instanceof Reason)) {
|
||||
sketch = sk
|
||||
} else if ((maybeSketch as Solid).sketch) {
|
||||
sketch = (maybeSketch as Solid).sketch
|
||||
} else if (maybeSketch && (maybeSketch.value as Solid)?.sketch) {
|
||||
sketch = (maybeSketch.value as Solid).sketch
|
||||
}
|
||||
if (!sketch) return
|
||||
|
||||
@ -2263,7 +2264,7 @@ export class SceneEntities {
|
||||
)
|
||||
if (trap(_node, { suppress: true })) return
|
||||
const node = _node.node
|
||||
editorManager.setHighlightRange([[node.start, node.end, true]])
|
||||
editorManager.setHighlightRange([topLevelRange(node.start, node.end)])
|
||||
const yellow = 0xffff00
|
||||
colorSegment(selected, yellow)
|
||||
const extraSegmentGroup = parent.getObjectByName(EXTRA_SEGMENT_HANDLE)
|
||||
@ -2540,7 +2541,7 @@ export function sketchFromPathToNode({
|
||||
const varDec = _varDec.node
|
||||
const result = programMemory.get(varDec?.id?.name || '')
|
||||
if (result?.type === 'Solid') {
|
||||
return result.sketch
|
||||
return result.value.sketch
|
||||
}
|
||||
const sg = sketchFromKclValue(result, varDec?.id?.name)
|
||||
if (err(sg)) {
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import { Popover } from '@headlessui/react'
|
||||
import { ActionButtonProps } from './ActionButton'
|
||||
import { CustomIcon } from './CustomIcon'
|
||||
import Tooltip from './Tooltip'
|
||||
|
||||
type ActionButtonSplitProps = ActionButtonProps & { Element: 'button' } & {
|
||||
name?: string
|
||||
dropdownTooltipText?: string
|
||||
splitMenuItems: {
|
||||
id: string
|
||||
label: string
|
||||
@ -17,6 +19,7 @@ type ActionButtonSplitProps = ActionButtonProps & { Element: 'button' } & {
|
||||
export function ActionButtonDropdown({
|
||||
splitMenuItems,
|
||||
className,
|
||||
dropdownTooltipText = 'More tools',
|
||||
children,
|
||||
...props
|
||||
}: ActionButtonSplitProps) {
|
||||
@ -26,7 +29,14 @@ export function ActionButtonDropdown({
|
||||
{({ close }) => (
|
||||
<>
|
||||
{children}
|
||||
<Popover.Button className="border-transparent dark:border-transparent p-0 m-0 rounded-none !outline-none ui-open:border-primary ui-open:bg-primary">
|
||||
<Popover.Button
|
||||
className={
|
||||
'!border-transparent dark:!border-transparent ' +
|
||||
'bg-chalkboard-transparent dark:bg-transparent disabled:bg-transparent dark:disabled:bg-transparent ' +
|
||||
'enabled:hover:bg-chalkboard-10 dark:enabled:hover:bg-chalkboard-100 ' +
|
||||
'pressed:!bg-primary pressed:enabled:hover:!text-chalkboard-10 p-0 m-0 rounded-none !outline-none ui-open:border-primary ui-open:bg-primary'
|
||||
}
|
||||
>
|
||||
<CustomIcon
|
||||
name="caretDown"
|
||||
className={
|
||||
@ -37,6 +47,14 @@ export function ActionButtonDropdown({
|
||||
<span className="sr-only">
|
||||
{props.name ? props.name + ': ' : ''}open menu
|
||||
</span>
|
||||
<Tooltip
|
||||
delay={0}
|
||||
position="bottom"
|
||||
hoverOnly
|
||||
wrapperClassName="ui-open:!hidden"
|
||||
>
|
||||
{dropdownTooltipText}
|
||||
</Tooltip>
|
||||
</Popover.Button>
|
||||
<Popover.Panel
|
||||
as="ul"
|
||||
|
||||
@ -5,7 +5,7 @@ import { useEffect, useRef, useState } from 'react'
|
||||
import { trap } from 'lib/trap'
|
||||
import { codeToIdSelections } from 'lib/selections'
|
||||
import { codeRefFromRange } from 'lang/std/artifactGraph'
|
||||
import { defaultSourceRange } from 'lang/wasm'
|
||||
import { defaultSourceRange, SourceRange, topLevelRange } from 'lang/wasm'
|
||||
|
||||
export function AstExplorer() {
|
||||
const { context } = useModelingContext()
|
||||
@ -118,19 +118,19 @@ function DisplayObj({
|
||||
hasCursor ? 'bg-violet-100/80 dark:bg-violet-100/25' : ''
|
||||
}`}
|
||||
onMouseEnter={(e) => {
|
||||
editorManager.setHighlightRange([[obj?.start || 0, obj.end, true]])
|
||||
editorManager.setHighlightRange([
|
||||
topLevelRange(obj?.start || 0, obj.end),
|
||||
])
|
||||
e.stopPropagation()
|
||||
}}
|
||||
onMouseMove={(e) => {
|
||||
e.stopPropagation()
|
||||
editorManager.setHighlightRange([[obj?.start || 0, obj.end, true]])
|
||||
editorManager.setHighlightRange([
|
||||
topLevelRange(obj?.start || 0, obj.end),
|
||||
])
|
||||
}}
|
||||
onClick={(e) => {
|
||||
const range: [number, number, boolean] = [
|
||||
obj?.start || 0,
|
||||
obj.end || 0,
|
||||
true,
|
||||
]
|
||||
const range = topLevelRange(obj?.start || 0, obj.end || 0)
|
||||
const idInfo = codeToIdSelections([
|
||||
{ codeRef: codeRefFromRange(range, kclManager.ast) },
|
||||
])[0]
|
||||
|
||||
@ -129,11 +129,13 @@ function CommandArgOptionInput({
|
||||
<label
|
||||
htmlFor="option-input"
|
||||
className="capitalize px-2 py-1 rounded-l bg-chalkboard-100 dark:bg-chalkboard-80 text-chalkboard-10 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80"
|
||||
data-testid="cmd-bar-arg-name"
|
||||
>
|
||||
{argName}
|
||||
</label>
|
||||
<Combobox.Input
|
||||
id="option-input"
|
||||
data-testid="cmd-bar-arg-value"
|
||||
ref={inputRef}
|
||||
onChange={(event) =>
|
||||
!event.target.disabled && setQuery(event.target.value)
|
||||
|
||||
@ -17,7 +17,7 @@ import { StateFrom } from 'xstate'
|
||||
const semanticEntityNames: {
|
||||
[key: string]: Array<Artifact['type'] | 'defaultPlane'>
|
||||
} = {
|
||||
face: ['wall', 'cap', 'solid2D'],
|
||||
face: ['wall', 'cap', 'solid2d'],
|
||||
edge: ['segment', 'sweepEdge', 'edgeCutEdge'],
|
||||
point: [],
|
||||
plane: ['defaultPlane'],
|
||||
|
||||
@ -52,6 +52,7 @@ function CommandComboBox({
|
||||
className="w-5 h-5 bg-primary/10 dark:bg-primary text-primary dark:text-inherit"
|
||||
/>
|
||||
<Combobox.Input
|
||||
data-testid="cmd-bar-search"
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
className="w-full bg-transparent focus:outline-none selection:bg-primary/20 dark:selection:bg-primary/40 dark:focus:outline-none"
|
||||
onKeyDown={(event) => {
|
||||
@ -85,6 +86,7 @@ function CommandComboBox({
|
||||
value={option}
|
||||
className="flex items-center gap-4 px-4 py-1.5 first:mt-2 last:mb-2 ui-active:bg-primary/10 dark:ui-active:bg-chalkboard-90 ui-disabled:!text-chalkboard-50"
|
||||
disabled={optionIsDisabled(option)}
|
||||
data-testid={`cmd-bar-option`}
|
||||
>
|
||||
{'icon' in option && option.icon && (
|
||||
<CustomIcon name={option.icon} className="w-5 h-5" />
|
||||
|
||||
@ -1,10 +1,7 @@
|
||||
import { useMemo } from 'react'
|
||||
import { engineCommandManager } from 'lib/singletons'
|
||||
import {
|
||||
ArtifactGraph,
|
||||
expandPlane,
|
||||
PlaneArtifactRich,
|
||||
} from 'lang/std/artifactGraph'
|
||||
import { expandPlane, PlaneArtifactRich } from 'lang/std/artifactGraph'
|
||||
import { ArtifactGraph } from 'lang/wasm'
|
||||
import { DebugDisplayArray, GenericObj } from './DebugDisplayObj'
|
||||
|
||||
export function DebugFeatureTree() {
|
||||
|
||||
@ -48,8 +48,9 @@ export const FileMachineProvider = ({
|
||||
}) => {
|
||||
const navigate = useNavigate()
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const { settings } = useSettingsAuthContext()
|
||||
const { project, file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
||||
const { settings, auth } = useSettingsAuthContext()
|
||||
const projectData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
||||
const { project, file } = projectData
|
||||
const [kclSamples, setKclSamples] = React.useState<KclSamplesManifestItem[]>(
|
||||
[]
|
||||
)
|
||||
@ -296,40 +297,47 @@ export const FileMachineProvider = ({
|
||||
|
||||
const kclCommandMemo = useMemo(
|
||||
() =>
|
||||
kclCommands(
|
||||
async (data) => {
|
||||
if (data.method === 'overwrite') {
|
||||
codeManager.updateCodeStateEditor(data.code)
|
||||
await kclManager.executeCode(true)
|
||||
await codeManager.writeToFile()
|
||||
} else if (data.method === 'newFile' && isDesktop()) {
|
||||
send({
|
||||
type: 'Create file',
|
||||
data: {
|
||||
name: data.sampleName,
|
||||
content: data.code,
|
||||
makeDir: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Either way, we want to overwrite the defaultUnit project setting
|
||||
// with the sample's setting.
|
||||
if (data.sampleUnits) {
|
||||
settings.send({
|
||||
type: 'set.modeling.defaultUnit',
|
||||
data: {
|
||||
level: 'project',
|
||||
value: data.sampleUnits,
|
||||
},
|
||||
})
|
||||
}
|
||||
kclCommands({
|
||||
authToken: auth?.context?.token ?? '',
|
||||
projectData,
|
||||
settings: {
|
||||
defaultUnit: settings?.context?.modeling.defaultUnit.current ?? 'mm',
|
||||
},
|
||||
kclSamples.map((sample) => ({
|
||||
value: sample.pathFromProjectDirectoryToFirstFile,
|
||||
name: sample.title,
|
||||
}))
|
||||
).filter(
|
||||
specialPropsForSampleCommand: {
|
||||
onSubmit: async (data) => {
|
||||
if (data.method === 'overwrite') {
|
||||
codeManager.updateCodeStateEditor(data.code)
|
||||
await kclManager.executeCode(true)
|
||||
await codeManager.writeToFile()
|
||||
} else if (data.method === 'newFile' && isDesktop()) {
|
||||
send({
|
||||
type: 'Create file',
|
||||
data: {
|
||||
name: data.sampleName,
|
||||
content: data.code,
|
||||
makeDir: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Either way, we want to overwrite the defaultUnit project setting
|
||||
// with the sample's setting.
|
||||
if (data.sampleUnits) {
|
||||
settings.send({
|
||||
type: 'set.modeling.defaultUnit',
|
||||
data: {
|
||||
level: 'project',
|
||||
value: data.sampleUnits,
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
providedOptions: kclSamples.map((sample) => ({
|
||||
value: sample.pathFromProjectDirectoryToFirstFile,
|
||||
name: sample.title,
|
||||
})),
|
||||
},
|
||||
}).filter(
|
||||
(command) => kclSamples.length || command.name !== 'open-kcl-example'
|
||||
),
|
||||
[codeManager, kclManager, send, kclSamples]
|
||||
|
||||
@ -95,9 +95,11 @@ export const processMemory = (programMemory: ProgramMemory) => {
|
||||
) {
|
||||
const sk = sketchFromKclValueOptional(val, key)
|
||||
if (val.type === 'Solid') {
|
||||
processedMemory[key] = val.value.map(({ ...rest }: ExtrudeSurface) => {
|
||||
return rest
|
||||
})
|
||||
processedMemory[key] = val.value.value.map(
|
||||
({ ...rest }: ExtrudeSurface) => {
|
||||
return rest
|
||||
}
|
||||
)
|
||||
} else if (!(sk instanceof Reason)) {
|
||||
processedMemory[key] = sk.paths.map(({ __geoMeta, ...rest }: Path) => {
|
||||
return rest
|
||||
|
||||
68
src/components/OpenInDesktopAppHandler.test.tsx
Normal file
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
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
|
||||
)
|
||||
}
|
||||
@ -10,11 +10,13 @@ import { APP_NAME } from 'lib/constants'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { CustomIcon } from './CustomIcon'
|
||||
import { useLspContext } from './LspProvider'
|
||||
import { engineCommandManager, kclManager } from 'lib/singletons'
|
||||
import { codeManager, engineCommandManager, kclManager } from 'lib/singletons'
|
||||
import { MachineManagerContext } from 'components/MachineManagerProvider'
|
||||
import usePlatform from 'hooks/usePlatform'
|
||||
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
||||
import Tooltip from './Tooltip'
|
||||
import { copyFileShareLink } from 'lib/links'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
|
||||
const ProjectSidebarMenu = ({
|
||||
project,
|
||||
@ -95,6 +97,7 @@ function ProjectMenuPopover({
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const filePath = useAbsoluteFilePath()
|
||||
const { settings, auth } = useSettingsAuthContext()
|
||||
const machineManager = useContext(MachineManagerContext)
|
||||
|
||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||
@ -155,7 +158,6 @@ function ProjectMenuPopover({
|
||||
data: exportCommandInfo,
|
||||
}),
|
||||
},
|
||||
'break',
|
||||
{
|
||||
id: 'make',
|
||||
Element: 'button',
|
||||
@ -181,6 +183,19 @@ function ProjectMenuPopover({
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'share-link',
|
||||
Element: 'button',
|
||||
children: 'Share link to file',
|
||||
onClick: async () => {
|
||||
await copyFileShareLink({
|
||||
token: auth?.context.token || '',
|
||||
code: codeManager.code,
|
||||
name: project?.name || '',
|
||||
units: settings.context.modeling.defaultUnit.current,
|
||||
})
|
||||
},
|
||||
},
|
||||
'break',
|
||||
{
|
||||
id: 'go-home',
|
||||
|
||||
@ -3,11 +3,11 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
||||
import { useProjectsLoader } from 'hooks/useProjectsLoader'
|
||||
import { projectsMachine } from 'machines/projectsMachine'
|
||||
import { createContext, useEffect, useState } from 'react'
|
||||
import { createContext, useCallback, useEffect, useState } from 'react'
|
||||
import { Actor, AnyStateMachine, fromPromise, Prop, StateFrom } from 'xstate'
|
||||
import { useLspContext } from './LspProvider'
|
||||
import toast from 'react-hot-toast'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { PATHS } from 'lib/paths'
|
||||
import {
|
||||
createNewProjectDirectory,
|
||||
@ -18,11 +18,28 @@ import {
|
||||
getNextProjectIndex,
|
||||
interpolateProjectNameWithIndex,
|
||||
doesProjectNameNeedInterpolated,
|
||||
getUniqueProjectName,
|
||||
getNextFileName,
|
||||
} from 'lib/desktopFS'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import useStateMachineCommands from 'hooks/useStateMachineCommands'
|
||||
import { projectsCommandBarConfig } from 'lib/commandBarConfigs/projectsCommandConfig'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import {
|
||||
CREATE_FILE_URL_PARAM,
|
||||
FILE_EXT,
|
||||
PROJECT_ENTRYPOINT,
|
||||
} from 'lib/constants'
|
||||
import { DeepPartial } from 'lib/types'
|
||||
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
||||
import { codeManager } from 'lib/singletons'
|
||||
import {
|
||||
loadAndValidateSettings,
|
||||
projectConfigurationToSettingsPayload,
|
||||
saveSettings,
|
||||
setSettingsAtLevel,
|
||||
} from 'lib/settings/settingsUtils'
|
||||
import { Project } from 'lib/project'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state?: StateFrom<T>
|
||||
@ -52,12 +69,110 @@ export const ProjectsContextProvider = ({
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* We need some of the functionality of the ProjectsContextProvider in the web version
|
||||
* but we can't perform file system operations in the browser,
|
||||
* so most of the behavior of this machine is stubbed out.
|
||||
*/
|
||||
const ProjectsContextWeb = ({ children }: { children: React.ReactNode }) => {
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const clearImportSearchParams = useCallback(() => {
|
||||
// Clear the search parameters related to the "Import file from URL" command
|
||||
// or we'll never be able cancel or submit it.
|
||||
searchParams.delete(CREATE_FILE_URL_PARAM)
|
||||
searchParams.delete('code')
|
||||
searchParams.delete('name')
|
||||
searchParams.delete('units')
|
||||
setSearchParams(searchParams)
|
||||
}, [searchParams, setSearchParams])
|
||||
const {
|
||||
settings: { context: settings, send: settingsSend },
|
||||
} = useSettingsAuthContext()
|
||||
|
||||
const [state, send, actor] = useMachine(
|
||||
projectsMachine.provide({
|
||||
actions: {
|
||||
navigateToProject: () => {},
|
||||
navigateToProjectIfNeeded: () => {},
|
||||
navigateToFile: () => {},
|
||||
toastSuccess: ({ event }) =>
|
||||
toast.success(
|
||||
('data' in event && typeof event.data === 'string' && event.data) ||
|
||||
('output' in event &&
|
||||
'message' in event.output &&
|
||||
typeof event.output.message === 'string' &&
|
||||
event.output.message) ||
|
||||
''
|
||||
),
|
||||
toastError: ({ event }) =>
|
||||
toast.error(
|
||||
('data' in event && typeof event.data === 'string' && event.data) ||
|
||||
('output' in event &&
|
||||
typeof event.output === 'string' &&
|
||||
event.output) ||
|
||||
''
|
||||
),
|
||||
},
|
||||
actors: {
|
||||
readProjects: fromPromise(async () => [] as Project[]),
|
||||
createProject: fromPromise(async () => ({
|
||||
message: 'not implemented on web',
|
||||
})),
|
||||
renameProject: fromPromise(async () => ({
|
||||
message: 'not implemented on web',
|
||||
oldName: '',
|
||||
newName: '',
|
||||
})),
|
||||
deleteProject: fromPromise(async () => ({
|
||||
message: 'not implemented on web',
|
||||
name: '',
|
||||
})),
|
||||
createFile: fromPromise(async ({ input }) => {
|
||||
// Browser version doesn't navigate, just overwrites the current file
|
||||
clearImportSearchParams()
|
||||
codeManager.updateCodeStateEditor(input.code || '')
|
||||
await codeManager.writeToFile()
|
||||
|
||||
settingsSend({
|
||||
type: 'set.modeling.defaultUnit',
|
||||
data: {
|
||||
level: 'project',
|
||||
value: input.units,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
message: 'File and units overwritten successfully',
|
||||
fileName: input.name,
|
||||
projectName: '',
|
||||
}
|
||||
}),
|
||||
},
|
||||
}),
|
||||
{
|
||||
input: {
|
||||
projects: [],
|
||||
defaultProjectName: settings.projects.defaultProjectName.current,
|
||||
defaultDirectory: settings.app.projectDirectory.current,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// register all project-related command palette commands
|
||||
useStateMachineCommands({
|
||||
machineId: 'projects',
|
||||
send,
|
||||
state,
|
||||
commandBarConfig: projectsCommandBarConfig,
|
||||
actor,
|
||||
onCancel: clearImportSearchParams,
|
||||
})
|
||||
|
||||
return (
|
||||
<ProjectsMachineContext.Provider
|
||||
value={{
|
||||
state: undefined,
|
||||
send: () => {},
|
||||
state,
|
||||
send,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
@ -72,19 +187,22 @@ const ProjectsContextDesktop = ({
|
||||
}) => {
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
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 { commandBarSend } = useCommandsContext()
|
||||
const { onProjectOpen } = useLspContext()
|
||||
const {
|
||||
settings: { context: settings },
|
||||
} = useSettingsAuthContext()
|
||||
|
||||
useEffect(() => {
|
||||
console.log(
|
||||
'project directory changed',
|
||||
settings.app.projectDirectory.current
|
||||
)
|
||||
}, [settings.app.projectDirectory.current])
|
||||
|
||||
const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0)
|
||||
const { projectPaths, projectsDir } = useProjectsLoader([
|
||||
projectsLoaderTrigger,
|
||||
@ -168,6 +286,31 @@ const ProjectsContextDesktop = ({
|
||||
}
|
||||
}
|
||||
},
|
||||
navigateToFile: ({ context, event }) => {
|
||||
if (event.type !== 'xstate.done.actor.create-file') return
|
||||
// For now, the browser version of create-file doesn't need to navigate
|
||||
// since it just overwrites the current file.
|
||||
if (!isDesktop()) return
|
||||
let projectPath = window.electron.join(
|
||||
context.defaultDirectory,
|
||||
event.output.projectName
|
||||
)
|
||||
let filePath = window.electron.join(
|
||||
projectPath,
|
||||
event.output.fileName
|
||||
)
|
||||
onProjectOpen(
|
||||
{
|
||||
name: event.output.projectName,
|
||||
path: projectPath,
|
||||
},
|
||||
null
|
||||
)
|
||||
const pathToNavigateTo = `${PATHS.FILE}/${encodeURIComponent(
|
||||
filePath
|
||||
)}`
|
||||
navigate(pathToNavigateTo)
|
||||
},
|
||||
toastSuccess: ({ event }) =>
|
||||
toast.success(
|
||||
('data' in event && typeof event.data === 'string' && event.data) ||
|
||||
@ -195,16 +338,12 @@ const ProjectsContextDesktop = ({
|
||||
: settings.projects.defaultProjectName.current
|
||||
).trim()
|
||||
|
||||
if (doesProjectNameNeedInterpolated(name)) {
|
||||
const nextIndex = getNextProjectIndex(name, input.projects)
|
||||
name = interpolateProjectNameWithIndex(name, nextIndex)
|
||||
}
|
||||
|
||||
await createNewProjectDirectory(name)
|
||||
const uniqueName = getUniqueProjectName(name, input.projects)
|
||||
await createNewProjectDirectory(uniqueName)
|
||||
|
||||
return {
|
||||
message: `Successfully created "${name}"`,
|
||||
name,
|
||||
message: `Successfully created "${uniqueName}"`,
|
||||
name: uniqueName,
|
||||
}
|
||||
}),
|
||||
renameProject: fromPromise(async ({ input }) => {
|
||||
@ -221,8 +360,6 @@ const ProjectsContextDesktop = ({
|
||||
name = interpolateProjectNameWithIndex(name, nextIndex)
|
||||
}
|
||||
|
||||
console.log('from Project')
|
||||
|
||||
await renameProjectDirectory(
|
||||
window.electron.path.join(defaultDirectory, oldName),
|
||||
name
|
||||
@ -245,13 +382,82 @@ const ProjectsContextDesktop = ({
|
||||
name: input.name,
|
||||
}
|
||||
}),
|
||||
},
|
||||
guards: {
|
||||
'Has at least 1 project': ({ event }) => {
|
||||
if (event.type !== 'xstate.done.actor.read-projects') return false
|
||||
console.log(`from has at least 1 project: ${event.output.length}`)
|
||||
return event.output.length ? event.output.length >= 1 : false
|
||||
},
|
||||
createFile: fromPromise(async ({ input }) => {
|
||||
let projectName =
|
||||
(input.method === 'newProject' ? input.name : input.projectName) ||
|
||||
settings.projects.defaultProjectName.current
|
||||
let fileName =
|
||||
input.method === 'newProject'
|
||||
? PROJECT_ENTRYPOINT
|
||||
: input.name.endsWith(FILE_EXT)
|
||||
? input.name
|
||||
: input.name + FILE_EXT
|
||||
let message = 'File created successfully'
|
||||
const unitsConfiguration: DeepPartial<Configuration> = {
|
||||
settings: {
|
||||
project: {
|
||||
directory: settings.app.projectDirectory.current,
|
||||
},
|
||||
modeling: {
|
||||
base_unit: input.units,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const needsInterpolated = doesProjectNameNeedInterpolated(projectName)
|
||||
if (needsInterpolated) {
|
||||
const nextIndex = getNextProjectIndex(projectName, input.projects)
|
||||
projectName = interpolateProjectNameWithIndex(
|
||||
projectName,
|
||||
nextIndex
|
||||
)
|
||||
}
|
||||
|
||||
// Create the project around the file if newProject
|
||||
if (input.method === 'newProject') {
|
||||
await createNewProjectDirectory(
|
||||
projectName,
|
||||
input.code,
|
||||
unitsConfiguration
|
||||
)
|
||||
message = `Project "${projectName}" created successfully with link contents`
|
||||
} else {
|
||||
let projectPath = window.electron.join(
|
||||
settings.app.projectDirectory.current,
|
||||
projectName
|
||||
)
|
||||
|
||||
message = `File "${fileName}" created successfully`
|
||||
const existingConfiguration = await loadAndValidateSettings(
|
||||
projectPath
|
||||
)
|
||||
const settingsToSave = setSettingsAtLevel(
|
||||
existingConfiguration.settings,
|
||||
'project',
|
||||
projectConfigurationToSettingsPayload(unitsConfiguration)
|
||||
)
|
||||
await saveSettings(settingsToSave, projectPath)
|
||||
}
|
||||
|
||||
// Create the file
|
||||
let baseDir = window.electron.join(
|
||||
settings.app.projectDirectory.current,
|
||||
projectName
|
||||
)
|
||||
const { name, path } = getNextFileName({
|
||||
entryName: fileName,
|
||||
baseDir,
|
||||
})
|
||||
|
||||
fileName = name
|
||||
await window.electron.writeFile(path, input.code || '')
|
||||
|
||||
return {
|
||||
message,
|
||||
fileName,
|
||||
projectName,
|
||||
}
|
||||
}),
|
||||
},
|
||||
}),
|
||||
{
|
||||
@ -274,6 +480,7 @@ const ProjectsContextDesktop = ({
|
||||
state,
|
||||
commandBarConfig: projectsCommandBarConfig,
|
||||
actor,
|
||||
onCancel: clearImportSearchParams,
|
||||
})
|
||||
|
||||
return (
|
||||
|
||||
@ -301,7 +301,7 @@ export const Stream = () => {
|
||||
return
|
||||
}
|
||||
const path = getArtifactOfTypes(
|
||||
{ key: entity_id, types: ['path', 'solid2D', 'segment'] },
|
||||
{ key: entity_id, types: ['path', 'solid2d', 'segment'] },
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
if (err(path)) {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { toolTips } from 'lang/langHelpers'
|
||||
import { Selection, Selections } from 'lib/selections'
|
||||
import { PathToNode, Program, Expr } from '../../lang/wasm'
|
||||
import { PathToNode, Program, Expr, topLevelRange } from '../../lang/wasm'
|
||||
import { getNodeFromPath } from '../../lang/queryAst'
|
||||
import {
|
||||
PathToNodeMap,
|
||||
@ -41,7 +41,7 @@ export function removeConstrainingValuesInfo({
|
||||
graphSelections: nodes.map(
|
||||
(node): Selection => ({
|
||||
codeRef: codeRefFromRange(
|
||||
[node.start, node.end, true],
|
||||
topLevelRange(node.start, node.end),
|
||||
kclManager.ast
|
||||
),
|
||||
})
|
||||
|
||||
@ -6,5 +6,6 @@ export const useCommandsContext = () => {
|
||||
return {
|
||||
commandBarSend: commandBarActor.send,
|
||||
commandBarState,
|
||||
commandBarActor,
|
||||
}
|
||||
}
|
||||
|
||||
65
src/hooks/useCreateFileLinkQueryWatcher.ts
Normal file
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(() => {
|
||||
// Useless on web, until we get fake filesystems over there.
|
||||
if (!isDesktop) return
|
||||
if (!isDesktop()) return
|
||||
|
||||
if (deps && deps[0] === lastTs) return
|
||||
|
||||
|
||||
@ -22,6 +22,7 @@ import {
|
||||
ProgramMemory,
|
||||
recast,
|
||||
SourceRange,
|
||||
topLevelRange,
|
||||
} from 'lang/wasm'
|
||||
import { getNodeFromPath } from './queryAst'
|
||||
import { codeManager, editorManager, sceneInfra } from 'lib/singletons'
|
||||
@ -376,11 +377,7 @@ export class KclManager {
|
||||
}
|
||||
this.ast = { ...ast }
|
||||
// updateArtifactGraph relies on updated executeState/programMemory
|
||||
await this.engineCommandManager.updateArtifactGraph(
|
||||
this.ast,
|
||||
execState.artifactCommands,
|
||||
execState.artifacts
|
||||
)
|
||||
this.engineCommandManager.updateArtifactGraph(execState.artifactGraph)
|
||||
this._executeCallback()
|
||||
if (!isInterrupted) {
|
||||
sceneInfra.modelingSend({ type: 'code edit during sketch' })
|
||||
@ -473,7 +470,7 @@ export class KclManager {
|
||||
...artifact,
|
||||
codeRef: {
|
||||
...artifact.codeRef,
|
||||
range: [node.start, node.end, true],
|
||||
range: topLevelRange(node.start, node.end),
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -594,7 +591,7 @@ export class KclManager {
|
||||
if (start && end) {
|
||||
returnVal.graphSelections.push({
|
||||
codeRef: {
|
||||
range: [start, end, true],
|
||||
range: topLevelRange(start, end),
|
||||
pathToNode: path,
|
||||
},
|
||||
})
|
||||
|
||||
@ -24,7 +24,10 @@ describe('testing AST', () => {
|
||||
type: 'Literal',
|
||||
start: 0,
|
||||
end: 1,
|
||||
value: 5,
|
||||
value: {
|
||||
suffix: 'None',
|
||||
value: 5,
|
||||
},
|
||||
raw: '5',
|
||||
},
|
||||
operator: '+',
|
||||
@ -32,7 +35,10 @@ describe('testing AST', () => {
|
||||
type: 'Literal',
|
||||
start: 3,
|
||||
end: 4,
|
||||
value: 6,
|
||||
value: {
|
||||
suffix: 'None',
|
||||
value: 6,
|
||||
},
|
||||
raw: '6',
|
||||
},
|
||||
},
|
||||
|
||||
@ -54,6 +54,9 @@ const mySketch001 = startSketchOn('XY')
|
||||
},
|
||||
],
|
||||
id: expect.any(String),
|
||||
units: {
|
||||
type: 'Mm',
|
||||
},
|
||||
__meta: [{ sourceRange: [46, 71, 0] }],
|
||||
},
|
||||
})
|
||||
@ -72,56 +75,65 @@ const mySketch001 = startSketchOn('XY')
|
||||
const sketch001 = execState.memory.get('mySketch001')
|
||||
expect(sketch001).toEqual({
|
||||
type: 'Solid',
|
||||
id: expect.any(String),
|
||||
value: [
|
||||
{
|
||||
type: 'extrudePlane',
|
||||
faceId: expect.any(String),
|
||||
tag: null,
|
||||
id: expect.any(String),
|
||||
sourceRange: [77, 102, 0],
|
||||
},
|
||||
{
|
||||
type: 'extrudePlane',
|
||||
faceId: expect.any(String),
|
||||
tag: null,
|
||||
id: expect.any(String),
|
||||
sourceRange: [108, 132, 0],
|
||||
},
|
||||
],
|
||||
sketch: {
|
||||
value: {
|
||||
type: 'Solid',
|
||||
id: expect.any(String),
|
||||
__meta: expect.any(Array),
|
||||
on: expect.any(Object),
|
||||
start: expect.any(Object),
|
||||
type: 'Sketch',
|
||||
paths: [
|
||||
value: [
|
||||
{
|
||||
type: 'ToPoint',
|
||||
from: [0, 0],
|
||||
to: [-1.59, -1.54],
|
||||
type: 'extrudePlane',
|
||||
faceId: expect.any(String),
|
||||
tag: null,
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [77, 102, 0],
|
||||
},
|
||||
id: expect.any(String),
|
||||
sourceRange: [77, 102, 0],
|
||||
},
|
||||
{
|
||||
type: 'ToPoint',
|
||||
from: [-1.59, -1.54],
|
||||
to: [0.46, -5.82],
|
||||
type: 'extrudePlane',
|
||||
faceId: expect.any(String),
|
||||
tag: null,
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [108, 132, 0],
|
||||
},
|
||||
id: expect.any(String),
|
||||
sourceRange: [108, 132, 0],
|
||||
},
|
||||
],
|
||||
sketch: {
|
||||
id: expect.any(String),
|
||||
units: {
|
||||
type: 'Mm',
|
||||
},
|
||||
__meta: expect.any(Array),
|
||||
on: expect.any(Object),
|
||||
start: expect.any(Object),
|
||||
type: 'Sketch',
|
||||
paths: [
|
||||
{
|
||||
type: 'ToPoint',
|
||||
from: [0, 0],
|
||||
to: [-1.59, -1.54],
|
||||
tag: null,
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [77, 102, 0],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'ToPoint',
|
||||
from: [-1.59, -1.54],
|
||||
to: [0.46, -5.82],
|
||||
tag: null,
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [108, 132, 0],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
height: 2,
|
||||
startCapId: expect.any(String),
|
||||
endCapId: expect.any(String),
|
||||
units: {
|
||||
type: 'Mm',
|
||||
},
|
||||
__meta: [{ sourceRange: [46, 71, 0] }],
|
||||
},
|
||||
height: 2,
|
||||
startCapId: expect.any(String),
|
||||
endCapId: expect.any(String),
|
||||
__meta: [{ sourceRange: [46, 71, 0] }],
|
||||
})
|
||||
})
|
||||
test('sketch extrude and sketch on one of the faces', async () => {
|
||||
@ -154,187 +166,205 @@ const sk2 = startSketchOn('XY')
|
||||
expect(geos).toEqual([
|
||||
{
|
||||
type: 'Solid',
|
||||
id: expect.any(String),
|
||||
value: [
|
||||
{
|
||||
type: 'extrudePlane',
|
||||
faceId: expect.any(String),
|
||||
tag: null,
|
||||
id: expect.any(String),
|
||||
sourceRange: [69, 89, 0],
|
||||
},
|
||||
{
|
||||
type: 'extrudePlane',
|
||||
faceId: expect.any(String),
|
||||
tag: {
|
||||
end: 116,
|
||||
start: 114,
|
||||
type: 'TagDeclarator',
|
||||
value: 'p',
|
||||
},
|
||||
id: expect.any(String),
|
||||
sourceRange: [95, 117, 0],
|
||||
},
|
||||
{
|
||||
type: 'extrudePlane',
|
||||
faceId: expect.any(String),
|
||||
tag: null,
|
||||
id: expect.any(String),
|
||||
sourceRange: [123, 142, 0],
|
||||
},
|
||||
],
|
||||
sketch: {
|
||||
value: {
|
||||
type: 'Solid',
|
||||
id: expect.any(String),
|
||||
__meta: expect.any(Array),
|
||||
on: expect.any(Object),
|
||||
start: expect.any(Object),
|
||||
type: 'Sketch',
|
||||
tags: {
|
||||
p: {
|
||||
__meta: [
|
||||
{
|
||||
sourceRange: [114, 116, 0],
|
||||
},
|
||||
],
|
||||
type: 'TagIdentifier',
|
||||
value: 'p',
|
||||
info: expect.any(Object),
|
||||
},
|
||||
},
|
||||
paths: [
|
||||
value: [
|
||||
{
|
||||
type: 'ToPoint',
|
||||
from: [0, 0],
|
||||
to: [-2.5, 0],
|
||||
type: 'extrudePlane',
|
||||
faceId: expect.any(String),
|
||||
tag: null,
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [69, 89, 0],
|
||||
},
|
||||
id: expect.any(String),
|
||||
sourceRange: [69, 89, 0],
|
||||
},
|
||||
{
|
||||
type: 'ToPoint',
|
||||
from: [-2.5, 0],
|
||||
to: [0, 10],
|
||||
type: 'extrudePlane',
|
||||
faceId: expect.any(String),
|
||||
tag: {
|
||||
end: 116,
|
||||
start: 114,
|
||||
type: 'TagDeclarator',
|
||||
value: 'p',
|
||||
},
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [95, 117, 0],
|
||||
},
|
||||
id: expect.any(String),
|
||||
sourceRange: [95, 117, 0],
|
||||
},
|
||||
{
|
||||
type: 'ToPoint',
|
||||
from: [0, 10],
|
||||
to: [2.5, 0],
|
||||
type: 'extrudePlane',
|
||||
faceId: expect.any(String),
|
||||
tag: null,
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [123, 142, 0],
|
||||
},
|
||||
id: expect.any(String),
|
||||
sourceRange: [123, 142, 0],
|
||||
},
|
||||
],
|
||||
sketch: {
|
||||
id: expect.any(String),
|
||||
__meta: expect.any(Array),
|
||||
on: expect.any(Object),
|
||||
start: expect.any(Object),
|
||||
type: 'Sketch',
|
||||
units: {
|
||||
type: 'Mm',
|
||||
},
|
||||
tags: {
|
||||
p: {
|
||||
__meta: [
|
||||
{
|
||||
sourceRange: [114, 116, 0],
|
||||
},
|
||||
],
|
||||
type: 'TagIdentifier',
|
||||
value: 'p',
|
||||
info: expect.any(Object),
|
||||
},
|
||||
},
|
||||
paths: [
|
||||
{
|
||||
type: 'ToPoint',
|
||||
from: [0, 0],
|
||||
to: [-2.5, 0],
|
||||
tag: null,
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [69, 89, 0],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'ToPoint',
|
||||
from: [-2.5, 0],
|
||||
to: [0, 10],
|
||||
tag: {
|
||||
end: 116,
|
||||
start: 114,
|
||||
type: 'TagDeclarator',
|
||||
value: 'p',
|
||||
},
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [95, 117, 0],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'ToPoint',
|
||||
from: [0, 10],
|
||||
to: [2.5, 0],
|
||||
tag: null,
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [123, 142, 0],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
height: 2,
|
||||
startCapId: expect.any(String),
|
||||
endCapId: expect.any(String),
|
||||
units: {
|
||||
type: 'Mm',
|
||||
},
|
||||
__meta: [{ sourceRange: [38, 63, 0] }],
|
||||
},
|
||||
height: 2,
|
||||
startCapId: expect.any(String),
|
||||
endCapId: expect.any(String),
|
||||
__meta: [{ sourceRange: [38, 63, 0] }],
|
||||
},
|
||||
{
|
||||
type: 'Solid',
|
||||
id: expect.any(String),
|
||||
value: [
|
||||
{
|
||||
type: 'extrudePlane',
|
||||
faceId: expect.any(String),
|
||||
tag: null,
|
||||
id: expect.any(String),
|
||||
sourceRange: [373, 393, 0],
|
||||
},
|
||||
{
|
||||
type: 'extrudePlane',
|
||||
faceId: expect.any(String),
|
||||
tag: {
|
||||
end: 419,
|
||||
start: 417,
|
||||
type: 'TagDeclarator',
|
||||
value: 'o',
|
||||
},
|
||||
id: expect.any(String),
|
||||
sourceRange: [399, 420, 0],
|
||||
},
|
||||
{
|
||||
type: 'extrudePlane',
|
||||
faceId: expect.any(String),
|
||||
tag: null,
|
||||
id: expect.any(String),
|
||||
sourceRange: [426, 445, 0],
|
||||
},
|
||||
],
|
||||
sketch: {
|
||||
value: {
|
||||
type: 'Solid',
|
||||
id: expect.any(String),
|
||||
__meta: expect.any(Array),
|
||||
on: expect.any(Object),
|
||||
start: expect.any(Object),
|
||||
type: 'Sketch',
|
||||
tags: {
|
||||
o: {
|
||||
__meta: [
|
||||
{
|
||||
sourceRange: [417, 419, 0],
|
||||
},
|
||||
],
|
||||
type: 'TagIdentifier',
|
||||
value: 'o',
|
||||
info: expect.any(Object),
|
||||
},
|
||||
},
|
||||
paths: [
|
||||
value: [
|
||||
{
|
||||
type: 'ToPoint',
|
||||
from: [0, 0],
|
||||
to: [-2.5, 0],
|
||||
type: 'extrudePlane',
|
||||
faceId: expect.any(String),
|
||||
tag: null,
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [373, 393, 0],
|
||||
},
|
||||
id: expect.any(String),
|
||||
sourceRange: [373, 393, 0],
|
||||
},
|
||||
{
|
||||
type: 'ToPoint',
|
||||
from: [-2.5, 0],
|
||||
to: [0, 3],
|
||||
type: 'extrudePlane',
|
||||
faceId: expect.any(String),
|
||||
tag: {
|
||||
end: 419,
|
||||
start: 417,
|
||||
type: 'TagDeclarator',
|
||||
value: 'o',
|
||||
},
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [399, 420, 0],
|
||||
},
|
||||
id: expect.any(String),
|
||||
sourceRange: [399, 420, 0],
|
||||
},
|
||||
{
|
||||
type: 'ToPoint',
|
||||
from: [0, 3],
|
||||
to: [2.5, 0],
|
||||
type: 'extrudePlane',
|
||||
faceId: expect.any(String),
|
||||
tag: null,
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [426, 445, 0],
|
||||
},
|
||||
id: expect.any(String),
|
||||
sourceRange: [426, 445, 0],
|
||||
},
|
||||
],
|
||||
sketch: {
|
||||
id: expect.any(String),
|
||||
units: {
|
||||
type: 'Mm',
|
||||
},
|
||||
__meta: expect.any(Array),
|
||||
on: expect.any(Object),
|
||||
start: expect.any(Object),
|
||||
type: 'Sketch',
|
||||
tags: {
|
||||
o: {
|
||||
__meta: [
|
||||
{
|
||||
sourceRange: [417, 419, 0],
|
||||
},
|
||||
],
|
||||
type: 'TagIdentifier',
|
||||
value: 'o',
|
||||
info: expect.any(Object),
|
||||
},
|
||||
},
|
||||
paths: [
|
||||
{
|
||||
type: 'ToPoint',
|
||||
from: [0, 0],
|
||||
to: [-2.5, 0],
|
||||
tag: null,
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [373, 393, 0],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'ToPoint',
|
||||
from: [-2.5, 0],
|
||||
to: [0, 3],
|
||||
tag: {
|
||||
end: 419,
|
||||
start: 417,
|
||||
type: 'TagDeclarator',
|
||||
value: 'o',
|
||||
},
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [399, 420, 0],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'ToPoint',
|
||||
from: [0, 3],
|
||||
to: [2.5, 0],
|
||||
tag: null,
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [426, 445, 0],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
height: 2,
|
||||
startCapId: expect.any(String),
|
||||
endCapId: expect.any(String),
|
||||
__meta: [{ sourceRange: [342, 367, 0] }],
|
||||
units: {
|
||||
type: 'Mm',
|
||||
},
|
||||
},
|
||||
height: 2,
|
||||
startCapId: expect.any(String),
|
||||
endCapId: expect.any(String),
|
||||
__meta: [{ sourceRange: [342, 367, 0] }],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { kclErrorsToDiagnostics, KCLError } from './errors'
|
||||
import { defaultArtifactGraph, topLevelRange } from 'lang/wasm'
|
||||
|
||||
describe('test kclErrToDiagnostic', () => {
|
||||
it('converts KCL errors to CodeMirror diagnostics', () => {
|
||||
@ -8,18 +9,20 @@ describe('test kclErrToDiagnostic', () => {
|
||||
message: '',
|
||||
kind: 'semantic',
|
||||
msg: 'Semantic error',
|
||||
sourceRange: [0, 1, true],
|
||||
sourceRange: topLevelRange(0, 1),
|
||||
operations: [],
|
||||
artifactCommands: [],
|
||||
artifactGraph: defaultArtifactGraph(),
|
||||
},
|
||||
{
|
||||
name: '',
|
||||
message: '',
|
||||
kind: 'type',
|
||||
msg: 'Type error',
|
||||
sourceRange: [4, 5, true],
|
||||
sourceRange: topLevelRange(4, 5),
|
||||
operations: [],
|
||||
artifactCommands: [],
|
||||
artifactGraph: defaultArtifactGraph(),
|
||||
},
|
||||
]
|
||||
const diagnostics = kclErrorsToDiagnostics(errors)
|
||||
|
||||
@ -5,7 +5,13 @@ import { posToOffset } from '@kittycad/codemirror-lsp-client'
|
||||
import { Diagnostic as LspDiagnostic } from 'vscode-languageserver-protocol'
|
||||
import { Text } from '@codemirror/state'
|
||||
import { EditorView } from 'codemirror'
|
||||
import { ArtifactCommand, SourceRange } from 'lang/wasm'
|
||||
import {
|
||||
ArtifactCommand,
|
||||
ArtifactGraph,
|
||||
defaultArtifactGraph,
|
||||
isTopLevelModule,
|
||||
SourceRange,
|
||||
} from 'lang/wasm'
|
||||
import { Operation } from 'wasm-lib/kcl/bindings/Operation'
|
||||
|
||||
type ExtractKind<T> = T extends { kind: infer K } ? K : never
|
||||
@ -15,13 +21,15 @@ export class KCLError extends Error {
|
||||
msg: string
|
||||
operations: Operation[]
|
||||
artifactCommands: ArtifactCommand[]
|
||||
artifactGraph: ArtifactGraph
|
||||
|
||||
constructor(
|
||||
kind: ExtractKind<RustKclError> | 'name',
|
||||
msg: string,
|
||||
sourceRange: SourceRange,
|
||||
operations: Operation[],
|
||||
artifactCommands: ArtifactCommand[]
|
||||
artifactCommands: ArtifactCommand[],
|
||||
artifactGraph: ArtifactGraph
|
||||
) {
|
||||
super()
|
||||
this.kind = kind
|
||||
@ -29,6 +37,7 @@ export class KCLError extends Error {
|
||||
this.sourceRange = sourceRange
|
||||
this.operations = operations
|
||||
this.artifactCommands = artifactCommands
|
||||
this.artifactGraph = artifactGraph
|
||||
Object.setPrototypeOf(this, KCLError.prototype)
|
||||
}
|
||||
}
|
||||
@ -38,9 +47,17 @@ export class KCLLexicalError extends KCLError {
|
||||
msg: string,
|
||||
sourceRange: SourceRange,
|
||||
operations: Operation[],
|
||||
artifactCommands: ArtifactCommand[]
|
||||
artifactCommands: ArtifactCommand[],
|
||||
artifactGraph: ArtifactGraph
|
||||
) {
|
||||
super('lexical', msg, sourceRange, operations, artifactCommands)
|
||||
super(
|
||||
'lexical',
|
||||
msg,
|
||||
sourceRange,
|
||||
operations,
|
||||
artifactCommands,
|
||||
artifactGraph
|
||||
)
|
||||
Object.setPrototypeOf(this, KCLSyntaxError.prototype)
|
||||
}
|
||||
}
|
||||
@ -50,9 +67,17 @@ export class KCLInternalError extends KCLError {
|
||||
msg: string,
|
||||
sourceRange: SourceRange,
|
||||
operations: Operation[],
|
||||
artifactCommands: ArtifactCommand[]
|
||||
artifactCommands: ArtifactCommand[],
|
||||
artifactGraph: ArtifactGraph
|
||||
) {
|
||||
super('internal', msg, sourceRange, operations, artifactCommands)
|
||||
super(
|
||||
'internal',
|
||||
msg,
|
||||
sourceRange,
|
||||
operations,
|
||||
artifactCommands,
|
||||
artifactGraph
|
||||
)
|
||||
Object.setPrototypeOf(this, KCLSyntaxError.prototype)
|
||||
}
|
||||
}
|
||||
@ -62,9 +87,17 @@ export class KCLSyntaxError extends KCLError {
|
||||
msg: string,
|
||||
sourceRange: SourceRange,
|
||||
operations: Operation[],
|
||||
artifactCommands: ArtifactCommand[]
|
||||
artifactCommands: ArtifactCommand[],
|
||||
artifactGraph: ArtifactGraph
|
||||
) {
|
||||
super('syntax', msg, sourceRange, operations, artifactCommands)
|
||||
super(
|
||||
'syntax',
|
||||
msg,
|
||||
sourceRange,
|
||||
operations,
|
||||
artifactCommands,
|
||||
artifactGraph
|
||||
)
|
||||
Object.setPrototypeOf(this, KCLSyntaxError.prototype)
|
||||
}
|
||||
}
|
||||
@ -74,9 +107,17 @@ export class KCLSemanticError extends KCLError {
|
||||
msg: string,
|
||||
sourceRange: SourceRange,
|
||||
operations: Operation[],
|
||||
artifactCommands: ArtifactCommand[]
|
||||
artifactCommands: ArtifactCommand[],
|
||||
artifactGraph: ArtifactGraph
|
||||
) {
|
||||
super('semantic', msg, sourceRange, operations, artifactCommands)
|
||||
super(
|
||||
'semantic',
|
||||
msg,
|
||||
sourceRange,
|
||||
operations,
|
||||
artifactCommands,
|
||||
artifactGraph
|
||||
)
|
||||
Object.setPrototypeOf(this, KCLSemanticError.prototype)
|
||||
}
|
||||
}
|
||||
@ -86,9 +127,10 @@ export class KCLTypeError extends KCLError {
|
||||
msg: string,
|
||||
sourceRange: SourceRange,
|
||||
operations: Operation[],
|
||||
artifactCommands: ArtifactCommand[]
|
||||
artifactCommands: ArtifactCommand[],
|
||||
artifactGraph: ArtifactGraph
|
||||
) {
|
||||
super('type', msg, sourceRange, operations, artifactCommands)
|
||||
super('type', msg, sourceRange, operations, artifactCommands, artifactGraph)
|
||||
Object.setPrototypeOf(this, KCLTypeError.prototype)
|
||||
}
|
||||
}
|
||||
@ -98,9 +140,17 @@ export class KCLUnimplementedError extends KCLError {
|
||||
msg: string,
|
||||
sourceRange: SourceRange,
|
||||
operations: Operation[],
|
||||
artifactCommands: ArtifactCommand[]
|
||||
artifactCommands: ArtifactCommand[],
|
||||
artifactGraph: ArtifactGraph
|
||||
) {
|
||||
super('unimplemented', msg, sourceRange, operations, artifactCommands)
|
||||
super(
|
||||
'unimplemented',
|
||||
msg,
|
||||
sourceRange,
|
||||
operations,
|
||||
artifactCommands,
|
||||
artifactGraph
|
||||
)
|
||||
Object.setPrototypeOf(this, KCLUnimplementedError.prototype)
|
||||
}
|
||||
}
|
||||
@ -110,9 +160,17 @@ export class KCLUnexpectedError extends KCLError {
|
||||
msg: string,
|
||||
sourceRange: SourceRange,
|
||||
operations: Operation[],
|
||||
artifactCommands: ArtifactCommand[]
|
||||
artifactCommands: ArtifactCommand[],
|
||||
artifactGraph: ArtifactGraph
|
||||
) {
|
||||
super('unexpected', msg, sourceRange, operations, artifactCommands)
|
||||
super(
|
||||
'unexpected',
|
||||
msg,
|
||||
sourceRange,
|
||||
operations,
|
||||
artifactCommands,
|
||||
artifactGraph
|
||||
)
|
||||
Object.setPrototypeOf(this, KCLUnexpectedError.prototype)
|
||||
}
|
||||
}
|
||||
@ -122,14 +180,16 @@ export class KCLValueAlreadyDefined extends KCLError {
|
||||
key: string,
|
||||
sourceRange: SourceRange,
|
||||
operations: Operation[],
|
||||
artifactCommands: ArtifactCommand[]
|
||||
artifactCommands: ArtifactCommand[],
|
||||
artifactGraph: ArtifactGraph
|
||||
) {
|
||||
super(
|
||||
'name',
|
||||
`Key ${key} was already defined elsewhere`,
|
||||
sourceRange,
|
||||
operations,
|
||||
artifactCommands
|
||||
artifactCommands,
|
||||
artifactGraph
|
||||
)
|
||||
Object.setPrototypeOf(this, KCLValueAlreadyDefined.prototype)
|
||||
}
|
||||
@ -140,14 +200,16 @@ export class KCLUndefinedValueError extends KCLError {
|
||||
key: string,
|
||||
sourceRange: SourceRange,
|
||||
operations: Operation[],
|
||||
artifactCommands: ArtifactCommand[]
|
||||
artifactCommands: ArtifactCommand[],
|
||||
artifactGraph: ArtifactGraph
|
||||
) {
|
||||
super(
|
||||
'name',
|
||||
`Key ${key} has not been defined`,
|
||||
sourceRange,
|
||||
operations,
|
||||
artifactCommands
|
||||
artifactCommands,
|
||||
artifactGraph
|
||||
)
|
||||
Object.setPrototypeOf(this, KCLUndefinedValueError.prototype)
|
||||
}
|
||||
@ -167,9 +229,10 @@ export function lspDiagnosticsToKclErrors(
|
||||
new KCLError(
|
||||
'unexpected',
|
||||
message,
|
||||
[posToOffset(doc, range.start)!, posToOffset(doc, range.end)!, true],
|
||||
[posToOffset(doc, range.start)!, posToOffset(doc, range.end)!, 0],
|
||||
[],
|
||||
[]
|
||||
[],
|
||||
defaultArtifactGraph()
|
||||
)
|
||||
)
|
||||
.sort((a, b) => {
|
||||
@ -193,7 +256,7 @@ export function kclErrorsToDiagnostics(
|
||||
errors: KCLError[]
|
||||
): CodeMirrorDiagnostic[] {
|
||||
return errors
|
||||
?.filter((err) => err.sourceRange[2])
|
||||
?.filter((err) => isTopLevelModule(err.sourceRange))
|
||||
.map((err) => {
|
||||
return {
|
||||
from: err.sourceRange[0],
|
||||
@ -208,7 +271,7 @@ export function complilationErrorsToDiagnostics(
|
||||
errors: CompilationError[]
|
||||
): CodeMirrorDiagnostic[] {
|
||||
return errors
|
||||
?.filter((err) => err.sourceRange[2] === 0)
|
||||
?.filter((err) => isTopLevelModule(err.sourceRange))
|
||||
.map((err) => {
|
||||
let severity: any = 'error'
|
||||
if (err.severity === 'Warning') {
|
||||
|
||||
@ -6,6 +6,8 @@ import {
|
||||
Sketch,
|
||||
initPromise,
|
||||
sketchFromKclValue,
|
||||
defaultArtifactGraph,
|
||||
topLevelRange,
|
||||
} from './wasm'
|
||||
import { enginelessExecutor } from '../lib/testHelpers'
|
||||
import { KCLError } from './errors'
|
||||
@ -219,6 +221,9 @@ const newVar = myVar + 1`
|
||||
},
|
||||
],
|
||||
id: expect.any(String),
|
||||
units: {
|
||||
type: 'Mm',
|
||||
},
|
||||
__meta: [{ sourceRange: [39, 63, 0] }],
|
||||
},
|
||||
})
|
||||
@ -480,9 +485,10 @@ const theExtrude = startSketchOn('XY')
|
||||
new KCLError(
|
||||
'undefined_value',
|
||||
'memory item key `myVarZ` is not defined',
|
||||
[129, 135, true],
|
||||
topLevelRange(129, 135),
|
||||
[],
|
||||
[]
|
||||
[],
|
||||
defaultArtifactGraph()
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
@ -1,5 +1,12 @@
|
||||
import { getNodePathFromSourceRange, getNodeFromPath } from './queryAst'
|
||||
import { Identifier, assertParse, initPromise, Parameter } from './wasm'
|
||||
import {
|
||||
Identifier,
|
||||
assertParse,
|
||||
initPromise,
|
||||
Parameter,
|
||||
SourceRange,
|
||||
topLevelRange,
|
||||
} from './wasm'
|
||||
import { err } from 'lib/trap'
|
||||
|
||||
beforeAll(async () => {
|
||||
@ -17,11 +24,10 @@ const sk3 = startSketchAt([0, 0])
|
||||
`
|
||||
const subStr = 'lineTo([3, 4], %, $yo)'
|
||||
const lineToSubstringIndex = code.indexOf(subStr)
|
||||
const sourceRange: [number, number, boolean] = [
|
||||
const sourceRange = topLevelRange(
|
||||
lineToSubstringIndex,
|
||||
lineToSubstringIndex + subStr.length,
|
||||
true,
|
||||
]
|
||||
lineToSubstringIndex + subStr.length
|
||||
)
|
||||
|
||||
const ast = assertParse(code)
|
||||
const nodePath = getNodePathFromSourceRange(ast, sourceRange)
|
||||
@ -29,7 +35,7 @@ const sk3 = startSketchAt([0, 0])
|
||||
if (err(_node)) throw _node
|
||||
const { node } = _node
|
||||
|
||||
expect([node.start, node.end, true]).toEqual(sourceRange)
|
||||
expect(topLevelRange(node.start, node.end)).toEqual(sourceRange)
|
||||
expect(node.type).toBe('CallExpression')
|
||||
})
|
||||
it('gets path right for function definition params', () => {
|
||||
@ -45,11 +51,7 @@ const sk3 = startSketchAt([0, 0])
|
||||
const b1 = cube([0,0], 10)`
|
||||
const subStr = 'pos, scale'
|
||||
const subStrIndex = code.indexOf(subStr)
|
||||
const sourceRange: [number, number, boolean] = [
|
||||
subStrIndex,
|
||||
subStrIndex + 'pos'.length,
|
||||
true,
|
||||
]
|
||||
const sourceRange = topLevelRange(subStrIndex, subStrIndex + 'pos'.length)
|
||||
|
||||
const ast = assertParse(code)
|
||||
const nodePath = getNodePathFromSourceRange(ast, sourceRange)
|
||||
@ -81,11 +83,7 @@ const b1 = cube([0,0], 10)`
|
||||
const b1 = cube([0,0], 10)`
|
||||
const subStr = 'scale, 0'
|
||||
const subStrIndex = code.indexOf(subStr)
|
||||
const sourceRange: [number, number, boolean] = [
|
||||
subStrIndex,
|
||||
subStrIndex + 'scale'.length,
|
||||
true,
|
||||
]
|
||||
const sourceRange = topLevelRange(subStrIndex, subStrIndex + 'scale'.length)
|
||||
|
||||
const ast = assertParse(code)
|
||||
const nodePath = getNodePathFromSourceRange(ast, sourceRange)
|
||||
|
||||
@ -1,4 +1,11 @@
|
||||
import { assertParse, recast, initPromise, Identifier } from './wasm'
|
||||
import {
|
||||
assertParse,
|
||||
recast,
|
||||
initPromise,
|
||||
Identifier,
|
||||
SourceRange,
|
||||
topLevelRange,
|
||||
} from './wasm'
|
||||
import {
|
||||
createLiteral,
|
||||
createIdentifier,
|
||||
@ -32,7 +39,7 @@ describe('Testing createLiteral', () => {
|
||||
it('should create a literal', () => {
|
||||
const result = createLiteral(5)
|
||||
expect(result.type).toBe('Literal')
|
||||
expect(result.value).toBe(5)
|
||||
expect((result as any).value.value).toBe(5)
|
||||
})
|
||||
})
|
||||
describe('Testing createIdentifier', () => {
|
||||
@ -49,7 +56,7 @@ describe('Testing createCallExpression', () => {
|
||||
expect(result.callee.type).toBe('Identifier')
|
||||
expect(result.callee.name).toBe('myFunc')
|
||||
expect(result.arguments[0].type).toBe('Literal')
|
||||
expect((result.arguments[0] as any).value).toBe(5)
|
||||
expect((result.arguments[0] as any).value.value).toBe(5)
|
||||
})
|
||||
})
|
||||
describe('Testing createObjectExpression', () => {
|
||||
@ -61,7 +68,7 @@ describe('Testing createObjectExpression', () => {
|
||||
expect(result.properties[0].type).toBe('ObjectProperty')
|
||||
expect(result.properties[0].key.name).toBe('myProp')
|
||||
expect(result.properties[0].value.type).toBe('Literal')
|
||||
expect((result.properties[0].value as any).value).toBe(5)
|
||||
expect((result.properties[0].value as any).value.value).toBe(5)
|
||||
})
|
||||
})
|
||||
describe('Testing createArrayExpression', () => {
|
||||
@ -69,7 +76,7 @@ describe('Testing createArrayExpression', () => {
|
||||
const result = createArrayExpression([createLiteral(5)])
|
||||
expect(result.type).toBe('ArrayExpression')
|
||||
expect(result.elements[0].type).toBe('Literal')
|
||||
expect((result.elements[0] as any).value).toBe(5)
|
||||
expect((result.elements[0] as any).value.value).toBe(5)
|
||||
})
|
||||
})
|
||||
describe('Testing createPipeSubstitution', () => {
|
||||
@ -86,7 +93,7 @@ describe('Testing createVariableDeclaration', () => {
|
||||
expect(result.declaration.id.type).toBe('Identifier')
|
||||
expect(result.declaration.id.name).toBe('myVar')
|
||||
expect(result.declaration.init.type).toBe('Literal')
|
||||
expect((result.declaration.init as any).value).toBe(5)
|
||||
expect((result.declaration.init as any).value.value).toBe(5)
|
||||
})
|
||||
})
|
||||
describe('Testing createPipeExpression', () => {
|
||||
@ -94,7 +101,7 @@ describe('Testing createPipeExpression', () => {
|
||||
const result = createPipeExpression([createLiteral(5)])
|
||||
expect(result.type).toBe('PipeExpression')
|
||||
expect(result.body[0].type).toBe('Literal')
|
||||
expect((result.body[0] as any).value).toBe(5)
|
||||
expect((result.body[0] as any).value.value).toBe(5)
|
||||
})
|
||||
})
|
||||
|
||||
@ -148,11 +155,7 @@ function giveSketchFnCallTagTestHelper(
|
||||
// making it more of an integration test, but easier to read the test intention is the goal
|
||||
const ast = assertParse(code)
|
||||
const start = code.indexOf(searchStr)
|
||||
const range: [number, number, boolean] = [
|
||||
start,
|
||||
start + searchStr.length,
|
||||
true,
|
||||
]
|
||||
const range = topLevelRange(start, start + searchStr.length)
|
||||
const sketchRes = giveSketchFnCallTag(ast, range)
|
||||
if (err(sketchRes)) throw sketchRes
|
||||
const { modifiedAst, tag, isTagExisting } = sketchRes
|
||||
@ -230,7 +233,7 @@ yo2 = hmm([identifierGuy + 5])`
|
||||
const { modifiedAst } = moveValueIntoNewVariable(
|
||||
ast,
|
||||
execState.memory,
|
||||
[startIndex, startIndex, true],
|
||||
topLevelRange(startIndex, startIndex),
|
||||
'newVar'
|
||||
)
|
||||
const newCode = recast(modifiedAst)
|
||||
@ -244,7 +247,7 @@ yo2 = hmm([identifierGuy + 5])`
|
||||
const { modifiedAst } = moveValueIntoNewVariable(
|
||||
ast,
|
||||
execState.memory,
|
||||
[startIndex, startIndex, true],
|
||||
topLevelRange(startIndex, startIndex),
|
||||
'newVar'
|
||||
)
|
||||
const newCode = recast(modifiedAst)
|
||||
@ -258,7 +261,7 @@ yo2 = hmm([identifierGuy + 5])`
|
||||
const { modifiedAst } = moveValueIntoNewVariable(
|
||||
ast,
|
||||
execState.memory,
|
||||
[startIndex, startIndex, true],
|
||||
topLevelRange(startIndex, startIndex),
|
||||
'newVar'
|
||||
)
|
||||
const newCode = recast(modifiedAst)
|
||||
@ -272,7 +275,7 @@ yo2 = hmm([identifierGuy + 5])`
|
||||
const { modifiedAst } = moveValueIntoNewVariable(
|
||||
ast,
|
||||
execState.memory,
|
||||
[startIndex, startIndex, true],
|
||||
topLevelRange(startIndex, startIndex),
|
||||
'newVar'
|
||||
)
|
||||
const newCode = recast(modifiedAst)
|
||||
@ -286,7 +289,7 @@ yo2 = hmm([identifierGuy + 5])`
|
||||
const { modifiedAst } = moveValueIntoNewVariable(
|
||||
ast,
|
||||
execState.memory,
|
||||
[startIndex, startIndex, true],
|
||||
topLevelRange(startIndex, startIndex),
|
||||
'newVar'
|
||||
)
|
||||
const newCode = recast(modifiedAst)
|
||||
@ -306,18 +309,16 @@ describe('testing sketchOnExtrudedFace', () => {
|
||||
const ast = assertParse(code)
|
||||
|
||||
const segmentSnippet = `line([9.7, 9.19], %)`
|
||||
const segmentRange: [number, number, boolean] = [
|
||||
const segmentRange = topLevelRange(
|
||||
code.indexOf(segmentSnippet),
|
||||
code.indexOf(segmentSnippet) + segmentSnippet.length,
|
||||
true,
|
||||
]
|
||||
code.indexOf(segmentSnippet) + segmentSnippet.length
|
||||
)
|
||||
const segmentPathToNode = getNodePathFromSourceRange(ast, segmentRange)
|
||||
const extrudeSnippet = `extrude(5 + 7, %)`
|
||||
const extrudeRange: [number, number, boolean] = [
|
||||
const extrudeRange = topLevelRange(
|
||||
code.indexOf(extrudeSnippet),
|
||||
code.indexOf(extrudeSnippet) + extrudeSnippet.length,
|
||||
true,
|
||||
]
|
||||
code.indexOf(extrudeSnippet) + extrudeSnippet.length
|
||||
)
|
||||
const extrudePathToNode = getNodePathFromSourceRange(ast, extrudeRange)
|
||||
|
||||
const extruded = sketchOnExtrudedFace(
|
||||
@ -346,18 +347,16 @@ sketch001 = startSketchOn(part001, seg01)`)
|
||||
|> extrude(5 + 7, %)`
|
||||
const ast = assertParse(code)
|
||||
const segmentSnippet = `close(%)`
|
||||
const segmentRange: [number, number, boolean] = [
|
||||
const segmentRange = topLevelRange(
|
||||
code.indexOf(segmentSnippet),
|
||||
code.indexOf(segmentSnippet) + segmentSnippet.length,
|
||||
true,
|
||||
]
|
||||
code.indexOf(segmentSnippet) + segmentSnippet.length
|
||||
)
|
||||
const segmentPathToNode = getNodePathFromSourceRange(ast, segmentRange)
|
||||
const extrudeSnippet = `extrude(5 + 7, %)`
|
||||
const extrudeRange: [number, number, boolean] = [
|
||||
const extrudeRange = topLevelRange(
|
||||
code.indexOf(extrudeSnippet),
|
||||
code.indexOf(extrudeSnippet) + extrudeSnippet.length,
|
||||
true,
|
||||
]
|
||||
code.indexOf(extrudeSnippet) + extrudeSnippet.length
|
||||
)
|
||||
const extrudePathToNode = getNodePathFromSourceRange(ast, extrudeRange)
|
||||
|
||||
const extruded = sketchOnExtrudedFace(
|
||||
@ -386,18 +385,16 @@ sketch001 = startSketchOn(part001, seg01)`)
|
||||
|> extrude(5 + 7, %)`
|
||||
const ast = assertParse(code)
|
||||
const sketchSnippet = `startProfileAt([3.58, 2.06], %)`
|
||||
const sketchRange: [number, number, boolean] = [
|
||||
const sketchRange = topLevelRange(
|
||||
code.indexOf(sketchSnippet),
|
||||
code.indexOf(sketchSnippet) + sketchSnippet.length,
|
||||
true,
|
||||
]
|
||||
code.indexOf(sketchSnippet) + sketchSnippet.length
|
||||
)
|
||||
const sketchPathToNode = getNodePathFromSourceRange(ast, sketchRange)
|
||||
const extrudeSnippet = `extrude(5 + 7, %)`
|
||||
const extrudeRange: [number, number, boolean] = [
|
||||
const extrudeRange = topLevelRange(
|
||||
code.indexOf(extrudeSnippet),
|
||||
code.indexOf(extrudeSnippet) + extrudeSnippet.length,
|
||||
true,
|
||||
]
|
||||
code.indexOf(extrudeSnippet) + extrudeSnippet.length
|
||||
)
|
||||
const extrudePathToNode = getNodePathFromSourceRange(ast, extrudeRange)
|
||||
|
||||
const extruded = sketchOnExtrudedFace(
|
||||
@ -435,18 +432,16 @@ sketch001 = startSketchOn(part001, 'END')`)
|
||||
part001 = extrude(5 + 7, sketch001)`
|
||||
const ast = assertParse(code)
|
||||
const segmentSnippet = `line([4.99, -0.46], %)`
|
||||
const segmentRange: [number, number, boolean] = [
|
||||
const segmentRange = topLevelRange(
|
||||
code.indexOf(segmentSnippet),
|
||||
code.indexOf(segmentSnippet) + segmentSnippet.length,
|
||||
true,
|
||||
]
|
||||
code.indexOf(segmentSnippet) + segmentSnippet.length
|
||||
)
|
||||
const segmentPathToNode = getNodePathFromSourceRange(ast, segmentRange)
|
||||
const extrudeSnippet = `extrude(5 + 7, sketch001)`
|
||||
const extrudeRange: [number, number, boolean] = [
|
||||
const extrudeRange = topLevelRange(
|
||||
code.indexOf(extrudeSnippet),
|
||||
code.indexOf(extrudeSnippet) + extrudeSnippet.length,
|
||||
true,
|
||||
]
|
||||
code.indexOf(extrudeSnippet) + extrudeSnippet.length
|
||||
)
|
||||
const extrudePathToNode = getNodePathFromSourceRange(ast, extrudeRange)
|
||||
|
||||
const updatedAst = sketchOnExtrudedFace(
|
||||
@ -471,11 +466,10 @@ describe('Testing deleteSegmentFromPipeExpression', () => {
|
||||
const ast = assertParse(code)
|
||||
const execState = await enginelessExecutor(ast)
|
||||
const lineOfInterest = 'line([306.21, 198.85], %, $a)'
|
||||
const range: [number, number, boolean] = [
|
||||
const range = topLevelRange(
|
||||
code.indexOf(lineOfInterest),
|
||||
code.indexOf(lineOfInterest) + lineOfInterest.length,
|
||||
true,
|
||||
]
|
||||
code.indexOf(lineOfInterest) + lineOfInterest.length
|
||||
)
|
||||
const pathToNode = getNodePathFromSourceRange(ast, range)
|
||||
const modifiedAst = deleteSegmentFromPipeExpression(
|
||||
[],
|
||||
@ -549,11 +543,10 @@ ${!replace1 ? ` |> ${line}\n` : ''} |> angledLine([-65, ${
|
||||
const ast = assertParse(code)
|
||||
const execState = await enginelessExecutor(ast)
|
||||
const lineOfInterest = line
|
||||
const range: [number, number, boolean] = [
|
||||
const range = topLevelRange(
|
||||
code.indexOf(lineOfInterest),
|
||||
code.indexOf(lineOfInterest) + lineOfInterest.length,
|
||||
true,
|
||||
]
|
||||
code.indexOf(lineOfInterest) + lineOfInterest.length
|
||||
)
|
||||
const pathToNode = getNodePathFromSourceRange(ast, range)
|
||||
const dependentSegments = findUsesOfTagInPipe(ast, pathToNode)
|
||||
const modifiedAst = deleteSegmentFromPipeExpression(
|
||||
@ -638,11 +631,10 @@ describe('Testing removeSingleConstraintInfo', () => {
|
||||
|
||||
const execState = await enginelessExecutor(ast)
|
||||
const lineOfInterest = expectedFinish.split('(')[0] + '('
|
||||
const range: [number, number, boolean] = [
|
||||
const range = topLevelRange(
|
||||
code.indexOf(lineOfInterest) + 1,
|
||||
code.indexOf(lineOfInterest) + lineOfInterest.length,
|
||||
true,
|
||||
]
|
||||
code.indexOf(lineOfInterest) + lineOfInterest.length
|
||||
)
|
||||
const pathToNode = getNodePathFromSourceRange(ast, range)
|
||||
let argPosition: SimplifiedArgDetails
|
||||
if (key === 'arrayIndex' && typeof value === 'number') {
|
||||
@ -692,11 +684,10 @@ describe('Testing removeSingleConstraintInfo', () => {
|
||||
|
||||
const execState = await enginelessExecutor(ast)
|
||||
const lineOfInterest = expectedFinish.split('(')[0] + '('
|
||||
const range: [number, number, boolean] = [
|
||||
const range = topLevelRange(
|
||||
code.indexOf(lineOfInterest) + 1,
|
||||
code.indexOf(lineOfInterest) + lineOfInterest.length,
|
||||
true,
|
||||
]
|
||||
code.indexOf(lineOfInterest) + lineOfInterest.length
|
||||
)
|
||||
let argPosition: SimplifiedArgDetails
|
||||
if (key === 'arrayIndex' && typeof value === 'number') {
|
||||
argPosition = {
|
||||
@ -889,11 +880,10 @@ sketch002 = startSketchOn({
|
||||
const execState = await enginelessExecutor(ast)
|
||||
|
||||
// deleteFromSelection
|
||||
const range: [number, number, boolean] = [
|
||||
const range = topLevelRange(
|
||||
codeBefore.indexOf(lineOfInterest),
|
||||
codeBefore.indexOf(lineOfInterest) + lineOfInterest.length,
|
||||
true,
|
||||
]
|
||||
codeBefore.indexOf(lineOfInterest) + lineOfInterest.length
|
||||
)
|
||||
const artifact = { type } as Artifact
|
||||
const newAst = await deleteFromSelection(
|
||||
ast,
|
||||
|
||||
@ -743,14 +743,18 @@ export function splitPathAtPipeExpression(pathToNode: PathToNode): {
|
||||
return splitPathAtPipeExpression(pathToNode.slice(0, -1))
|
||||
}
|
||||
|
||||
export function createLiteral(value: LiteralValue): Node<Literal> {
|
||||
export function createLiteral(value: LiteralValue | number): Node<Literal> {
|
||||
const raw = `${value}`
|
||||
if (typeof value === 'number') {
|
||||
value = { value, suffix: 'None' }
|
||||
}
|
||||
return {
|
||||
type: 'Literal',
|
||||
start: 0,
|
||||
end: 0,
|
||||
moduleId: 0,
|
||||
value,
|
||||
raw: `${value}`,
|
||||
raw,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -8,6 +8,8 @@ import {
|
||||
makeDefaultPlanes,
|
||||
PipeExpression,
|
||||
VariableDeclarator,
|
||||
SourceRange,
|
||||
topLevelRange,
|
||||
} from '../wasm'
|
||||
import {
|
||||
EdgeTreatmentType,
|
||||
@ -77,11 +79,10 @@ const runGetPathToExtrudeForSegmentSelectionTest = async (
|
||||
code: string,
|
||||
expectedExtrudeSnippet: string
|
||||
): CallExpression | PipeExpression | Error {
|
||||
const extrudeRange: [number, number, boolean] = [
|
||||
const extrudeRange = topLevelRange(
|
||||
code.indexOf(expectedExtrudeSnippet),
|
||||
code.indexOf(expectedExtrudeSnippet) + expectedExtrudeSnippet.length,
|
||||
true,
|
||||
]
|
||||
code.indexOf(expectedExtrudeSnippet) + expectedExtrudeSnippet.length
|
||||
)
|
||||
const expectedExtrudePath = getNodePathFromSourceRange(ast, extrudeRange)
|
||||
const expectedExtrudeNodeResult = getNodeFromPath<
|
||||
VariableDeclarator | CallExpression
|
||||
@ -112,11 +113,10 @@ const runGetPathToExtrudeForSegmentSelectionTest = async (
|
||||
const ast = assertParse(code)
|
||||
|
||||
// selection
|
||||
const segmentRange: [number, number, boolean] = [
|
||||
const segmentRange = topLevelRange(
|
||||
code.indexOf(selectedSegmentSnippet),
|
||||
code.indexOf(selectedSegmentSnippet) + selectedSegmentSnippet.length,
|
||||
true,
|
||||
]
|
||||
code.indexOf(selectedSegmentSnippet) + selectedSegmentSnippet.length
|
||||
)
|
||||
const selection: Selection = {
|
||||
codeRef: codeRefFromRange(segmentRange, ast),
|
||||
}
|
||||
@ -260,12 +260,12 @@ const runModifyAstCloneWithEdgeTreatmentAndTag = async (
|
||||
const ast = assertParse(code)
|
||||
|
||||
// selection
|
||||
const segmentRanges: Array<[number, number, boolean]> = selectionSnippets.map(
|
||||
(selectionSnippet) => [
|
||||
code.indexOf(selectionSnippet),
|
||||
code.indexOf(selectionSnippet) + selectionSnippet.length,
|
||||
true,
|
||||
]
|
||||
const segmentRanges: Array<SourceRange> = selectionSnippets.map(
|
||||
(selectionSnippet) =>
|
||||
topLevelRange(
|
||||
code.indexOf(selectionSnippet),
|
||||
code.indexOf(selectionSnippet) + selectionSnippet.length
|
||||
)
|
||||
)
|
||||
|
||||
// executeAst
|
||||
@ -596,11 +596,10 @@ extrude001 = extrude(-5, sketch001)
|
||||
it('should correctly identify getOppositeEdge and baseEdge edges', () => {
|
||||
const ast = assertParse(code)
|
||||
const lineOfInterest = `line([7.11, 3.48], %, $seg01)`
|
||||
const range: [number, number, boolean] = [
|
||||
const range = topLevelRange(
|
||||
code.indexOf(lineOfInterest),
|
||||
code.indexOf(lineOfInterest) + lineOfInterest.length,
|
||||
true,
|
||||
]
|
||||
code.indexOf(lineOfInterest) + lineOfInterest.length
|
||||
)
|
||||
const pathToNode = getNodePathFromSourceRange(ast, range)
|
||||
if (err(pathToNode)) return
|
||||
const callExp = getNodeFromPath<CallExpression>(
|
||||
@ -615,11 +614,10 @@ extrude001 = extrude(-5, sketch001)
|
||||
it('should correctly identify getPreviousAdjacentEdge edges', () => {
|
||||
const ast = assertParse(code)
|
||||
const lineOfInterest = `line([-6.37, 3.88], %, $seg02)`
|
||||
const range: [number, number, boolean] = [
|
||||
const range = topLevelRange(
|
||||
code.indexOf(lineOfInterest),
|
||||
code.indexOf(lineOfInterest) + lineOfInterest.length,
|
||||
true,
|
||||
]
|
||||
code.indexOf(lineOfInterest) + lineOfInterest.length
|
||||
)
|
||||
const pathToNode = getNodePathFromSourceRange(ast, range)
|
||||
if (err(pathToNode)) return
|
||||
const callExp = getNodeFromPath<CallExpression>(
|
||||
@ -634,11 +632,10 @@ extrude001 = extrude(-5, sketch001)
|
||||
it('should correctly identify no edges', () => {
|
||||
const ast = assertParse(code)
|
||||
const lineOfInterest = `line([-3.29, -13.85], %)`
|
||||
const range: [number, number, boolean] = [
|
||||
const range = topLevelRange(
|
||||
code.indexOf(lineOfInterest),
|
||||
code.indexOf(lineOfInterest) + lineOfInterest.length,
|
||||
true,
|
||||
]
|
||||
code.indexOf(lineOfInterest) + lineOfInterest.length
|
||||
)
|
||||
const pathToNode = getNodePathFromSourceRange(ast, range)
|
||||
if (err(pathToNode)) return
|
||||
const callExp = getNodeFromPath<CallExpression>(
|
||||
@ -660,13 +657,12 @@ describe('Testing button states', () => {
|
||||
) => {
|
||||
const ast = assertParse(code)
|
||||
|
||||
const range: [number, number, boolean] = segmentSnippet
|
||||
? [
|
||||
const range = segmentSnippet
|
||||
? topLevelRange(
|
||||
code.indexOf(segmentSnippet),
|
||||
code.indexOf(segmentSnippet) + segmentSnippet.length,
|
||||
true,
|
||||
]
|
||||
: [ast.end, ast.end, true] // empty line in the end of the code
|
||||
code.indexOf(segmentSnippet) + segmentSnippet.length
|
||||
)
|
||||
: topLevelRange(ast.end, ast.end) // empty line in the end of the code
|
||||
|
||||
const selectionRanges: Selections = {
|
||||
graphSelections: [
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import {
|
||||
ArtifactGraph,
|
||||
CallExpression,
|
||||
Expr,
|
||||
Identifier,
|
||||
@ -31,11 +32,7 @@ import {
|
||||
import { err, trap } from 'lib/trap'
|
||||
import { Selection, Selections } from 'lib/selections'
|
||||
import { KclCommandValue } from 'lib/commandTypes'
|
||||
import {
|
||||
Artifact,
|
||||
ArtifactGraph,
|
||||
getSweepFromSuspectedPath,
|
||||
} from 'lang/std/artifactGraph'
|
||||
import { Artifact, getSweepFromSuspectedPath } from 'lang/std/artifactGraph'
|
||||
import {
|
||||
kclManager,
|
||||
engineCommandManager,
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import { ArtifactGraph } from 'lang/std/artifactGraph'
|
||||
import { Selections } from 'lib/selections'
|
||||
import { Expr } from 'wasm-lib/kcl/bindings/Expr'
|
||||
import { Program } from 'wasm-lib/kcl/bindings/Program'
|
||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
import { PathToNode, VariableDeclarator } from 'lang/wasm'
|
||||
import { ArtifactGraph, PathToNode, VariableDeclarator } from 'lang/wasm'
|
||||
import {
|
||||
getPathToExtrudeForSegmentSelection,
|
||||
mutateAstWithTagForSketchSegment,
|
||||
|
||||
@ -4,6 +4,7 @@ import {
|
||||
initPromise,
|
||||
PathToNode,
|
||||
Identifier,
|
||||
topLevelRange,
|
||||
} from './wasm'
|
||||
import {
|
||||
findAllPreviousVariables,
|
||||
@ -57,7 +58,7 @@ variableBelowShouldNotBeIncluded = 3
|
||||
const { variables, bodyPath, insertIndex } = findAllPreviousVariables(
|
||||
ast,
|
||||
execState.memory,
|
||||
[rangeStart, rangeStart, true]
|
||||
topLevelRange(rangeStart, rangeStart)
|
||||
)
|
||||
expect(variables).toEqual([
|
||||
{ key: 'baseThick', value: 1 },
|
||||
@ -87,7 +88,10 @@ yo2 = hmm([identifierGuy + 5])`
|
||||
it('find a safe binaryExpression', () => {
|
||||
const ast = assertParse(code)
|
||||
const rangeStart = code.indexOf('100 + 100') + 2
|
||||
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart, true])
|
||||
const result = isNodeSafeToReplace(
|
||||
ast,
|
||||
topLevelRange(rangeStart, rangeStart)
|
||||
)
|
||||
if (err(result)) throw result
|
||||
expect(result.isSafe).toBe(true)
|
||||
expect(result.value?.type).toBe('BinaryExpression')
|
||||
@ -100,7 +104,10 @@ yo2 = hmm([identifierGuy + 5])`
|
||||
it('find a safe Identifier', () => {
|
||||
const ast = assertParse(code)
|
||||
const rangeStart = code.indexOf('abc')
|
||||
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart, true])
|
||||
const result = isNodeSafeToReplace(
|
||||
ast,
|
||||
topLevelRange(rangeStart, rangeStart)
|
||||
)
|
||||
if (err(result)) throw result
|
||||
expect(result.isSafe).toBe(true)
|
||||
expect(result.value?.type).toBe('Identifier')
|
||||
@ -109,7 +116,10 @@ yo2 = hmm([identifierGuy + 5])`
|
||||
it('find a safe CallExpression', () => {
|
||||
const ast = assertParse(code)
|
||||
const rangeStart = code.indexOf('def')
|
||||
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart, true])
|
||||
const result = isNodeSafeToReplace(
|
||||
ast,
|
||||
topLevelRange(rangeStart, rangeStart)
|
||||
)
|
||||
if (err(result)) throw result
|
||||
expect(result.isSafe).toBe(true)
|
||||
expect(result.value?.type).toBe('CallExpression')
|
||||
@ -122,7 +132,7 @@ yo2 = hmm([identifierGuy + 5])`
|
||||
it('find an UNsafe CallExpression, as it has a PipeSubstitution', () => {
|
||||
const ast = assertParse(code)
|
||||
const rangeStart = code.indexOf('ghi')
|
||||
const range: [number, number, boolean] = [rangeStart, rangeStart, true]
|
||||
const range = topLevelRange(rangeStart, rangeStart)
|
||||
const result = isNodeSafeToReplace(ast, range)
|
||||
if (err(result)) throw result
|
||||
expect(result.isSafe).toBe(false)
|
||||
@ -132,7 +142,10 @@ yo2 = hmm([identifierGuy + 5])`
|
||||
it('find an UNsafe Identifier, as it is a callee', () => {
|
||||
const ast = assertParse(code)
|
||||
const rangeStart = code.indexOf('ine([2.8,')
|
||||
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart, true])
|
||||
const result = isNodeSafeToReplace(
|
||||
ast,
|
||||
topLevelRange(rangeStart, rangeStart)
|
||||
)
|
||||
if (err(result)) throw result
|
||||
expect(result.isSafe).toBe(false)
|
||||
expect(result.value?.type).toBe('CallExpression')
|
||||
@ -143,7 +156,10 @@ yo2 = hmm([identifierGuy + 5])`
|
||||
it("find a safe BinaryExpression that's assigned to a variable", () => {
|
||||
const ast = assertParse(code)
|
||||
const rangeStart = code.indexOf('5 + 6') + 1
|
||||
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart, true])
|
||||
const result = isNodeSafeToReplace(
|
||||
ast,
|
||||
topLevelRange(rangeStart, rangeStart)
|
||||
)
|
||||
if (err(result)) throw result
|
||||
expect(result.isSafe).toBe(true)
|
||||
expect(result.value?.type).toBe('BinaryExpression')
|
||||
@ -156,7 +172,10 @@ yo2 = hmm([identifierGuy + 5])`
|
||||
it('find a safe BinaryExpression that has a CallExpression within', () => {
|
||||
const ast = assertParse(code)
|
||||
const rangeStart = code.indexOf('jkl') + 1
|
||||
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart, true])
|
||||
const result = isNodeSafeToReplace(
|
||||
ast,
|
||||
topLevelRange(rangeStart, rangeStart)
|
||||
)
|
||||
if (err(result)) throw result
|
||||
expect(result.isSafe).toBe(true)
|
||||
expect(result.value?.type).toBe('BinaryExpression')
|
||||
@ -173,7 +192,10 @@ yo2 = hmm([identifierGuy + 5])`
|
||||
const ast = assertParse(code)
|
||||
|
||||
const rangeStart = code.indexOf('identifierGuy') + 1
|
||||
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart, true])
|
||||
const result = isNodeSafeToReplace(
|
||||
ast,
|
||||
topLevelRange(rangeStart, rangeStart)
|
||||
)
|
||||
if (err(result)) throw result
|
||||
|
||||
expect(result.isSafe).toBe(true)
|
||||
@ -222,11 +244,10 @@ describe('testing getNodePathFromSourceRange', () => {
|
||||
const sourceIndex = code.indexOf(searchLn) + searchLn.length
|
||||
const ast = assertParse(code)
|
||||
|
||||
const result = getNodePathFromSourceRange(ast, [
|
||||
sourceIndex,
|
||||
sourceIndex,
|
||||
true,
|
||||
])
|
||||
const result = getNodePathFromSourceRange(
|
||||
ast,
|
||||
topLevelRange(sourceIndex, sourceIndex)
|
||||
)
|
||||
expect(result).toEqual([
|
||||
['body', ''],
|
||||
[0, 'index'],
|
||||
@ -241,11 +262,10 @@ describe('testing getNodePathFromSourceRange', () => {
|
||||
const sourceIndex = code.indexOf(searchLn) + searchLn.length
|
||||
const ast = assertParse(code)
|
||||
|
||||
const result = getNodePathFromSourceRange(ast, [
|
||||
sourceIndex,
|
||||
sourceIndex,
|
||||
true,
|
||||
])
|
||||
const result = getNodePathFromSourceRange(
|
||||
ast,
|
||||
topLevelRange(sourceIndex, sourceIndex)
|
||||
)
|
||||
const expected = [
|
||||
['body', ''],
|
||||
[0, 'index'],
|
||||
@ -257,18 +277,16 @@ describe('testing getNodePathFromSourceRange', () => {
|
||||
expect(result).toEqual(expected)
|
||||
// expect similar result for start of line
|
||||
const startSourceIndex = code.indexOf(searchLn)
|
||||
const startResult = getNodePathFromSourceRange(ast, [
|
||||
startSourceIndex,
|
||||
startSourceIndex,
|
||||
true,
|
||||
])
|
||||
const startResult = getNodePathFromSourceRange(
|
||||
ast,
|
||||
topLevelRange(startSourceIndex, startSourceIndex)
|
||||
)
|
||||
expect(startResult).toEqual([...expected, ['callee', 'CallExpression']])
|
||||
// expect similar result when whole line is selected
|
||||
const selectWholeThing = getNodePathFromSourceRange(ast, [
|
||||
startSourceIndex,
|
||||
sourceIndex,
|
||||
true,
|
||||
])
|
||||
const selectWholeThing = getNodePathFromSourceRange(
|
||||
ast,
|
||||
topLevelRange(startSourceIndex, sourceIndex)
|
||||
)
|
||||
expect(selectWholeThing).toEqual(expected)
|
||||
})
|
||||
|
||||
@ -283,11 +301,10 @@ describe('testing getNodePathFromSourceRange', () => {
|
||||
const sourceIndex = code.indexOf(searchLn)
|
||||
const ast = assertParse(code)
|
||||
|
||||
const result = getNodePathFromSourceRange(ast, [
|
||||
sourceIndex,
|
||||
sourceIndex,
|
||||
true,
|
||||
])
|
||||
const result = getNodePathFromSourceRange(
|
||||
ast,
|
||||
topLevelRange(sourceIndex, sourceIndex)
|
||||
)
|
||||
expect(result).toEqual([
|
||||
['body', ''],
|
||||
[1, 'index'],
|
||||
@ -313,11 +330,10 @@ describe('testing getNodePathFromSourceRange', () => {
|
||||
const sourceIndex = code.indexOf(searchLn)
|
||||
const ast = assertParse(code)
|
||||
|
||||
const result = getNodePathFromSourceRange(ast, [
|
||||
sourceIndex,
|
||||
sourceIndex,
|
||||
true,
|
||||
])
|
||||
const result = getNodePathFromSourceRange(
|
||||
ast,
|
||||
topLevelRange(sourceIndex, sourceIndex)
|
||||
)
|
||||
expect(result).toEqual([
|
||||
['body', ''],
|
||||
[1, 'index'],
|
||||
@ -341,11 +357,10 @@ describe('testing getNodePathFromSourceRange', () => {
|
||||
const sourceIndex = code.indexOf(searchLn)
|
||||
const ast = assertParse(code)
|
||||
|
||||
const result = getNodePathFromSourceRange(ast, [
|
||||
sourceIndex,
|
||||
sourceIndex,
|
||||
true,
|
||||
])
|
||||
const result = getNodePathFromSourceRange(
|
||||
ast,
|
||||
topLevelRange(sourceIndex, sourceIndex)
|
||||
)
|
||||
expect(result).toEqual([
|
||||
['body', ''],
|
||||
[0, 'index'],
|
||||
@ -375,7 +390,7 @@ part001 = startSketchAt([-1.41, 3.46])
|
||||
const result = hasExtrudeSketch({
|
||||
ast,
|
||||
selection: {
|
||||
codeRef: codeRefFromRange([100, 101, true], ast),
|
||||
codeRef: codeRefFromRange(topLevelRange(100, 101), ast),
|
||||
},
|
||||
programMemory: execState.memory,
|
||||
})
|
||||
@ -395,7 +410,7 @@ part001 = startSketchAt([-1.41, 3.46])
|
||||
const result = hasExtrudeSketch({
|
||||
ast,
|
||||
selection: {
|
||||
codeRef: codeRefFromRange([100, 101, true], ast),
|
||||
codeRef: codeRefFromRange(topLevelRange(100, 101), ast),
|
||||
},
|
||||
programMemory: execState.memory,
|
||||
})
|
||||
@ -409,7 +424,7 @@ part001 = startSketchAt([-1.41, 3.46])
|
||||
const result = hasExtrudeSketch({
|
||||
ast,
|
||||
selection: {
|
||||
codeRef: codeRefFromRange([10, 11, true], ast),
|
||||
codeRef: codeRefFromRange(topLevelRange(10, 11), ast),
|
||||
},
|
||||
programMemory: execState.memory,
|
||||
})
|
||||
@ -431,11 +446,10 @@ describe('Testing findUsesOfTagInPipe', () => {
|
||||
const lineOfInterest = `198.85], %, $seg01`
|
||||
const characterIndex =
|
||||
exampleCode.indexOf(lineOfInterest) + lineOfInterest.length
|
||||
const pathToNode = getNodePathFromSourceRange(ast, [
|
||||
characterIndex,
|
||||
characterIndex,
|
||||
true,
|
||||
])
|
||||
const pathToNode = getNodePathFromSourceRange(
|
||||
ast,
|
||||
topLevelRange(characterIndex, characterIndex)
|
||||
)
|
||||
const result = findUsesOfTagInPipe(ast, pathToNode)
|
||||
expect(result).toHaveLength(2)
|
||||
result.forEach((range) => {
|
||||
@ -448,11 +462,10 @@ describe('Testing findUsesOfTagInPipe', () => {
|
||||
const lineOfInterest = `line([306.21, 198.82], %)`
|
||||
const characterIndex =
|
||||
exampleCode.indexOf(lineOfInterest) + lineOfInterest.length
|
||||
const pathToNode = getNodePathFromSourceRange(ast, [
|
||||
characterIndex,
|
||||
characterIndex,
|
||||
true,
|
||||
])
|
||||
const pathToNode = getNodePathFromSourceRange(
|
||||
ast,
|
||||
topLevelRange(characterIndex, characterIndex)
|
||||
)
|
||||
const result = findUsesOfTagInPipe(ast, pathToNode)
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
@ -498,7 +511,10 @@ sketch003 = startSketchOn(extrude001, 'END')
|
||||
exampleCode.indexOf(lineOfInterest) + lineOfInterest.length
|
||||
const extruded = hasSketchPipeBeenExtruded(
|
||||
{
|
||||
codeRef: codeRefFromRange([characterIndex, characterIndex, true], ast),
|
||||
codeRef: codeRefFromRange(
|
||||
topLevelRange(characterIndex, characterIndex),
|
||||
ast
|
||||
),
|
||||
},
|
||||
ast
|
||||
)
|
||||
@ -511,7 +527,10 @@ sketch003 = startSketchOn(extrude001, 'END')
|
||||
exampleCode.indexOf(lineOfInterest) + lineOfInterest.length
|
||||
const extruded = hasSketchPipeBeenExtruded(
|
||||
{
|
||||
codeRef: codeRefFromRange([characterIndex, characterIndex, true], ast),
|
||||
codeRef: codeRefFromRange(
|
||||
topLevelRange(characterIndex, characterIndex),
|
||||
ast
|
||||
),
|
||||
},
|
||||
ast
|
||||
)
|
||||
@ -524,7 +543,10 @@ sketch003 = startSketchOn(extrude001, 'END')
|
||||
exampleCode.indexOf(lineOfInterest) + lineOfInterest.length
|
||||
const extruded = hasSketchPipeBeenExtruded(
|
||||
{
|
||||
codeRef: codeRefFromRange([characterIndex, characterIndex, true], ast),
|
||||
codeRef: codeRefFromRange(
|
||||
topLevelRange(characterIndex, characterIndex),
|
||||
ast
|
||||
),
|
||||
},
|
||||
ast
|
||||
)
|
||||
@ -638,7 +660,7 @@ myNestedVar = [
|
||||
enter: (node, path) => {
|
||||
if (
|
||||
node.type === 'Literal' &&
|
||||
String(node.value) === literalOfInterest
|
||||
String((node as any).value.value) === literalOfInterest
|
||||
) {
|
||||
pathToNode = path
|
||||
} else if (
|
||||
@ -651,11 +673,10 @@ myNestedVar = [
|
||||
})
|
||||
|
||||
const literalIndex = code.indexOf(literalOfInterest)
|
||||
const pathToNode2 = getNodePathFromSourceRange(ast, [
|
||||
literalIndex + 2,
|
||||
literalIndex + 2,
|
||||
true,
|
||||
])
|
||||
const pathToNode2 = getNodePathFromSourceRange(
|
||||
ast,
|
||||
topLevelRange(literalIndex + 2, literalIndex + 2)
|
||||
)
|
||||
expect(pathToNode).toEqual(pathToNode2)
|
||||
})
|
||||
})
|
||||
|
||||
@ -2,6 +2,7 @@ import { ToolTip } from 'lang/langHelpers'
|
||||
import { Selection, Selections } from 'lib/selections'
|
||||
import {
|
||||
ArrayExpression,
|
||||
ArtifactGraph,
|
||||
BinaryExpression,
|
||||
CallExpression,
|
||||
Expr,
|
||||
@ -16,8 +17,8 @@ import {
|
||||
sketchFromKclValue,
|
||||
sketchFromKclValueOptional,
|
||||
SourceRange,
|
||||
sourceRangeFromRust,
|
||||
SyntaxType,
|
||||
topLevelRange,
|
||||
VariableDeclaration,
|
||||
VariableDeclarator,
|
||||
} from './wasm'
|
||||
@ -32,7 +33,7 @@ import {
|
||||
import { err, Reason } from 'lib/trap'
|
||||
import { ImportStatement } from 'wasm-lib/kcl/bindings/ImportStatement'
|
||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
import { ArtifactGraph, codeRefFromRange } from './std/artifactGraph'
|
||||
import { codeRefFromRange } from './std/artifactGraph'
|
||||
|
||||
/**
|
||||
* Retrieves a node from a given path within a Program node structure, optionally stopping at a specified node type.
|
||||
@ -716,16 +717,6 @@ function isTypeInArrayExp(
|
||||
return node.elements.some((el) => isTypeInValue(el, syntaxType))
|
||||
}
|
||||
|
||||
export function isValueZero(val?: Expr): boolean {
|
||||
return (
|
||||
(val?.type === 'Literal' && Number(val.value) === 0) ||
|
||||
(val?.type === 'UnaryExpression' &&
|
||||
val.operator === '-' &&
|
||||
val.argument.type === 'Literal' &&
|
||||
Number(val.argument.value) === 0)
|
||||
)
|
||||
}
|
||||
|
||||
export function isLinesParallelAndConstrained(
|
||||
ast: Program,
|
||||
artifactGraph: ArtifactGraph,
|
||||
@ -819,7 +810,7 @@ export function isLinesParallelAndConstrained(
|
||||
return {
|
||||
isParallelAndConstrained,
|
||||
selection: {
|
||||
codeRef: codeRefFromRange(sourceRangeFromRust(prevSourceRange), ast),
|
||||
codeRef: codeRefFromRange(prevSourceRange, ast),
|
||||
artifact: artifactGraph.get(prevSegment.__geoMeta.id),
|
||||
},
|
||||
}
|
||||
@ -937,7 +928,7 @@ export function findUsesOfTagInPipe(
|
||||
const tagArgValue =
|
||||
tagArg.type === 'TagDeclarator' ? String(tagArg.value) : tagArg.name
|
||||
if (tagArgValue === tag)
|
||||
dependentRanges.push([node.start, node.end, true])
|
||||
dependentRanges.push(topLevelRange(node.start, node.end))
|
||||
},
|
||||
})
|
||||
return dependentRanges
|
||||
|
||||
@ -1,559 +0,0 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`testing createArtifactGraph > code with an extrusion, fillet and sketch of face: > snapshot of the artifactGraph 1`] = `
|
||||
Map {
|
||||
"UUID-0" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
12,
|
||||
31,
|
||||
true,
|
||||
],
|
||||
},
|
||||
"id": "UUID",
|
||||
"pathIds": [
|
||||
"UUID",
|
||||
],
|
||||
"type": "plane",
|
||||
},
|
||||
"UUID-1" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
37,
|
||||
64,
|
||||
true,
|
||||
],
|
||||
},
|
||||
"id": "UUID",
|
||||
"planeId": "UUID",
|
||||
"segIds": [
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
],
|
||||
"solid2dId": "UUID",
|
||||
"sweepId": "UUID",
|
||||
"type": "path",
|
||||
},
|
||||
"UUID-2" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
70,
|
||||
86,
|
||||
true,
|
||||
],
|
||||
},
|
||||
"edgeIds": [
|
||||
"UUID",
|
||||
"UUID",
|
||||
],
|
||||
"id": "UUID",
|
||||
"pathId": "UUID",
|
||||
"surfaceId": "UUID",
|
||||
"type": "segment",
|
||||
},
|
||||
"UUID-3" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
92,
|
||||
119,
|
||||
true,
|
||||
],
|
||||
},
|
||||
"edgeCutId": "UUID",
|
||||
"edgeIds": [
|
||||
"UUID",
|
||||
"UUID",
|
||||
],
|
||||
"id": "UUID",
|
||||
"pathId": "UUID",
|
||||
"surfaceId": "UUID",
|
||||
"type": "segment",
|
||||
},
|
||||
"UUID-4" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
125,
|
||||
150,
|
||||
true,
|
||||
],
|
||||
},
|
||||
"edgeIds": [
|
||||
"UUID",
|
||||
"UUID",
|
||||
],
|
||||
"id": "UUID",
|
||||
"pathId": "UUID",
|
||||
"surfaceId": "UUID",
|
||||
"type": "segment",
|
||||
},
|
||||
"UUID-5" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
156,
|
||||
203,
|
||||
true,
|
||||
],
|
||||
},
|
||||
"edgeIds": [
|
||||
"UUID",
|
||||
"UUID",
|
||||
],
|
||||
"id": "UUID",
|
||||
"pathId": "UUID",
|
||||
"surfaceId": "UUID",
|
||||
"type": "segment",
|
||||
},
|
||||
"UUID-6" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
209,
|
||||
217,
|
||||
true,
|
||||
],
|
||||
},
|
||||
"edgeIds": [],
|
||||
"id": "UUID",
|
||||
"pathId": "UUID",
|
||||
"type": "segment",
|
||||
},
|
||||
"UUID-7" => {
|
||||
"id": "UUID",
|
||||
"pathId": "UUID",
|
||||
"type": "solid2D",
|
||||
},
|
||||
"UUID-8" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
231,
|
||||
254,
|
||||
true,
|
||||
],
|
||||
},
|
||||
"edgeIds": [
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
],
|
||||
"id": "UUID",
|
||||
"pathId": "UUID",
|
||||
"subType": "extrusion",
|
||||
"surfaceIds": [
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
],
|
||||
"type": "sweep",
|
||||
},
|
||||
"UUID-9" => {
|
||||
"edgeCutEdgeIds": [],
|
||||
"id": "UUID",
|
||||
"pathIds": [],
|
||||
"segId": "UUID",
|
||||
"sweepId": "UUID",
|
||||
"type": "wall",
|
||||
},
|
||||
"UUID-10" => {
|
||||
"edgeCutEdgeIds": [],
|
||||
"id": "UUID",
|
||||
"pathIds": [
|
||||
"UUID",
|
||||
],
|
||||
"segId": "UUID",
|
||||
"sweepId": "UUID",
|
||||
"type": "wall",
|
||||
},
|
||||
"UUID-11" => {
|
||||
"edgeCutEdgeIds": [],
|
||||
"id": "UUID",
|
||||
"pathIds": [],
|
||||
"segId": "UUID",
|
||||
"sweepId": "UUID",
|
||||
"type": "wall",
|
||||
},
|
||||
"UUID-12" => {
|
||||
"edgeCutEdgeIds": [],
|
||||
"id": "UUID",
|
||||
"pathIds": [],
|
||||
"segId": "UUID",
|
||||
"sweepId": "UUID",
|
||||
"type": "wall",
|
||||
},
|
||||
"UUID-13" => {
|
||||
"edgeCutEdgeIds": [],
|
||||
"id": "UUID",
|
||||
"pathIds": [],
|
||||
"subType": "start",
|
||||
"sweepId": "UUID",
|
||||
"type": "cap",
|
||||
},
|
||||
"UUID-14" => {
|
||||
"edgeCutEdgeIds": [],
|
||||
"id": "UUID",
|
||||
"pathIds": [],
|
||||
"subType": "end",
|
||||
"sweepId": "UUID",
|
||||
"type": "cap",
|
||||
},
|
||||
"UUID-15" => {
|
||||
"id": "UUID",
|
||||
"segId": "UUID",
|
||||
"subType": "opposite",
|
||||
"sweepId": "UUID",
|
||||
"type": "sweepEdge",
|
||||
},
|
||||
"UUID-16" => {
|
||||
"id": "UUID",
|
||||
"segId": "UUID",
|
||||
"subType": "adjacent",
|
||||
"sweepId": "UUID",
|
||||
"type": "sweepEdge",
|
||||
},
|
||||
"UUID-17" => {
|
||||
"id": "UUID",
|
||||
"segId": "UUID",
|
||||
"subType": "opposite",
|
||||
"sweepId": "UUID",
|
||||
"type": "sweepEdge",
|
||||
},
|
||||
"UUID-18" => {
|
||||
"id": "UUID",
|
||||
"segId": "UUID",
|
||||
"subType": "adjacent",
|
||||
"sweepId": "UUID",
|
||||
"type": "sweepEdge",
|
||||
},
|
||||
"UUID-19" => {
|
||||
"id": "UUID",
|
||||
"segId": "UUID",
|
||||
"subType": "opposite",
|
||||
"sweepId": "UUID",
|
||||
"type": "sweepEdge",
|
||||
},
|
||||
"UUID-20" => {
|
||||
"id": "UUID",
|
||||
"segId": "UUID",
|
||||
"subType": "adjacent",
|
||||
"sweepId": "UUID",
|
||||
"type": "sweepEdge",
|
||||
},
|
||||
"UUID-21" => {
|
||||
"id": "UUID",
|
||||
"segId": "UUID",
|
||||
"subType": "opposite",
|
||||
"sweepId": "UUID",
|
||||
"type": "sweepEdge",
|
||||
},
|
||||
"UUID-22" => {
|
||||
"id": "UUID",
|
||||
"segId": "UUID",
|
||||
"subType": "adjacent",
|
||||
"sweepId": "UUID",
|
||||
"type": "sweepEdge",
|
||||
},
|
||||
"UUID-23" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
260,
|
||||
299,
|
||||
true,
|
||||
],
|
||||
},
|
||||
"consumedEdgeId": "UUID",
|
||||
"edgeIds": [],
|
||||
"id": "UUID",
|
||||
"subType": "fillet",
|
||||
"type": "edgeCut",
|
||||
},
|
||||
"UUID-24" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
350,
|
||||
377,
|
||||
true,
|
||||
],
|
||||
},
|
||||
"id": "UUID",
|
||||
"planeId": "UUID",
|
||||
"segIds": [
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
],
|
||||
"solid2dId": "UUID",
|
||||
"sweepId": "UUID",
|
||||
"type": "path",
|
||||
},
|
||||
"UUID-25" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
383,
|
||||
398,
|
||||
true,
|
||||
],
|
||||
},
|
||||
"edgeIds": [
|
||||
"UUID",
|
||||
"UUID",
|
||||
],
|
||||
"id": "UUID",
|
||||
"pathId": "UUID",
|
||||
"surfaceId": "UUID",
|
||||
"type": "segment",
|
||||
},
|
||||
"UUID-26" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
404,
|
||||
420,
|
||||
true,
|
||||
],
|
||||
},
|
||||
"edgeIds": [
|
||||
"UUID",
|
||||
"UUID",
|
||||
],
|
||||
"id": "UUID",
|
||||
"pathId": "UUID",
|
||||
"surfaceId": "UUID",
|
||||
"type": "segment",
|
||||
},
|
||||
"UUID-27" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
426,
|
||||
473,
|
||||
true,
|
||||
],
|
||||
},
|
||||
"edgeIds": [
|
||||
"UUID",
|
||||
"UUID",
|
||||
],
|
||||
"id": "UUID",
|
||||
"pathId": "UUID",
|
||||
"surfaceId": "UUID",
|
||||
"type": "segment",
|
||||
},
|
||||
"UUID-28" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
479,
|
||||
487,
|
||||
true,
|
||||
],
|
||||
},
|
||||
"edgeIds": [],
|
||||
"id": "UUID",
|
||||
"pathId": "UUID",
|
||||
"type": "segment",
|
||||
},
|
||||
"UUID-29" => {
|
||||
"id": "UUID",
|
||||
"pathId": "UUID",
|
||||
"type": "solid2D",
|
||||
},
|
||||
"UUID-30" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
501,
|
||||
522,
|
||||
true,
|
||||
],
|
||||
},
|
||||
"edgeIds": [
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
],
|
||||
"id": "UUID",
|
||||
"pathId": "UUID",
|
||||
"subType": "extrusion",
|
||||
"surfaceIds": [
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
],
|
||||
"type": "sweep",
|
||||
},
|
||||
"UUID-31" => {
|
||||
"edgeCutEdgeIds": [],
|
||||
"id": "UUID",
|
||||
"pathIds": [],
|
||||
"segId": "UUID",
|
||||
"sweepId": "UUID",
|
||||
"type": "wall",
|
||||
},
|
||||
"UUID-32" => {
|
||||
"edgeCutEdgeIds": [],
|
||||
"id": "UUID",
|
||||
"pathIds": [],
|
||||
"segId": "UUID",
|
||||
"sweepId": "UUID",
|
||||
"type": "wall",
|
||||
},
|
||||
"UUID-33" => {
|
||||
"edgeCutEdgeIds": [],
|
||||
"id": "UUID",
|
||||
"pathIds": [],
|
||||
"segId": "UUID",
|
||||
"sweepId": "UUID",
|
||||
"type": "wall",
|
||||
},
|
||||
"UUID-34" => {
|
||||
"edgeCutEdgeIds": [],
|
||||
"id": "UUID",
|
||||
"pathIds": [],
|
||||
"subType": "end",
|
||||
"sweepId": "UUID",
|
||||
"type": "cap",
|
||||
},
|
||||
"UUID-35" => {
|
||||
"id": "UUID",
|
||||
"segId": "UUID",
|
||||
"subType": "opposite",
|
||||
"sweepId": "UUID",
|
||||
"type": "sweepEdge",
|
||||
},
|
||||
"UUID-36" => {
|
||||
"id": "UUID",
|
||||
"segId": "UUID",
|
||||
"subType": "adjacent",
|
||||
"sweepId": "UUID",
|
||||
"type": "sweepEdge",
|
||||
},
|
||||
"UUID-37" => {
|
||||
"id": "UUID",
|
||||
"segId": "UUID",
|
||||
"subType": "opposite",
|
||||
"sweepId": "UUID",
|
||||
"type": "sweepEdge",
|
||||
},
|
||||
"UUID-38" => {
|
||||
"id": "UUID",
|
||||
"segId": "UUID",
|
||||
"subType": "adjacent",
|
||||
"sweepId": "UUID",
|
||||
"type": "sweepEdge",
|
||||
},
|
||||
"UUID-39" => {
|
||||
"id": "UUID",
|
||||
"segId": "UUID",
|
||||
"subType": "opposite",
|
||||
"sweepId": "UUID",
|
||||
"type": "sweepEdge",
|
||||
},
|
||||
"UUID-40" => {
|
||||
"id": "UUID",
|
||||
"segId": "UUID",
|
||||
"subType": "adjacent",
|
||||
"sweepId": "UUID",
|
||||
"type": "sweepEdge",
|
||||
},
|
||||
}
|
||||
`;
|
||||
@ -1,960 +0,0 @@
|
||||
import {
|
||||
makeDefaultPlanes,
|
||||
assertParse,
|
||||
initPromise,
|
||||
Program,
|
||||
ArtifactCommand,
|
||||
ExecState,
|
||||
} from 'lang/wasm'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import {
|
||||
ResponseMap,
|
||||
createArtifactGraph,
|
||||
filterArtifacts,
|
||||
expandPlane,
|
||||
expandPath,
|
||||
expandSweep,
|
||||
ArtifactGraph,
|
||||
expandSegment,
|
||||
getArtifactsToUpdate,
|
||||
} from './artifactGraph'
|
||||
import { err } from 'lib/trap'
|
||||
import { engineCommandManager, kclManager } from 'lib/singletons'
|
||||
import { VITE_KC_DEV_TOKEN } from 'env'
|
||||
import fsp from 'fs/promises'
|
||||
import fs from 'fs'
|
||||
import { chromium } from 'playwright'
|
||||
import * as d3 from 'd3-force'
|
||||
import path from 'path'
|
||||
import pixelmatch from 'pixelmatch'
|
||||
import { PNG } from 'pngjs'
|
||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
|
||||
/*
|
||||
Note this is an integration test, these tests connect to our real dev server and make websocket commands.
|
||||
It's needed for testing the artifactGraph, as it is tied to the websocket commands.
|
||||
*/
|
||||
|
||||
const pathStart = 'src/lang/std/artifactMapCache'
|
||||
const fullPath = `${pathStart}/artifactMapCache.json`
|
||||
|
||||
const exampleCode1 = `sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-5, -5], %)
|
||||
|> line([0, 10], %)
|
||||
|> line([10.55, 0], %, $seg01)
|
||||
|> line([0, -10], %, $seg02)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
extrude001 = extrude(-10, sketch001)
|
||||
|> fillet({ radius: 5, tags: [seg01] }, %)
|
||||
sketch002 = startSketchOn(extrude001, seg02)
|
||||
|> startProfileAt([-2, -6], %)
|
||||
|> line([2, 3], %)
|
||||
|> line([2, -3], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
extrude002 = extrude(5, sketch002)
|
||||
`
|
||||
|
||||
const exampleCodeNo3D = `sketch003 = startSketchOn('YZ')
|
||||
|> startProfileAt([5.82, 0], %)
|
||||
|> angledLine([180, 11.54], %, $rectangleSegmentA001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001) - 90,
|
||||
8.21
|
||||
], %, $rectangleSegmentB001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001),
|
||||
-segLen(rectangleSegmentA001)
|
||||
], %, $rectangleSegmentC001)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
sketch004 = startSketchOn('-XZ')
|
||||
|> startProfileAt([0, 14.36], %)
|
||||
|> line([15.49, 0.05], %)
|
||||
|> tangentialArcTo([0, 0], %)
|
||||
|> tangentialArcTo([-6.8, 8.17], %)
|
||||
`
|
||||
|
||||
const sketchOnFaceOnFaceEtc = `sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> line([4, 8], %)
|
||||
|> line([5, -8], %, $seg01)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
extrude001 = extrude(6, sketch001)
|
||||
sketch002 = startSketchOn(extrude001, seg01)
|
||||
|> startProfileAt([-0.5, 0.5], %)
|
||||
|> line([2, 5], %)
|
||||
|> line([2, -5], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
extrude002 = extrude(5, sketch002)
|
||||
sketch003 = startSketchOn(extrude002, 'END')
|
||||
|> startProfileAt([1, 1.5], %)
|
||||
|> line([0.5, 2], %, $seg02)
|
||||
|> line([1, -2], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
extrude003 = extrude(4, sketch003)
|
||||
sketch004 = startSketchOn(extrude003, seg02)
|
||||
|> startProfileAt([-3, 14], %)
|
||||
|> line([0.5, 1], %)
|
||||
|> line([0.5, -2], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
extrude004 = extrude(3, sketch004)
|
||||
`
|
||||
const exampleCodeOffsetPlanes = `
|
||||
offsetPlane001 = offsetPlane("XY", 20)
|
||||
offsetPlane002 = offsetPlane("XZ", -50)
|
||||
offsetPlane003 = offsetPlane("YZ", 10)
|
||||
|
||||
sketch002 = startSketchOn(offsetPlane001)
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> line([6.78, 15.01], %)
|
||||
`
|
||||
|
||||
// add more code snippets here and use `getCommands` to get the artifactCommands and responseMap for more tests
|
||||
const codeToWriteCacheFor = {
|
||||
exampleCode1,
|
||||
sketchOnFaceOnFaceEtc,
|
||||
exampleCodeNo3D,
|
||||
exampleCodeOffsetPlanes,
|
||||
} as const
|
||||
|
||||
type CodeKey = keyof typeof codeToWriteCacheFor
|
||||
|
||||
type CacheShape = {
|
||||
[key in CodeKey]: {
|
||||
artifactCommands: ArtifactCommand[]
|
||||
responseMap: ResponseMap
|
||||
execStateArtifacts: ExecState['artifacts']
|
||||
}
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
await initPromise
|
||||
|
||||
// THESE TEST WILL FAIL without VITE_KC_DEV_TOKEN set in .env.development.local
|
||||
await new Promise((resolve) => {
|
||||
engineCommandManager.start({
|
||||
// disableWebRTC: true,
|
||||
token: VITE_KC_DEV_TOKEN,
|
||||
// there does seem to be a minimum resolution, not sure what it is but 256 works ok.
|
||||
width: 256,
|
||||
height: 256,
|
||||
makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager),
|
||||
setMediaStream: () => {},
|
||||
setIsStreamReady: () => {},
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
callbackOnEngineLiteConnect: async () => {
|
||||
const cacheEntries = Object.entries(codeToWriteCacheFor) as [
|
||||
CodeKey,
|
||||
string
|
||||
][]
|
||||
const cacheToWriteToFileTemp: Partial<CacheShape> = {}
|
||||
for (const [codeKey, code] of cacheEntries) {
|
||||
const ast = assertParse(code)
|
||||
await kclManager.executeAst({ ast })
|
||||
|
||||
cacheToWriteToFileTemp[codeKey] = {
|
||||
artifactCommands: kclManager.execState.artifactCommands,
|
||||
responseMap: engineCommandManager.responseMap,
|
||||
execStateArtifacts: kclManager.execState.artifacts,
|
||||
}
|
||||
}
|
||||
const cache = JSON.stringify(cacheToWriteToFileTemp)
|
||||
|
||||
await fsp.mkdir(pathStart, { recursive: true })
|
||||
await fsp.writeFile(fullPath, cache)
|
||||
resolve(true)
|
||||
},
|
||||
})
|
||||
})
|
||||
}, 20_000)
|
||||
|
||||
afterAll(() => {
|
||||
engineCommandManager.tearDown()
|
||||
})
|
||||
|
||||
describe('testing createArtifactGraph', () => {
|
||||
describe('code with offset planes and a sketch:', () => {
|
||||
let ast: Node<Program>
|
||||
let theMap: ReturnType<typeof createArtifactGraph>
|
||||
|
||||
it('setup', () => {
|
||||
// putting this logic in here because describe blocks runs before beforeAll has finished
|
||||
const {
|
||||
artifactCommands,
|
||||
responseMap,
|
||||
ast: _ast,
|
||||
execStateArtifacts,
|
||||
} = getCommands('exampleCodeOffsetPlanes')
|
||||
ast = _ast
|
||||
theMap = createArtifactGraph({
|
||||
artifactCommands,
|
||||
responseMap,
|
||||
ast,
|
||||
execStateArtifacts,
|
||||
})
|
||||
})
|
||||
|
||||
it(`there should be one sketch`, () => {
|
||||
const sketches = [...filterArtifacts({ types: ['path'] }, theMap)].map(
|
||||
(path) => expandPath(path[1], theMap)
|
||||
)
|
||||
expect(sketches).toHaveLength(1)
|
||||
sketches.forEach((path) => {
|
||||
if (err(path)) throw path
|
||||
expect(path.type).toBe('path')
|
||||
})
|
||||
})
|
||||
|
||||
it(`there should be three offsetPlanes`, () => {
|
||||
const offsetPlanes = [
|
||||
...filterArtifacts({ types: ['plane'] }, theMap),
|
||||
].map((plane) => expandPlane(plane[1], theMap))
|
||||
expect(offsetPlanes).toHaveLength(3)
|
||||
offsetPlanes.forEach((path) => {
|
||||
expect(path.type).toBe('plane')
|
||||
})
|
||||
})
|
||||
|
||||
it(`Only one offset plane should have a path`, () => {
|
||||
const offsetPlanes = [
|
||||
...filterArtifacts({ types: ['plane'] }, theMap),
|
||||
].map((plane) => expandPlane(plane[1], theMap))
|
||||
const offsetPlaneWithPaths = offsetPlanes.filter(
|
||||
(plane) => plane.paths.length
|
||||
)
|
||||
expect(offsetPlaneWithPaths).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
describe('code with an extrusion, fillet and sketch of face:', () => {
|
||||
let ast: Node<Program>
|
||||
let theMap: ReturnType<typeof createArtifactGraph>
|
||||
it('setup', () => {
|
||||
// putting this logic in here because describe blocks runs before beforeAll has finished
|
||||
const {
|
||||
artifactCommands,
|
||||
responseMap,
|
||||
ast: _ast,
|
||||
execStateArtifacts,
|
||||
} = getCommands('exampleCode1')
|
||||
ast = _ast
|
||||
theMap = createArtifactGraph({
|
||||
artifactCommands,
|
||||
responseMap,
|
||||
ast,
|
||||
execStateArtifacts,
|
||||
})
|
||||
})
|
||||
|
||||
it('there should be two planes for the extrusion and the sketch on face', () => {
|
||||
const planes = [...filterArtifacts({ types: ['plane'] }, theMap)].map(
|
||||
(plane) => expandPlane(plane[1], theMap)
|
||||
)
|
||||
expect(planes).toHaveLength(1)
|
||||
planes.forEach((path) => {
|
||||
expect(path.type).toBe('plane')
|
||||
})
|
||||
})
|
||||
it('there should be two paths for the extrusion and the sketch on face', () => {
|
||||
const paths = [...filterArtifacts({ types: ['path'] }, theMap)].map(
|
||||
(path) => expandPath(path[1], theMap)
|
||||
)
|
||||
expect(paths).toHaveLength(2)
|
||||
paths.forEach((path) => {
|
||||
if (err(path)) throw path
|
||||
expect(path.type).toBe('path')
|
||||
})
|
||||
})
|
||||
|
||||
it('there should be two extrusions, for the original and the sketchOnFace, the first extrusion should have 6 sides of the cube', () => {
|
||||
const extrusions = [...filterArtifacts({ types: ['sweep'] }, theMap)].map(
|
||||
(extrusion) => expandSweep(extrusion[1], theMap)
|
||||
)
|
||||
expect(extrusions).toHaveLength(2)
|
||||
extrusions.forEach((extrusion, index) => {
|
||||
if (err(extrusion)) throw extrusion
|
||||
expect(extrusion.type).toBe('sweep')
|
||||
const firstExtrusionIsACubeIE6Sides = 6
|
||||
// Each face of the triangular prism (5), but without the bottom cap.
|
||||
// The engine doesn't generate that.
|
||||
const secondExtrusionIsATriangularPrism = 4
|
||||
expect(extrusion.surfaces.length).toBe(
|
||||
!index
|
||||
? firstExtrusionIsACubeIE6Sides
|
||||
: secondExtrusionIsATriangularPrism
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('there should be 5 + 4 segments, 4 (+close) from the first extrusion and 3 (+close) from the second', () => {
|
||||
const segments = [...filterArtifacts({ types: ['segment'] }, theMap)].map(
|
||||
(segment) => expandSegment(segment[1], theMap)
|
||||
)
|
||||
expect(segments).toHaveLength(9)
|
||||
})
|
||||
|
||||
it('snapshot of the artifactGraph', () => {
|
||||
const stableMap = new Map(
|
||||
[...theMap].map(([, artifact], index): [string, any] => {
|
||||
const stableValue: any = {}
|
||||
Object.entries(artifact).forEach(([propName, value]) => {
|
||||
if (
|
||||
propName === 'type' ||
|
||||
propName === 'codeRef' ||
|
||||
propName === 'subType'
|
||||
) {
|
||||
stableValue[propName] = value
|
||||
return
|
||||
}
|
||||
if (Array.isArray(value))
|
||||
stableValue[propName] = value.map(() => 'UUID')
|
||||
if (typeof value === 'string' && value)
|
||||
stableValue[propName] = 'UUID'
|
||||
})
|
||||
return [`UUID-${index}`, stableValue]
|
||||
})
|
||||
)
|
||||
expect(stableMap).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('screenshot graph', async () => {
|
||||
// Ostensibly this takes a screen shot of the graph of the artifactGraph
|
||||
// but it's it also tests that all of the id links are correct because if one
|
||||
// of the edges refers to a non-existent node, the graph will throw.
|
||||
// further more we can check that each edge is bi-directional, if it's not
|
||||
// by checking the arrow heads going both ways, on the graph.
|
||||
await GraphTheGraph(theMap, 2000, 2000, 'exampleCode1.png')
|
||||
}, 20000)
|
||||
})
|
||||
|
||||
describe(`code with sketches but no extrusions or other 3D elements`, () => {
|
||||
let ast: Node<Program>
|
||||
let theMap: ReturnType<typeof createArtifactGraph>
|
||||
it(`setup`, () => {
|
||||
// putting this logic in here because describe blocks runs before beforeAll has finished
|
||||
const {
|
||||
artifactCommands,
|
||||
responseMap,
|
||||
ast: _ast,
|
||||
execStateArtifacts,
|
||||
} = getCommands('exampleCodeNo3D')
|
||||
ast = _ast
|
||||
theMap = createArtifactGraph({
|
||||
artifactCommands,
|
||||
responseMap,
|
||||
ast,
|
||||
execStateArtifacts,
|
||||
})
|
||||
})
|
||||
|
||||
it('there should be two planes, one for each sketch path', () => {
|
||||
const planes = [...filterArtifacts({ types: ['plane'] }, theMap)].map(
|
||||
(plane) => expandPlane(plane[1], theMap)
|
||||
)
|
||||
expect(planes).toHaveLength(2)
|
||||
planes.forEach((path) => {
|
||||
expect(path.type).toBe('plane')
|
||||
})
|
||||
})
|
||||
it('there should be two paths, one on each plane', () => {
|
||||
const paths = [...filterArtifacts({ types: ['path'] }, theMap)].map(
|
||||
(path) => expandPath(path[1], theMap)
|
||||
)
|
||||
expect(paths).toHaveLength(2)
|
||||
paths.forEach((path) => {
|
||||
if (err(path)) throw path
|
||||
expect(path.type).toBe('path')
|
||||
})
|
||||
})
|
||||
|
||||
it(`there should be 1 solid2D, just for the first closed path`, () => {
|
||||
const solid2Ds = [...filterArtifacts({ types: ['solid2D'] }, theMap)]
|
||||
expect(solid2Ds).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('there should be no extrusions', () => {
|
||||
const extrusions = [...filterArtifacts({ types: ['sweep'] }, theMap)].map(
|
||||
(extrusion) => expandSweep(extrusion[1], theMap)
|
||||
)
|
||||
expect(extrusions).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('there should be 8 segments, 4 + 1 (close) from the first sketch and 3 from the second', () => {
|
||||
const segments = [...filterArtifacts({ types: ['segment'] }, theMap)].map(
|
||||
(segment) => expandSegment(segment[1], theMap)
|
||||
)
|
||||
expect(segments).toHaveLength(8)
|
||||
})
|
||||
|
||||
it('screenshot graph', async () => {
|
||||
// Ostensibly this takes a screen shot of the graph of the artifactGraph
|
||||
// but it's it also tests that all of the id links are correct because if one
|
||||
// of the edges refers to a non-existent node, the graph will throw.
|
||||
// further more we can check that each edge is bi-directional, if it's not
|
||||
// by checking the arrow heads going both ways, on the graph.
|
||||
await GraphTheGraph(theMap, 2000, 2000, 'exampleCodeNo3D.png')
|
||||
}, 20000)
|
||||
})
|
||||
})
|
||||
|
||||
describe('capture graph of sketchOnFaceOnFace...', () => {
|
||||
describe('code with an extrusion, fillet and sketch of face:', () => {
|
||||
let ast: Node<Program>
|
||||
let theMap: ReturnType<typeof createArtifactGraph>
|
||||
it('setup', async () => {
|
||||
// putting this logic in here because describe blocks runs before beforeAll has finished
|
||||
const {
|
||||
artifactCommands,
|
||||
responseMap,
|
||||
ast: _ast,
|
||||
execStateArtifacts,
|
||||
} = getCommands('sketchOnFaceOnFaceEtc')
|
||||
ast = _ast
|
||||
theMap = createArtifactGraph({
|
||||
artifactCommands,
|
||||
responseMap,
|
||||
ast,
|
||||
execStateArtifacts,
|
||||
})
|
||||
|
||||
// Ostensibly this takes a screen shot of the graph of the artifactGraph
|
||||
// but it's it also tests that all of the id links are correct because if one
|
||||
// of the edges refers to a non-existent node, the graph will throw.
|
||||
// further more we can check that each edge is bi-directional, if it's not
|
||||
// by checking the arrow heads going both ways, on the graph.
|
||||
await GraphTheGraph(theMap, 3000, 3000, 'sketchOnFaceOnFaceEtc.png')
|
||||
}, 20000)
|
||||
})
|
||||
})
|
||||
|
||||
function getCommands(
|
||||
codeKey: CodeKey
|
||||
): CacheShape[CodeKey] & { ast: Node<Program> } {
|
||||
const ast = assertParse(codeKey)
|
||||
const file = fs.readFileSync(fullPath, 'utf-8')
|
||||
const parsed: CacheShape = JSON.parse(file)
|
||||
// these either already exist from the last run, or were created in
|
||||
const artifactCommands = parsed[codeKey].artifactCommands
|
||||
const responseMap = parsed[codeKey].responseMap
|
||||
const execStateArtifacts = parsed[codeKey].execStateArtifacts
|
||||
return {
|
||||
artifactCommands,
|
||||
responseMap,
|
||||
ast,
|
||||
execStateArtifacts,
|
||||
}
|
||||
}
|
||||
|
||||
async function GraphTheGraph(
|
||||
theMap: ArtifactGraph,
|
||||
sizeX: number,
|
||||
sizeY: number,
|
||||
imageName: string
|
||||
) {
|
||||
const nodes: Array<{ id: string; label: string }> = []
|
||||
const edges: Array<{ source: string; target: string; label: string }> = []
|
||||
let index = 0
|
||||
for (const [commandId, artifact] of theMap) {
|
||||
nodes.push({
|
||||
id: commandId,
|
||||
label: `${artifact.type}-${index++}`,
|
||||
})
|
||||
Object.entries(artifact).forEach(([propName, value]) => {
|
||||
if (
|
||||
propName === 'type' ||
|
||||
propName === 'codeRef' ||
|
||||
propName === 'subType' ||
|
||||
propName === 'id'
|
||||
)
|
||||
return
|
||||
if (Array.isArray(value))
|
||||
value.forEach((v) => {
|
||||
v && edges.push({ source: commandId, target: v, label: propName })
|
||||
})
|
||||
if (typeof value === 'string' && value)
|
||||
edges.push({ source: commandId, target: value, label: propName })
|
||||
})
|
||||
}
|
||||
|
||||
// Create a force simulation to calculate node positions
|
||||
const simulation = d3
|
||||
.forceSimulation(nodes as any)
|
||||
.force(
|
||||
'link',
|
||||
d3
|
||||
.forceLink(edges)
|
||||
.id((d: any) => d.id)
|
||||
.distance(100)
|
||||
)
|
||||
.force('charge', d3.forceManyBody().strength(-300))
|
||||
.force('center', d3.forceCenter(300, 200))
|
||||
.stop()
|
||||
|
||||
// Run the simulation
|
||||
for (let i = 0; i < 300; ++i) simulation.tick()
|
||||
|
||||
// Create traces for Plotly
|
||||
const nodeTrace = {
|
||||
x: nodes.map((node: any) => node.x),
|
||||
y: nodes.map((node: any) => node.y),
|
||||
text: nodes.map((node) => node.label), // Use the custom label
|
||||
mode: 'markers+text',
|
||||
type: 'scatter',
|
||||
marker: { size: 20, color: 'gray' }, // Nodes in gray
|
||||
textfont: { size: 14, color: 'black' }, // Labels in black
|
||||
textposition: 'top center', // Position text on top
|
||||
}
|
||||
|
||||
const edgeTrace = {
|
||||
x: [],
|
||||
y: [],
|
||||
mode: 'lines',
|
||||
type: 'scatter',
|
||||
line: { width: 2, color: 'lightgray' }, // Edges in light gray
|
||||
}
|
||||
|
||||
const annotations: any[] = []
|
||||
|
||||
edges.forEach((edge) => {
|
||||
const sourceNode = nodes.find(
|
||||
(node: any) => node.id === (edge as any).source.id
|
||||
)
|
||||
const targetNode = nodes.find(
|
||||
(node: any) => node.id === (edge as any).target.id
|
||||
)
|
||||
|
||||
// Check if nodes are found
|
||||
if (!sourceNode || !targetNode) {
|
||||
throw new Error(
|
||||
// @ts-ignore
|
||||
`Node not found: ${!sourceNode ? edge.source.id : edge.target.id}`
|
||||
)
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
edgeTrace.x.push(sourceNode.x, targetNode.x, null)
|
||||
// @ts-ignore
|
||||
edgeTrace.y.push(sourceNode.y, targetNode.y, null)
|
||||
|
||||
// Calculate offset for arrowhead
|
||||
const offsetFactor = 0.9 // Adjust this factor to control the offset distance
|
||||
// @ts-ignore
|
||||
const offsetX = (targetNode.x - sourceNode.x) * offsetFactor
|
||||
// @ts-ignore
|
||||
const offsetY = (targetNode.y - sourceNode.y) * offsetFactor
|
||||
|
||||
// Add arrowhead annotation with offset
|
||||
annotations.push({
|
||||
// @ts-ignore
|
||||
ax: sourceNode.x,
|
||||
// @ts-ignore
|
||||
ay: sourceNode.y,
|
||||
// @ts-ignore
|
||||
x: targetNode.x - offsetX,
|
||||
// @ts-ignore
|
||||
y: targetNode.y - offsetY,
|
||||
xref: 'x',
|
||||
yref: 'y',
|
||||
axref: 'x',
|
||||
ayref: 'y',
|
||||
showarrow: true,
|
||||
arrowhead: 2,
|
||||
arrowsize: 1,
|
||||
arrowwidth: 2,
|
||||
arrowcolor: 'darkgray', // Arrowheads in dark gray
|
||||
})
|
||||
|
||||
// Add edge label annotation closer to the edge tail (25% of the length)
|
||||
// @ts-ignore
|
||||
const labelX = sourceNode.x * 0.75 + targetNode.x * 0.25
|
||||
// @ts-ignore
|
||||
const labelY = sourceNode.y * 0.75 + targetNode.y * 0.25
|
||||
annotations.push({
|
||||
x: labelX,
|
||||
y: labelY,
|
||||
xref: 'x',
|
||||
yref: 'y',
|
||||
text: edge.label,
|
||||
showarrow: false,
|
||||
font: { size: 12, color: 'black' }, // Edge labels in black
|
||||
align: 'center',
|
||||
})
|
||||
})
|
||||
|
||||
const data = [edgeTrace, nodeTrace]
|
||||
|
||||
const layout = {
|
||||
// title: 'Force-Directed Graph with Nodes and Edges',
|
||||
xaxis: { showgrid: false, zeroline: false, showticklabels: false },
|
||||
yaxis: { showgrid: false, zeroline: false, showticklabels: false },
|
||||
showlegend: false,
|
||||
annotations: annotations,
|
||||
}
|
||||
|
||||
// Export to PNG using Playwright
|
||||
const browser = await chromium.launch()
|
||||
const page = await browser.newPage()
|
||||
await page.setContent(`
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="plotly-graph" style="width:${sizeX}px;height:${sizeY}px;"></div>
|
||||
<script>
|
||||
Plotly.newPlot('plotly-graph', ${JSON.stringify(
|
||||
data
|
||||
)}, ${JSON.stringify(layout)})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
await page.waitForSelector('#plotly-graph')
|
||||
const element = await page.$('#plotly-graph')
|
||||
|
||||
// @ts-ignore
|
||||
await element.screenshot({
|
||||
path: `./e2e/playwright/temp3.png`,
|
||||
})
|
||||
|
||||
await browser.close()
|
||||
|
||||
const originalImgPath = path.resolve(
|
||||
`./src/lang/std/artifactMapGraphs/${imageName}`
|
||||
)
|
||||
// chop the top 30 pixels off the image
|
||||
const originalImgExists = fs.existsSync(originalImgPath)
|
||||
const originalImg = originalImgExists
|
||||
? PNG.sync.read(fs.readFileSync(originalImgPath))
|
||||
: null
|
||||
// const img1Data = new Uint8Array(img1.data)
|
||||
// const img1DataChopped = img1Data.slice(30 * img1.width * 4)
|
||||
// img1.data = Buffer.from(img1DataChopped)
|
||||
|
||||
const newImagePath = path.resolve('./e2e/playwright/temp3.png')
|
||||
const newImage = PNG.sync.read(fs.readFileSync(newImagePath))
|
||||
const newImageData = new Uint8Array(newImage.data)
|
||||
const newImageDataChopped = newImageData.slice(30 * newImage.width * 4)
|
||||
newImage.data = Buffer.from(newImageDataChopped)
|
||||
|
||||
const { width, height } = originalImg ?? newImage
|
||||
const diff = new PNG({ width, height })
|
||||
|
||||
const imageSizeDifferent = originalImg?.data.length !== newImage.data.length
|
||||
let numDiffPixels = 0
|
||||
if (!imageSizeDifferent) {
|
||||
numDiffPixels = pixelmatch(
|
||||
originalImg.data,
|
||||
newImage.data,
|
||||
diff.data,
|
||||
width,
|
||||
height,
|
||||
{
|
||||
threshold: 0.1,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (numDiffPixels > 10 || imageSizeDifferent) {
|
||||
console.warn('numDiffPixels', numDiffPixels)
|
||||
// write file out to final place
|
||||
fs.writeFileSync(
|
||||
`src/lang/std/artifactMapGraphs/${imageName}`,
|
||||
PNG.sync.write(newImage)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
describe('testing getArtifactsToUpdate', () => {
|
||||
it('should return an array of artifacts to update', () => {
|
||||
const { artifactCommands, responseMap, ast, execStateArtifacts } =
|
||||
getCommands('exampleCode1')
|
||||
const map = createArtifactGraph({
|
||||
artifactCommands,
|
||||
responseMap,
|
||||
ast,
|
||||
execStateArtifacts,
|
||||
})
|
||||
const getArtifact = (id: string) => map.get(id)
|
||||
const currentPlaneId = 'UUID-1'
|
||||
const getUpdateObjects = (type: Models['ModelingCmd_type']['type']) => {
|
||||
const artifactCommand = artifactCommands.find(
|
||||
(a) => a.command.type === type
|
||||
)
|
||||
if (!artifactCommand) {
|
||||
throw new Error(`No artifactCommand found for ${type}`)
|
||||
}
|
||||
const artifactsToUpdate = getArtifactsToUpdate({
|
||||
artifactCommand,
|
||||
responseMap,
|
||||
getArtifact,
|
||||
currentPlaneId,
|
||||
ast,
|
||||
execStateArtifacts,
|
||||
})
|
||||
return artifactsToUpdate.map(({ artifact }) => artifact)
|
||||
}
|
||||
expect(getUpdateObjects('start_path')).toEqual([
|
||||
{
|
||||
type: 'path',
|
||||
segIds: [],
|
||||
id: expect.any(String),
|
||||
planeId: 'UUID-1',
|
||||
sweepId: undefined,
|
||||
codeRef: {
|
||||
pathToNode: [['body', '']],
|
||||
range: [37, 64, true],
|
||||
},
|
||||
},
|
||||
])
|
||||
expect(getUpdateObjects('extrude')).toEqual([
|
||||
{
|
||||
type: 'sweep',
|
||||
subType: 'extrusion',
|
||||
pathId: expect.any(String),
|
||||
id: expect.any(String),
|
||||
surfaceIds: [],
|
||||
edgeIds: [],
|
||||
codeRef: {
|
||||
range: [231, 254, true],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'path',
|
||||
id: expect.any(String),
|
||||
segIds: expect.any(Array),
|
||||
planeId: expect.any(String),
|
||||
sweepId: expect.any(String),
|
||||
codeRef: {
|
||||
range: [37, 64, true],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
solid2dId: expect.any(String),
|
||||
},
|
||||
])
|
||||
expect(getUpdateObjects('extend_path')).toEqual([
|
||||
{
|
||||
type: 'segment',
|
||||
id: expect.any(String),
|
||||
pathId: expect.any(String),
|
||||
surfaceId: undefined,
|
||||
edgeIds: [],
|
||||
codeRef: {
|
||||
range: [70, 86, true],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'path',
|
||||
id: expect.any(String),
|
||||
segIds: expect.any(Array),
|
||||
planeId: expect.any(String),
|
||||
sweepId: expect.any(String),
|
||||
codeRef: {
|
||||
range: [37, 64, true],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
solid2dId: expect.any(String),
|
||||
},
|
||||
])
|
||||
expect(getUpdateObjects('solid3d_fillet_edge')).toEqual([
|
||||
{
|
||||
type: 'edgeCut',
|
||||
subType: 'fillet',
|
||||
id: expect.any(String),
|
||||
consumedEdgeId: expect.any(String),
|
||||
edgeIds: [],
|
||||
surfaceId: undefined,
|
||||
codeRef: {
|
||||
range: [260, 299, true],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'segment',
|
||||
id: expect.any(String),
|
||||
pathId: expect.any(String),
|
||||
surfaceId: expect.any(String),
|
||||
edgeIds: expect.any(Array),
|
||||
codeRef: {
|
||||
range: [92, 119, true],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
edgeCutId: expect.any(String),
|
||||
},
|
||||
])
|
||||
expect(getUpdateObjects('solid3d_get_extrusion_face_info')).toEqual([
|
||||
{
|
||||
type: 'wall',
|
||||
id: expect.any(String),
|
||||
segId: expect.any(String),
|
||||
edgeCutEdgeIds: [],
|
||||
sweepId: expect.any(String),
|
||||
pathIds: [],
|
||||
},
|
||||
{
|
||||
type: 'segment',
|
||||
id: expect.any(String),
|
||||
pathId: expect.any(String),
|
||||
surfaceId: expect.any(String),
|
||||
edgeIds: expect.any(Array),
|
||||
codeRef: {
|
||||
range: [156, 203, true],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'sweep',
|
||||
subType: 'extrusion',
|
||||
id: expect.any(String),
|
||||
pathId: expect.any(String),
|
||||
surfaceIds: expect.any(Array),
|
||||
edgeIds: expect.any(Array),
|
||||
codeRef: {
|
||||
range: [231, 254, true],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'wall',
|
||||
id: expect.any(String),
|
||||
segId: expect.any(String),
|
||||
edgeCutEdgeIds: [],
|
||||
sweepId: expect.any(String),
|
||||
pathIds: [],
|
||||
},
|
||||
{
|
||||
type: 'segment',
|
||||
id: expect.any(String),
|
||||
pathId: expect.any(String),
|
||||
surfaceId: expect.any(String),
|
||||
edgeIds: expect.any(Array),
|
||||
codeRef: {
|
||||
range: [125, 150, true],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'sweep',
|
||||
subType: 'extrusion',
|
||||
id: expect.any(String),
|
||||
pathId: expect.any(String),
|
||||
surfaceIds: expect.any(Array),
|
||||
edgeIds: expect.any(Array),
|
||||
codeRef: {
|
||||
range: [231, 254, true],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'wall',
|
||||
id: expect.any(String),
|
||||
segId: expect.any(String),
|
||||
edgeCutEdgeIds: [],
|
||||
sweepId: expect.any(String),
|
||||
pathIds: [],
|
||||
},
|
||||
{
|
||||
type: 'segment',
|
||||
id: expect.any(String),
|
||||
pathId: expect.any(String),
|
||||
surfaceId: expect.any(String),
|
||||
edgeIds: expect.any(Array),
|
||||
codeRef: {
|
||||
range: [92, 119, true],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
edgeCutId: expect.any(String),
|
||||
},
|
||||
{
|
||||
type: 'sweep',
|
||||
subType: 'extrusion',
|
||||
id: expect.any(String),
|
||||
pathId: expect.any(String),
|
||||
surfaceIds: expect.any(Array),
|
||||
edgeIds: expect.any(Array),
|
||||
codeRef: {
|
||||
range: [231, 254, true],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'wall',
|
||||
id: expect.any(String),
|
||||
segId: expect.any(String),
|
||||
edgeCutEdgeIds: [],
|
||||
sweepId: expect.any(String),
|
||||
pathIds: [],
|
||||
},
|
||||
{
|
||||
type: 'segment',
|
||||
id: expect.any(String),
|
||||
pathId: expect.any(String),
|
||||
surfaceId: expect.any(String),
|
||||
edgeIds: expect.any(Array),
|
||||
codeRef: {
|
||||
range: [70, 86, true],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'sweep',
|
||||
subType: 'extrusion',
|
||||
id: expect.any(String),
|
||||
pathId: expect.any(String),
|
||||
surfaceIds: expect.any(Array),
|
||||
edgeIds: expect.any(Array),
|
||||
codeRef: {
|
||||
range: [231, 254, true],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'cap',
|
||||
subType: 'start',
|
||||
id: expect.any(String),
|
||||
edgeCutEdgeIds: [],
|
||||
sweepId: expect.any(String),
|
||||
pathIds: [],
|
||||
},
|
||||
{
|
||||
type: 'sweep',
|
||||
subType: 'extrusion',
|
||||
id: expect.any(String),
|
||||
pathId: expect.any(String),
|
||||
surfaceIds: expect.any(Array),
|
||||
edgeIds: expect.any(Array),
|
||||
codeRef: {
|
||||
range: [231, 254, true],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'cap',
|
||||
subType: 'end',
|
||||
id: expect.any(String),
|
||||
edgeCutEdgeIds: [],
|
||||
sweepId: expect.any(String),
|
||||
pathIds: [],
|
||||
},
|
||||
{
|
||||
type: 'sweep',
|
||||
subType: 'extrusion',
|
||||
id: expect.any(String),
|
||||
pathId: expect.any(String),
|
||||
surfaceIds: expect.any(Array),
|
||||
edgeIds: expect.any(Array),
|
||||
codeRef: {
|
||||
range: [231, 254, true],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
@ -1,17 +1,25 @@
|
||||
import {
|
||||
ArtifactCommand,
|
||||
ExecState,
|
||||
Artifact,
|
||||
ArtifactGraph,
|
||||
ArtifactId,
|
||||
PathToNode,
|
||||
Program,
|
||||
SourceRange,
|
||||
sourceRangeFromRust,
|
||||
PathArtifact,
|
||||
PlaneArtifact,
|
||||
WallArtifact,
|
||||
SegmentArtifact,
|
||||
Solid2dArtifact as Solid2D,
|
||||
SweepArtifact,
|
||||
SweepEdge,
|
||||
CapArtifact,
|
||||
EdgeCut,
|
||||
} from 'lang/wasm'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAst'
|
||||
import { err } from 'lib/trap'
|
||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
|
||||
export type ArtifactId = string
|
||||
export type { Artifact, ArtifactId, SegmentArtifact } from 'lang/wasm'
|
||||
|
||||
interface BaseArtifact {
|
||||
id: ArtifactId
|
||||
@ -22,30 +30,12 @@ export interface CodeRef {
|
||||
pathToNode: PathToNode
|
||||
}
|
||||
|
||||
export interface PlaneArtifact extends BaseArtifact {
|
||||
type: 'plane'
|
||||
pathIds: Array<ArtifactId>
|
||||
codeRef: CodeRef
|
||||
}
|
||||
export interface PlaneArtifactRich extends BaseArtifact {
|
||||
type: 'plane'
|
||||
paths: Array<PathArtifact>
|
||||
codeRef: CodeRef
|
||||
}
|
||||
|
||||
export interface PathArtifact extends BaseArtifact {
|
||||
type: 'path'
|
||||
planeId: ArtifactId
|
||||
segIds: Array<ArtifactId>
|
||||
sweepId?: ArtifactId
|
||||
solid2dId?: ArtifactId
|
||||
codeRef: CodeRef
|
||||
}
|
||||
|
||||
interface solid2D extends BaseArtifact {
|
||||
type: 'solid2D'
|
||||
pathId: ArtifactId
|
||||
}
|
||||
export interface PathArtifactRich extends BaseArtifact {
|
||||
type: 'path'
|
||||
/** A path must always lie on a plane */
|
||||
@ -53,18 +43,10 @@ export interface PathArtifactRich extends BaseArtifact {
|
||||
/** A path must always contain 0 or more segments */
|
||||
segments: Array<SegmentArtifact>
|
||||
/** A path may not result in a sweep artifact */
|
||||
sweep?: SweepArtifact
|
||||
sweep: SweepArtifact | null
|
||||
codeRef: CodeRef
|
||||
}
|
||||
|
||||
export interface SegmentArtifact extends BaseArtifact {
|
||||
type: 'segment'
|
||||
pathId: ArtifactId
|
||||
surfaceId?: ArtifactId
|
||||
edgeIds: Array<ArtifactId>
|
||||
edgeCutId?: ArtifactId
|
||||
codeRef: CodeRef
|
||||
}
|
||||
interface SegmentArtifactRich extends BaseArtifact {
|
||||
type: 'segment'
|
||||
path: PathArtifact
|
||||
@ -74,15 +56,6 @@ interface SegmentArtifactRich extends BaseArtifact {
|
||||
codeRef: CodeRef
|
||||
}
|
||||
|
||||
/** A Sweep is a more generic term for extrude, revolve, loft and sweep*/
|
||||
interface SweepArtifact extends BaseArtifact {
|
||||
type: 'sweep'
|
||||
subType: 'extrusion' | 'revolve' | 'loft' | 'sweep'
|
||||
pathId: string
|
||||
surfaceIds: Array<string>
|
||||
edgeIds: Array<string>
|
||||
codeRef: CodeRef
|
||||
}
|
||||
interface SweepArtifactRich extends BaseArtifact {
|
||||
type: 'sweep'
|
||||
subType: 'extrusion' | 'revolve' | 'loft' | 'sweep'
|
||||
@ -92,58 +65,6 @@ interface SweepArtifactRich extends BaseArtifact {
|
||||
codeRef: CodeRef
|
||||
}
|
||||
|
||||
interface WallArtifact extends BaseArtifact {
|
||||
type: 'wall'
|
||||
segId: ArtifactId
|
||||
edgeCutEdgeIds: Array<ArtifactId>
|
||||
sweepId: ArtifactId
|
||||
pathIds: Array<ArtifactId>
|
||||
}
|
||||
interface CapArtifact extends BaseArtifact {
|
||||
type: 'cap'
|
||||
subType: 'start' | 'end'
|
||||
edgeCutEdgeIds: Array<ArtifactId>
|
||||
sweepId: ArtifactId
|
||||
pathIds: Array<ArtifactId>
|
||||
}
|
||||
|
||||
interface SweepEdge extends BaseArtifact {
|
||||
type: 'sweepEdge'
|
||||
segId: ArtifactId
|
||||
sweepId: ArtifactId
|
||||
subType: 'opposite' | 'adjacent'
|
||||
}
|
||||
|
||||
/** A edgeCut is a more generic term for both fillet or chamfer */
|
||||
interface EdgeCut extends BaseArtifact {
|
||||
type: 'edgeCut'
|
||||
subType: 'fillet' | 'chamfer'
|
||||
consumedEdgeId: ArtifactId
|
||||
edgeIds: Array<ArtifactId>
|
||||
surfaceId?: ArtifactId
|
||||
codeRef: CodeRef
|
||||
}
|
||||
|
||||
interface EdgeCutEdge extends BaseArtifact {
|
||||
type: 'edgeCutEdge'
|
||||
edgeCutId: ArtifactId
|
||||
surfaceId: ArtifactId
|
||||
}
|
||||
|
||||
export type Artifact =
|
||||
| PlaneArtifact
|
||||
| PathArtifact
|
||||
| SegmentArtifact
|
||||
| SweepArtifact
|
||||
| WallArtifact
|
||||
| CapArtifact
|
||||
| SweepEdge
|
||||
| EdgeCut
|
||||
| EdgeCutEdge
|
||||
| solid2D
|
||||
|
||||
export type ArtifactGraph = Map<ArtifactId, Artifact>
|
||||
|
||||
export type EngineCommand = Models['WebSocketRequest_type']
|
||||
|
||||
type OkWebSocketResponseData = Models['OkWebSocketResponseData_type']
|
||||
@ -152,437 +73,6 @@ export interface ResponseMap {
|
||||
[commandId: string]: OkWebSocketResponseData
|
||||
}
|
||||
|
||||
/** Creates a graph of artifacts from a list of ordered commands and their responses
|
||||
* muting the Map should happen entirely this function, other functions called within
|
||||
* should return data on how to update the map, and not do so directly.
|
||||
*/
|
||||
export function createArtifactGraph({
|
||||
artifactCommands,
|
||||
responseMap,
|
||||
ast,
|
||||
execStateArtifacts,
|
||||
}: {
|
||||
artifactCommands: Array<ArtifactCommand>
|
||||
responseMap: ResponseMap
|
||||
ast: Node<Program>
|
||||
execStateArtifacts: ExecState['artifacts']
|
||||
}) {
|
||||
const myMap = new Map<ArtifactId, Artifact>()
|
||||
|
||||
/** see docstring for {@link getArtifactsToUpdate} as to why this is needed */
|
||||
let currentPlaneId = ''
|
||||
|
||||
for (const artifactCommand of artifactCommands) {
|
||||
if (artifactCommand.command.type === 'enable_sketch_mode') {
|
||||
currentPlaneId = artifactCommand.command.entity_id
|
||||
}
|
||||
if (artifactCommand.command.type === 'sketch_mode_disable') {
|
||||
currentPlaneId = ''
|
||||
}
|
||||
const artifactsToUpdate = getArtifactsToUpdate({
|
||||
artifactCommand,
|
||||
responseMap,
|
||||
getArtifact: (id: ArtifactId) => myMap.get(id),
|
||||
currentPlaneId,
|
||||
ast,
|
||||
execStateArtifacts,
|
||||
})
|
||||
artifactsToUpdate.forEach(({ id, artifact }) => {
|
||||
const mergedArtifact = mergeArtifacts(myMap.get(id), artifact)
|
||||
myMap.set(id, mergedArtifact)
|
||||
})
|
||||
}
|
||||
return myMap
|
||||
}
|
||||
|
||||
/** Merges two artifacts, since our artifacts only contain strings and arrays of string for values we coerce that
|
||||
* but maybe types can be improved here.
|
||||
*/
|
||||
function mergeArtifacts(
|
||||
oldArtifact: Artifact | undefined,
|
||||
newArtifact: Artifact
|
||||
): Artifact {
|
||||
// only has string and array of strings
|
||||
interface GenericArtifact {
|
||||
[key: string]: string | Array<string>
|
||||
}
|
||||
if (!oldArtifact) return newArtifact
|
||||
// merging artifacts of different types should never happen, but if it does, just return the new artifact
|
||||
if (oldArtifact.type !== newArtifact.type) return newArtifact
|
||||
const _oldArtifact = oldArtifact as any as GenericArtifact
|
||||
const mergedArtifact = { ...oldArtifact, ...newArtifact } as GenericArtifact
|
||||
Object.entries(newArtifact as any as GenericArtifact).forEach(
|
||||
([propName, value]) => {
|
||||
const otherValue = _oldArtifact[propName]
|
||||
if (Array.isArray(value) && Array.isArray(otherValue)) {
|
||||
mergedArtifact[propName] = [...new Set([...otherValue, ...value])]
|
||||
}
|
||||
}
|
||||
)
|
||||
return mergedArtifact as any as Artifact
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a single command and it's response in order to populate the artifact map
|
||||
* It does not mutate the map directly, but returns an array of artifacts to update
|
||||
*
|
||||
* @param currentPlaneId is only needed for `start_path` commands because this command does not have a pathId
|
||||
* instead it relies on the id used with the `enable_sketch_mode` command, so this much be kept track of
|
||||
* outside of this function. It would be good to update the `start_path` command to include the planeId so we
|
||||
* can remove this.
|
||||
*/
|
||||
export function getArtifactsToUpdate({
|
||||
artifactCommand,
|
||||
getArtifact,
|
||||
responseMap,
|
||||
currentPlaneId,
|
||||
ast,
|
||||
execStateArtifacts,
|
||||
}: {
|
||||
artifactCommand: ArtifactCommand
|
||||
responseMap: ResponseMap
|
||||
/** Passing in a getter because we don't wan this function to update the map directly */
|
||||
getArtifact: (id: ArtifactId) => Artifact | undefined
|
||||
currentPlaneId: ArtifactId
|
||||
ast: Node<Program>
|
||||
execStateArtifacts: ExecState['artifacts']
|
||||
}): Array<{
|
||||
id: ArtifactId
|
||||
artifact: Artifact
|
||||
}> {
|
||||
const range = sourceRangeFromRust(artifactCommand.range)
|
||||
const pathToNode = getNodePathFromSourceRange(ast, range)
|
||||
|
||||
const id = artifactCommand.cmdId
|
||||
const response = responseMap[id]
|
||||
const cmd = artifactCommand.command
|
||||
const returnArr: ReturnType<typeof getArtifactsToUpdate> = []
|
||||
if (!response) return returnArr
|
||||
if (cmd.type === 'make_plane' && range[1] !== 0) {
|
||||
// If we're calling `make_plane` and the code range doesn't end at `0`
|
||||
// it's not a default plane, but a custom one from the offsetPlane standard library function
|
||||
return [
|
||||
{
|
||||
id,
|
||||
artifact: {
|
||||
type: 'plane',
|
||||
id,
|
||||
pathIds: [],
|
||||
codeRef: { range, pathToNode },
|
||||
},
|
||||
},
|
||||
]
|
||||
} else if (cmd.type === 'enable_sketch_mode') {
|
||||
const plane = getArtifact(currentPlaneId)
|
||||
const pathIds = plane?.type === 'plane' ? plane?.pathIds : []
|
||||
const codeRef =
|
||||
plane?.type === 'plane' ? plane?.codeRef : { range, pathToNode }
|
||||
const existingPlane = getArtifact(currentPlaneId)
|
||||
if (existingPlane?.type === 'wall') {
|
||||
return [
|
||||
{
|
||||
id: currentPlaneId,
|
||||
artifact: {
|
||||
type: 'wall',
|
||||
id: currentPlaneId,
|
||||
segId: existingPlane.segId,
|
||||
edgeCutEdgeIds: existingPlane.edgeCutEdgeIds,
|
||||
sweepId: existingPlane.sweepId,
|
||||
pathIds: existingPlane.pathIds,
|
||||
},
|
||||
},
|
||||
]
|
||||
} else {
|
||||
return [
|
||||
{
|
||||
id: currentPlaneId,
|
||||
artifact: { type: 'plane', id: currentPlaneId, pathIds, codeRef },
|
||||
},
|
||||
]
|
||||
}
|
||||
} else if (cmd.type === 'start_path') {
|
||||
returnArr.push({
|
||||
id,
|
||||
artifact: {
|
||||
type: 'path',
|
||||
id,
|
||||
segIds: [],
|
||||
planeId: currentPlaneId,
|
||||
sweepId: undefined,
|
||||
codeRef: { range, pathToNode },
|
||||
},
|
||||
})
|
||||
const plane = getArtifact(currentPlaneId)
|
||||
const codeRef =
|
||||
plane?.type === 'plane' ? plane?.codeRef : { range, pathToNode }
|
||||
if (plane?.type === 'plane') {
|
||||
returnArr.push({
|
||||
id: currentPlaneId,
|
||||
artifact: { type: 'plane', id: currentPlaneId, pathIds: [id], codeRef },
|
||||
})
|
||||
}
|
||||
if (plane?.type === 'wall') {
|
||||
returnArr.push({
|
||||
id: currentPlaneId,
|
||||
artifact: {
|
||||
type: 'wall',
|
||||
id: currentPlaneId,
|
||||
segId: plane.segId,
|
||||
edgeCutEdgeIds: plane.edgeCutEdgeIds,
|
||||
sweepId: plane.sweepId,
|
||||
pathIds: [id],
|
||||
},
|
||||
})
|
||||
}
|
||||
return returnArr
|
||||
} else if (cmd.type === 'extend_path' || cmd.type === 'close_path') {
|
||||
const pathId = cmd.type === 'extend_path' ? cmd.path : cmd.path_id
|
||||
returnArr.push({
|
||||
id,
|
||||
artifact: {
|
||||
type: 'segment',
|
||||
id,
|
||||
pathId,
|
||||
surfaceId: undefined,
|
||||
edgeIds: [],
|
||||
codeRef: { range, pathToNode },
|
||||
},
|
||||
})
|
||||
const path = getArtifact(pathId)
|
||||
if (path?.type === 'path')
|
||||
returnArr.push({
|
||||
id: pathId,
|
||||
artifact: { ...path, segIds: [id] },
|
||||
})
|
||||
if (
|
||||
response?.type === 'modeling' &&
|
||||
response.data.modeling_response.type === 'close_path'
|
||||
) {
|
||||
returnArr.push({
|
||||
id: response.data.modeling_response.data.face_id,
|
||||
artifact: {
|
||||
type: 'solid2D',
|
||||
id: response.data.modeling_response.data.face_id,
|
||||
pathId,
|
||||
},
|
||||
})
|
||||
const path = getArtifact(pathId)
|
||||
if (path?.type === 'path')
|
||||
returnArr.push({
|
||||
id: pathId,
|
||||
artifact: {
|
||||
...path,
|
||||
solid2dId: response.data.modeling_response.data.face_id,
|
||||
},
|
||||
})
|
||||
}
|
||||
return returnArr
|
||||
} else if (
|
||||
cmd.type === 'extrude' ||
|
||||
cmd.type === 'revolve' ||
|
||||
cmd.type === 'sweep'
|
||||
) {
|
||||
const subType = cmd.type === 'extrude' ? 'extrusion' : cmd.type
|
||||
returnArr.push({
|
||||
id,
|
||||
artifact: {
|
||||
type: 'sweep',
|
||||
subType: subType,
|
||||
id,
|
||||
pathId: cmd.target,
|
||||
surfaceIds: [],
|
||||
edgeIds: [],
|
||||
codeRef: { range, pathToNode },
|
||||
},
|
||||
})
|
||||
const path = getArtifact(cmd.target)
|
||||
if (path?.type === 'path')
|
||||
returnArr.push({
|
||||
id: cmd.target,
|
||||
artifact: { ...path, sweepId: id },
|
||||
})
|
||||
return returnArr
|
||||
} else if (
|
||||
cmd.type === 'loft' &&
|
||||
response.type === 'modeling' &&
|
||||
response.data.modeling_response.type === 'loft'
|
||||
) {
|
||||
returnArr.push({
|
||||
id,
|
||||
artifact: {
|
||||
type: 'sweep',
|
||||
subType: 'loft',
|
||||
id,
|
||||
// TODO: make sure to revisit this choice, don't think it matters for now
|
||||
pathId: cmd.section_ids[0],
|
||||
surfaceIds: [],
|
||||
edgeIds: [],
|
||||
codeRef: { range, pathToNode },
|
||||
},
|
||||
})
|
||||
for (const sectionId of cmd.section_ids) {
|
||||
const path = getArtifact(sectionId)
|
||||
if (path?.type === 'path')
|
||||
returnArr.push({
|
||||
id: sectionId,
|
||||
artifact: { ...path, sweepId: id },
|
||||
})
|
||||
}
|
||||
return returnArr
|
||||
} else if (
|
||||
cmd.type === 'solid3d_get_extrusion_face_info' &&
|
||||
response?.type === 'modeling' &&
|
||||
response.data.modeling_response.type === 'solid3d_get_extrusion_face_info'
|
||||
) {
|
||||
let lastPath: PathArtifact
|
||||
response.data.modeling_response.data.faces.forEach(
|
||||
({ curve_id, cap, face_id }) => {
|
||||
if (cap === 'none' && curve_id && face_id) {
|
||||
const seg = getArtifact(curve_id)
|
||||
if (seg?.type !== 'segment') return
|
||||
const path = getArtifact(seg.pathId)
|
||||
if (path?.type === 'path' && seg?.type === 'segment') {
|
||||
lastPath = path
|
||||
returnArr.push({
|
||||
id: face_id,
|
||||
artifact: {
|
||||
type: 'wall',
|
||||
id: face_id,
|
||||
segId: curve_id,
|
||||
edgeCutEdgeIds: [],
|
||||
// TODO: Add explicit check for sweepId. Should never use ''
|
||||
sweepId: path.sweepId ?? '',
|
||||
pathIds: [],
|
||||
},
|
||||
})
|
||||
returnArr.push({
|
||||
id: curve_id,
|
||||
artifact: { ...seg, surfaceId: face_id },
|
||||
})
|
||||
if (path.sweepId) {
|
||||
const sweep = getArtifact(path.sweepId)
|
||||
if (sweep?.type === 'sweep') {
|
||||
returnArr.push({
|
||||
id: path.sweepId,
|
||||
artifact: {
|
||||
...sweep,
|
||||
surfaceIds: [face_id],
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
response.data.modeling_response.data.faces.forEach(({ cap, face_id }) => {
|
||||
if ((cap === 'top' || cap === 'bottom') && face_id) {
|
||||
const path = lastPath
|
||||
if (path?.type === 'path') {
|
||||
returnArr.push({
|
||||
id: face_id,
|
||||
artifact: {
|
||||
type: 'cap',
|
||||
id: face_id,
|
||||
subType: cap === 'bottom' ? 'start' : 'end',
|
||||
edgeCutEdgeIds: [],
|
||||
// TODO: Add explicit check for sweepId. Should never use ''
|
||||
sweepId: path.sweepId ?? '',
|
||||
pathIds: [],
|
||||
},
|
||||
})
|
||||
if (path.sweepId) {
|
||||
const sweep = getArtifact(path.sweepId)
|
||||
if (sweep?.type !== 'sweep') return
|
||||
returnArr.push({
|
||||
id: path.sweepId,
|
||||
artifact: {
|
||||
...sweep,
|
||||
surfaceIds: [face_id],
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
return returnArr
|
||||
} else if (
|
||||
// is opposite edge
|
||||
(cmd.type === 'solid3d_get_opposite_edge' &&
|
||||
response.type === 'modeling' &&
|
||||
response.data.modeling_response.type === 'solid3d_get_opposite_edge' &&
|
||||
response.data.modeling_response.data.edge) ||
|
||||
// or is adjacent edge
|
||||
(cmd.type === 'solid3d_get_next_adjacent_edge' &&
|
||||
response.type === 'modeling' &&
|
||||
response.data.modeling_response.type ===
|
||||
'solid3d_get_next_adjacent_edge' &&
|
||||
response.data.modeling_response.data.edge)
|
||||
) {
|
||||
const wall = getArtifact(cmd.face_id)
|
||||
if (wall?.type !== 'wall') return returnArr
|
||||
const sweep = getArtifact(wall.sweepId)
|
||||
if (sweep?.type !== 'sweep') return returnArr
|
||||
const path = getArtifact(sweep.pathId)
|
||||
if (path?.type !== 'path') return returnArr
|
||||
const segment = getArtifact(cmd.edge_id)
|
||||
if (segment?.type !== 'segment') return returnArr
|
||||
|
||||
return [
|
||||
{
|
||||
id: response.data.modeling_response.data.edge,
|
||||
artifact: {
|
||||
type: 'sweepEdge',
|
||||
id: response.data.modeling_response.data.edge,
|
||||
subType:
|
||||
cmd.type === 'solid3d_get_next_adjacent_edge'
|
||||
? 'adjacent'
|
||||
: 'opposite',
|
||||
segId: cmd.edge_id,
|
||||
// TODO: Add explicit check for sweepId. Should never use ''
|
||||
sweepId: path.sweepId ?? '',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: cmd.edge_id,
|
||||
artifact: {
|
||||
...segment,
|
||||
edgeIds: [response.data.modeling_response.data.edge],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: sweep.id,
|
||||
artifact: {
|
||||
...sweep,
|
||||
edgeIds: [response.data.modeling_response.data.edge],
|
||||
},
|
||||
},
|
||||
]
|
||||
} else if (cmd.type === 'solid3d_fillet_edge') {
|
||||
returnArr.push({
|
||||
id,
|
||||
artifact: {
|
||||
type: 'edgeCut',
|
||||
id,
|
||||
subType: cmd.cut_type,
|
||||
consumedEdgeId: cmd.edge_id,
|
||||
edgeIds: [],
|
||||
surfaceId: undefined,
|
||||
codeRef: { range, pathToNode },
|
||||
},
|
||||
})
|
||||
const consumedEdge = getArtifact(cmd.edge_id)
|
||||
if (consumedEdge?.type === 'segment') {
|
||||
returnArr.push({
|
||||
id: cmd.edge_id,
|
||||
artifact: { ...consumedEdge, edgeCutId: id },
|
||||
})
|
||||
}
|
||||
return returnArr
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/** filter map items of a specific type */
|
||||
export function filterArtifacts<T extends Artifact['type'][]>(
|
||||
{
|
||||
@ -676,7 +166,7 @@ export function expandPath(
|
||||
},
|
||||
artifactGraph
|
||||
)
|
||||
: undefined
|
||||
: null
|
||||
const plane = getArtifactOfTypes(
|
||||
{ key: path.planeId, types: ['plane', 'wall'] },
|
||||
artifactGraph
|
||||
@ -778,11 +268,11 @@ export function getCapCodeRef(
|
||||
}
|
||||
|
||||
export function getSolid2dCodeRef(
|
||||
solid2D: solid2D,
|
||||
solid2d: Solid2D,
|
||||
artifactGraph: ArtifactGraph
|
||||
): CodeRef | Error {
|
||||
const path = getArtifactOfTypes(
|
||||
{ key: solid2D.pathId, types: ['path'] },
|
||||
{ key: solid2d.pathId, types: ['path'] },
|
||||
artifactGraph
|
||||
)
|
||||
if (err(path)) return path
|
||||
@ -881,7 +371,7 @@ export function getCodeRefsByArtifactId(
|
||||
artifactGraph: ArtifactGraph
|
||||
): Array<CodeRef> | null {
|
||||
const artifact = artifactGraph.get(id)
|
||||
if (artifact?.type === 'solid2D') {
|
||||
if (artifact?.type === 'solid2d') {
|
||||
const codeRef = getSolid2dCodeRef(artifact, artifactGraph)
|
||||
if (err(codeRef)) return null
|
||||
return [codeRef]
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import {
|
||||
ArtifactCommand,
|
||||
defaultRustSourceRange,
|
||||
ArtifactGraph,
|
||||
defaultSourceRange,
|
||||
ExecState,
|
||||
Program,
|
||||
RustSourceRange,
|
||||
SourceRange,
|
||||
} from 'lang/wasm'
|
||||
import { VITE_KC_API_WS_MODELING_URL, VITE_KC_DEV_TOKEN } from 'env'
|
||||
@ -17,12 +15,7 @@ import {
|
||||
darkModeMatcher,
|
||||
} from 'lib/theme'
|
||||
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
|
||||
import {
|
||||
ArtifactGraph,
|
||||
EngineCommand,
|
||||
ResponseMap,
|
||||
createArtifactGraph,
|
||||
} from 'lang/std/artifactGraph'
|
||||
import { EngineCommand, ResponseMap } from 'lang/std/artifactGraph'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { exportMake } from 'lib/exportMake'
|
||||
import toast from 'react-hot-toast'
|
||||
@ -36,7 +29,6 @@ import { KclManager } from 'lang/KclSingleton'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { markOnce } from 'lib/performance'
|
||||
import { MachineManager } from 'components/MachineManagerProvider'
|
||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
|
||||
// TODO(paultag): This ought to be tweakable.
|
||||
const pingIntervalMs = 5_000
|
||||
@ -1022,6 +1014,11 @@ class EngineConnection extends EventTarget {
|
||||
this.pingPongSpan.pong = new Date()
|
||||
break
|
||||
|
||||
case 'modeling_session_data':
|
||||
let api_call_id = resp.data?.session?.api_call_id
|
||||
console.log(`API Call ID: ${api_call_id}`)
|
||||
break
|
||||
|
||||
// Only fires on successful authentication.
|
||||
case 'ice_server_info':
|
||||
let ice_servers = resp.data?.ice_servers
|
||||
@ -1309,8 +1306,8 @@ export enum EngineCommandManagerEvents {
|
||||
|
||||
interface PendingMessage {
|
||||
command: EngineCommand
|
||||
range: RustSourceRange
|
||||
idToRangeMap: { [key: string]: RustSourceRange }
|
||||
range: SourceRange
|
||||
idToRangeMap: { [key: string]: SourceRange }
|
||||
resolve: (data: [Models['WebSocketResponse_type']]) => void
|
||||
reject: (reason: string) => void
|
||||
promise: Promise<[Models['WebSocketResponse_type']]>
|
||||
@ -1994,7 +1991,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
{
|
||||
command,
|
||||
idToRangeMap: {},
|
||||
range: defaultRustSourceRange(),
|
||||
range: defaultSourceRange(),
|
||||
},
|
||||
true // isSceneCommand
|
||||
)
|
||||
@ -2025,9 +2022,9 @@ export class EngineCommandManager extends EventTarget {
|
||||
return Promise.reject(new Error('rangeStr is undefined'))
|
||||
if (commandStr === undefined)
|
||||
return Promise.reject(new Error('commandStr is undefined'))
|
||||
const range: RustSourceRange = JSON.parse(rangeStr)
|
||||
const range: SourceRange = JSON.parse(rangeStr)
|
||||
const command: EngineCommand = JSON.parse(commandStr)
|
||||
const idToRangeMap: { [key: string]: RustSourceRange } =
|
||||
const idToRangeMap: { [key: string]: SourceRange } =
|
||||
JSON.parse(idToRangeStr)
|
||||
|
||||
// Current executeAst is stale, going to interrupt, a new executeAst will trigger
|
||||
@ -2087,17 +2084,8 @@ export class EngineCommandManager extends EventTarget {
|
||||
Object.values(this.pendingCommands).map((a) => a.promise)
|
||||
)
|
||||
}
|
||||
updateArtifactGraph(
|
||||
ast: Node<Program>,
|
||||
artifactCommands: ArtifactCommand[],
|
||||
execStateArtifacts: ExecState['artifacts']
|
||||
) {
|
||||
this.artifactGraph = createArtifactGraph({
|
||||
artifactCommands,
|
||||
responseMap: this.responseMap,
|
||||
ast,
|
||||
execStateArtifacts,
|
||||
})
|
||||
updateArtifactGraph(execStateArtifactGraph: ExecState['artifactGraph']) {
|
||||
this.artifactGraph = execStateArtifactGraph
|
||||
// TODO check if these still need to be deferred once e2e tests are working again.
|
||||
if (this.artifactGraph.size) {
|
||||
this.deferredArtifactEmptied(null)
|
||||
|
||||
@ -11,8 +11,8 @@ import {
|
||||
assertParse,
|
||||
recast,
|
||||
initPromise,
|
||||
SourceRange,
|
||||
CallExpression,
|
||||
topLevelRange,
|
||||
} from '../wasm'
|
||||
import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst'
|
||||
import { enginelessExecutor } from '../../lib/testHelpers'
|
||||
@ -124,7 +124,10 @@ describe('testing changeSketchArguments', () => {
|
||||
execState.memory,
|
||||
{
|
||||
type: 'sourceRange',
|
||||
sourceRange: [sourceStart, sourceStart + lineToChange.length, true],
|
||||
sourceRange: topLevelRange(
|
||||
sourceStart,
|
||||
sourceStart + lineToChange.length
|
||||
),
|
||||
},
|
||||
{
|
||||
type: 'straight-segment',
|
||||
@ -219,11 +222,10 @@ describe('testing addTagForSketchOnFace', () => {
|
||||
const ast = assertParse(code)
|
||||
await enginelessExecutor(ast)
|
||||
const sourceStart = code.indexOf(originalLine)
|
||||
const sourceRange: [number, number, boolean] = [
|
||||
const sourceRange = topLevelRange(
|
||||
sourceStart,
|
||||
sourceStart + originalLine.length,
|
||||
true,
|
||||
]
|
||||
sourceStart + originalLine.length
|
||||
)
|
||||
if (err(ast)) return ast
|
||||
const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
|
||||
const sketchOnFaceRetVal = addTagForSketchOnFace(
|
||||
@ -292,11 +294,10 @@ ${insertCode}
|
||||
await enginelessExecutor(ast)
|
||||
const sourceStart = code.indexOf(originalChamfer)
|
||||
const extraChars = originalChamfer.indexOf('chamfer')
|
||||
const sourceRange: [number, number, boolean] = [
|
||||
const sourceRange = topLevelRange(
|
||||
sourceStart + extraChars,
|
||||
sourceStart + originalChamfer.length - extraChars,
|
||||
true,
|
||||
]
|
||||
sourceStart + originalChamfer.length - extraChars
|
||||
)
|
||||
|
||||
if (err(ast)) throw ast
|
||||
const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
|
||||
@ -357,7 +358,6 @@ describe('testing getConstraintInfo', () => {
|
||||
offset = 0
|
||||
}, %)
|
||||
|> tangentialArcTo([3.14, 13.14], %)`
|
||||
const ast = assertParse(code)
|
||||
test.each([
|
||||
[
|
||||
'line',
|
||||
@ -366,7 +366,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'xRelative',
|
||||
isConstrained: false,
|
||||
value: '3',
|
||||
sourceRange: [78, 79, true],
|
||||
sourceRange: topLevelRange(78, 79),
|
||||
argPosition: { type: 'arrayItem', index: 0 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'line',
|
||||
@ -375,7 +375,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'yRelative',
|
||||
isConstrained: false,
|
||||
value: '4',
|
||||
sourceRange: [81, 82, true],
|
||||
sourceRange: topLevelRange(81, 82),
|
||||
argPosition: { type: 'arrayItem', index: 1 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'line',
|
||||
@ -389,7 +389,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'angle',
|
||||
isConstrained: false,
|
||||
value: '3.14',
|
||||
sourceRange: [118, 122, true],
|
||||
sourceRange: topLevelRange(118, 122),
|
||||
argPosition: { type: 'objectProperty', key: 'angle' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLine',
|
||||
@ -398,7 +398,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'length',
|
||||
isConstrained: false,
|
||||
value: '3.14',
|
||||
sourceRange: [137, 141, true],
|
||||
sourceRange: topLevelRange(137, 141),
|
||||
argPosition: { type: 'objectProperty', key: 'length' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLine',
|
||||
@ -412,7 +412,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'xAbsolute',
|
||||
isConstrained: false,
|
||||
value: '6.14',
|
||||
sourceRange: [164, 168, true],
|
||||
sourceRange: topLevelRange(164, 168),
|
||||
argPosition: { type: 'arrayItem', index: 0 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'lineTo',
|
||||
@ -421,7 +421,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'yAbsolute',
|
||||
isConstrained: false,
|
||||
value: '3.14',
|
||||
sourceRange: [170, 174, true],
|
||||
sourceRange: topLevelRange(170, 174),
|
||||
argPosition: { type: 'arrayItem', index: 1 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'lineTo',
|
||||
@ -435,7 +435,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'horizontal',
|
||||
isConstrained: true,
|
||||
value: 'xLineTo',
|
||||
sourceRange: [185, 192, true],
|
||||
sourceRange: topLevelRange(185, 192),
|
||||
argPosition: undefined,
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'xLineTo',
|
||||
@ -444,7 +444,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'xAbsolute',
|
||||
isConstrained: false,
|
||||
value: '8',
|
||||
sourceRange: [193, 194, true],
|
||||
sourceRange: topLevelRange(193, 194),
|
||||
argPosition: { type: 'singleValue' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'xLineTo',
|
||||
@ -458,7 +458,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'vertical',
|
||||
isConstrained: true,
|
||||
value: 'yLineTo',
|
||||
sourceRange: [204, 211, true],
|
||||
sourceRange: topLevelRange(204, 211),
|
||||
argPosition: undefined,
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'yLineTo',
|
||||
@ -467,7 +467,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'yAbsolute',
|
||||
isConstrained: false,
|
||||
value: '5',
|
||||
sourceRange: [212, 213, true],
|
||||
sourceRange: topLevelRange(212, 213),
|
||||
argPosition: { type: 'singleValue' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'yLineTo',
|
||||
@ -481,7 +481,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'vertical',
|
||||
isConstrained: true,
|
||||
value: 'yLine',
|
||||
sourceRange: [223, 228, true],
|
||||
sourceRange: topLevelRange(223, 228),
|
||||
argPosition: undefined,
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'yLine',
|
||||
@ -490,7 +490,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'yRelative',
|
||||
isConstrained: false,
|
||||
value: '3.14',
|
||||
sourceRange: [229, 233, true],
|
||||
sourceRange: topLevelRange(229, 233),
|
||||
argPosition: { type: 'singleValue' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'yLine',
|
||||
@ -504,7 +504,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'horizontal',
|
||||
isConstrained: true,
|
||||
value: 'xLine',
|
||||
sourceRange: [247, 252, true],
|
||||
sourceRange: topLevelRange(247, 252),
|
||||
argPosition: undefined,
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'xLine',
|
||||
@ -513,7 +513,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'xRelative',
|
||||
isConstrained: false,
|
||||
value: '3.14',
|
||||
sourceRange: [253, 257, true],
|
||||
sourceRange: topLevelRange(253, 257),
|
||||
argPosition: { type: 'singleValue' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'xLine',
|
||||
@ -527,7 +527,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'angle',
|
||||
isConstrained: false,
|
||||
value: '3.14',
|
||||
sourceRange: [301, 305, true],
|
||||
sourceRange: topLevelRange(301, 305),
|
||||
argPosition: { type: 'objectProperty', key: 'angle' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineOfXLength',
|
||||
@ -536,7 +536,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'xRelative',
|
||||
isConstrained: false,
|
||||
value: '3.14',
|
||||
sourceRange: [320, 324, true],
|
||||
sourceRange: topLevelRange(320, 324),
|
||||
argPosition: { type: 'objectProperty', key: 'length' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineOfXLength',
|
||||
@ -550,7 +550,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'angle',
|
||||
isConstrained: false,
|
||||
value: '30',
|
||||
sourceRange: [373, 375, true],
|
||||
sourceRange: topLevelRange(373, 375),
|
||||
argPosition: { type: 'objectProperty', key: 'angle' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineOfYLength',
|
||||
@ -559,7 +559,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'yRelative',
|
||||
isConstrained: false,
|
||||
value: '3',
|
||||
sourceRange: [390, 391, true],
|
||||
sourceRange: topLevelRange(390, 391),
|
||||
argPosition: { type: 'objectProperty', key: 'length' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineOfYLength',
|
||||
@ -573,7 +573,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'angle',
|
||||
isConstrained: false,
|
||||
value: '12.14',
|
||||
sourceRange: [434, 439, true],
|
||||
sourceRange: topLevelRange(434, 439),
|
||||
argPosition: { type: 'objectProperty', key: 'angle' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineToX',
|
||||
@ -582,7 +582,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'xAbsolute',
|
||||
isConstrained: false,
|
||||
value: '12',
|
||||
sourceRange: [450, 452, true],
|
||||
sourceRange: topLevelRange(450, 452),
|
||||
argPosition: { type: 'objectProperty', key: 'to' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineToX',
|
||||
@ -596,7 +596,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'angle',
|
||||
isConstrained: false,
|
||||
value: '30',
|
||||
sourceRange: [495, 497, true],
|
||||
sourceRange: topLevelRange(495, 497),
|
||||
argPosition: { type: 'objectProperty', key: 'angle' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineToY',
|
||||
@ -605,7 +605,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'yAbsolute',
|
||||
isConstrained: false,
|
||||
value: '10.14',
|
||||
sourceRange: [508, 513, true],
|
||||
sourceRange: topLevelRange(508, 513),
|
||||
argPosition: { type: 'objectProperty', key: 'to' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineToY',
|
||||
@ -619,7 +619,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'angle',
|
||||
isConstrained: false,
|
||||
value: '3.14',
|
||||
sourceRange: [567, 571, true],
|
||||
sourceRange: topLevelRange(567, 571),
|
||||
argPosition: { type: 'objectProperty', key: 'angle' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineThatIntersects',
|
||||
@ -628,7 +628,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'intersectionOffset',
|
||||
isConstrained: false,
|
||||
value: '0',
|
||||
sourceRange: [608, 609, true],
|
||||
sourceRange: topLevelRange(608, 609),
|
||||
argPosition: { type: 'objectProperty', key: 'offset' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineThatIntersects',
|
||||
@ -637,7 +637,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'intersectionTag',
|
||||
isConstrained: false,
|
||||
value: 'a',
|
||||
sourceRange: [592, 593, true],
|
||||
sourceRange: topLevelRange(592, 593),
|
||||
argPosition: {
|
||||
key: 'intersectTag',
|
||||
type: 'objectProperty',
|
||||
@ -654,7 +654,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'tangentialWithPrevious',
|
||||
isConstrained: true,
|
||||
value: 'tangentialArcTo',
|
||||
sourceRange: [623, 638, true],
|
||||
sourceRange: topLevelRange(623, 638),
|
||||
argPosition: undefined,
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'tangentialArcTo',
|
||||
@ -663,7 +663,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'xAbsolute',
|
||||
isConstrained: false,
|
||||
value: '3.14',
|
||||
sourceRange: [640, 644, true],
|
||||
sourceRange: topLevelRange(640, 644),
|
||||
argPosition: { type: 'arrayItem', index: 0 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'tangentialArcTo',
|
||||
@ -672,7 +672,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'yAbsolute',
|
||||
isConstrained: false,
|
||||
value: '13.14',
|
||||
sourceRange: [646, 651, true],
|
||||
sourceRange: topLevelRange(646, 651),
|
||||
argPosition: { type: 'arrayItem', index: 1 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'tangentialArcTo',
|
||||
@ -680,11 +680,11 @@ describe('testing getConstraintInfo', () => {
|
||||
],
|
||||
],
|
||||
])('testing %s when inputs are unconstrained', (functionName, expected) => {
|
||||
const sourceRange: SourceRange = [
|
||||
const ast = assertParse(code)
|
||||
const sourceRange = topLevelRange(
|
||||
code.indexOf(functionName),
|
||||
code.indexOf(functionName) + functionName.length,
|
||||
true,
|
||||
]
|
||||
code.indexOf(functionName) + functionName.length
|
||||
)
|
||||
if (err(ast)) return ast
|
||||
const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
|
||||
const callExp = getNodeFromPath<Node<CallExpression>>(
|
||||
@ -717,7 +717,6 @@ describe('testing getConstraintInfo', () => {
|
||||
offset = 0
|
||||
}, %)
|
||||
|> tangentialArcTo([3.14, 13.14], %)`
|
||||
const ast = assertParse(code)
|
||||
test.each([
|
||||
[
|
||||
`angledLine(`,
|
||||
@ -726,7 +725,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'angle',
|
||||
isConstrained: false,
|
||||
value: '3.14',
|
||||
sourceRange: [112, 116, true],
|
||||
sourceRange: topLevelRange(112, 116),
|
||||
argPosition: { type: 'arrayItem', index: 0 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLine',
|
||||
@ -735,7 +734,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'length',
|
||||
isConstrained: false,
|
||||
value: '3.14',
|
||||
sourceRange: [118, 122, true],
|
||||
sourceRange: topLevelRange(118, 122),
|
||||
argPosition: { type: 'arrayItem', index: 1 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLine',
|
||||
@ -749,7 +748,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'angle',
|
||||
isConstrained: false,
|
||||
value: '3.14',
|
||||
sourceRange: [277, 281, true],
|
||||
sourceRange: topLevelRange(277, 281),
|
||||
argPosition: { type: 'arrayItem', index: 0 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineOfXLength',
|
||||
@ -758,7 +757,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'xRelative',
|
||||
isConstrained: false,
|
||||
value: '3.14',
|
||||
sourceRange: [283, 287, true],
|
||||
sourceRange: topLevelRange(283, 287),
|
||||
argPosition: { type: 'arrayItem', index: 1 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineOfXLength',
|
||||
@ -772,7 +771,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'angle',
|
||||
isConstrained: false,
|
||||
value: '30',
|
||||
sourceRange: [321, 323, true],
|
||||
sourceRange: topLevelRange(321, 323),
|
||||
argPosition: { type: 'arrayItem', index: 0 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineOfYLength',
|
||||
@ -781,7 +780,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'yRelative',
|
||||
isConstrained: false,
|
||||
value: '3',
|
||||
sourceRange: [325, 326, true],
|
||||
sourceRange: topLevelRange(325, 326),
|
||||
argPosition: { type: 'arrayItem', index: 1 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineOfYLength',
|
||||
@ -795,7 +794,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'angle',
|
||||
isConstrained: false,
|
||||
value: '12',
|
||||
sourceRange: [354, 356, true],
|
||||
sourceRange: topLevelRange(354, 356),
|
||||
argPosition: { type: 'arrayItem', index: 0 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineToX',
|
||||
@ -804,7 +803,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'xAbsolute',
|
||||
isConstrained: false,
|
||||
value: '12',
|
||||
sourceRange: [358, 360, true],
|
||||
sourceRange: topLevelRange(358, 360),
|
||||
argPosition: { type: 'arrayItem', index: 1 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineToX',
|
||||
@ -818,7 +817,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'angle',
|
||||
isConstrained: false,
|
||||
value: '30',
|
||||
sourceRange: [388, 390, true],
|
||||
sourceRange: topLevelRange(388, 390),
|
||||
argPosition: { type: 'arrayItem', index: 0 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineToY',
|
||||
@ -827,7 +826,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'yAbsolute',
|
||||
isConstrained: false,
|
||||
value: '10',
|
||||
sourceRange: [392, 394, true],
|
||||
sourceRange: topLevelRange(392, 394),
|
||||
argPosition: { type: 'arrayItem', index: 1 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineToY',
|
||||
@ -835,11 +834,11 @@ describe('testing getConstraintInfo', () => {
|
||||
],
|
||||
],
|
||||
])('testing %s when inputs are unconstrained', (functionName, expected) => {
|
||||
const sourceRange: SourceRange = [
|
||||
const ast = assertParse(code)
|
||||
const sourceRange = topLevelRange(
|
||||
code.indexOf(functionName),
|
||||
code.indexOf(functionName) + functionName.length,
|
||||
true,
|
||||
]
|
||||
code.indexOf(functionName) + functionName.length
|
||||
)
|
||||
if (err(ast)) return ast
|
||||
const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
|
||||
const callExp = getNodeFromPath<Node<CallExpression>>(
|
||||
@ -872,7 +871,6 @@ describe('testing getConstraintInfo', () => {
|
||||
offset = 0 + 0
|
||||
}, %)
|
||||
|> tangentialArcTo([3.14 + 0, 13.14 + 0], %)`
|
||||
const ast = assertParse(code)
|
||||
test.each([
|
||||
[
|
||||
'line',
|
||||
@ -881,7 +879,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'xRelative',
|
||||
isConstrained: true,
|
||||
value: '3 + 0',
|
||||
sourceRange: [83, 88, true],
|
||||
sourceRange: topLevelRange(83, 88),
|
||||
argPosition: { type: 'arrayItem', index: 0 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'line',
|
||||
@ -890,7 +888,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'yRelative',
|
||||
isConstrained: true,
|
||||
value: '4 + 0',
|
||||
sourceRange: [90, 95, true],
|
||||
sourceRange: topLevelRange(90, 95),
|
||||
argPosition: { type: 'arrayItem', index: 1 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'line',
|
||||
@ -904,7 +902,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'angle',
|
||||
isConstrained: true,
|
||||
value: '3.14 + 0',
|
||||
sourceRange: [129, 137, true],
|
||||
sourceRange: topLevelRange(129, 137),
|
||||
argPosition: { type: 'objectProperty', key: 'angle' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLine',
|
||||
@ -913,7 +911,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'length',
|
||||
isConstrained: true,
|
||||
value: '3.14 + 0',
|
||||
sourceRange: [148, 156, true],
|
||||
sourceRange: topLevelRange(148, 156),
|
||||
argPosition: { type: 'objectProperty', key: 'length' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLine',
|
||||
@ -927,7 +925,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'xAbsolute',
|
||||
isConstrained: true,
|
||||
value: '6.14 + 0',
|
||||
sourceRange: [178, 186, true],
|
||||
sourceRange: topLevelRange(178, 186),
|
||||
argPosition: { type: 'arrayItem', index: 0 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'lineTo',
|
||||
@ -936,7 +934,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'yAbsolute',
|
||||
isConstrained: true,
|
||||
value: '3.14 + 0',
|
||||
sourceRange: [188, 196, true],
|
||||
sourceRange: topLevelRange(188, 196),
|
||||
argPosition: { type: 'arrayItem', index: 1 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'lineTo',
|
||||
@ -950,7 +948,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'horizontal',
|
||||
isConstrained: true,
|
||||
value: 'xLineTo',
|
||||
sourceRange: [209, 216, true],
|
||||
sourceRange: topLevelRange(209, 216),
|
||||
argPosition: undefined,
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'xLineTo',
|
||||
@ -959,7 +957,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'xAbsolute',
|
||||
isConstrained: true,
|
||||
value: '8 + 0',
|
||||
sourceRange: [217, 222, true],
|
||||
sourceRange: topLevelRange(217, 222),
|
||||
argPosition: { type: 'singleValue' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'xLineTo',
|
||||
@ -973,7 +971,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'vertical',
|
||||
isConstrained: true,
|
||||
value: 'yLineTo',
|
||||
sourceRange: [234, 241, true],
|
||||
sourceRange: topLevelRange(234, 241),
|
||||
argPosition: undefined,
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'yLineTo',
|
||||
@ -982,7 +980,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'yAbsolute',
|
||||
isConstrained: true,
|
||||
value: '5 + 0',
|
||||
sourceRange: [242, 247, true],
|
||||
sourceRange: topLevelRange(242, 247),
|
||||
argPosition: { type: 'singleValue' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'yLineTo',
|
||||
@ -996,7 +994,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'vertical',
|
||||
isConstrained: true,
|
||||
value: 'yLine',
|
||||
sourceRange: [259, 264, true],
|
||||
sourceRange: topLevelRange(259, 264),
|
||||
argPosition: undefined,
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'yLine',
|
||||
@ -1005,7 +1003,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'yRelative',
|
||||
isConstrained: true,
|
||||
value: '3.14 + 0',
|
||||
sourceRange: [265, 273, true],
|
||||
sourceRange: topLevelRange(265, 273),
|
||||
argPosition: { type: 'singleValue' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'yLine',
|
||||
@ -1019,7 +1017,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'horizontal',
|
||||
isConstrained: true,
|
||||
value: 'xLine',
|
||||
sourceRange: [289, 294, true],
|
||||
sourceRange: topLevelRange(289, 294),
|
||||
argPosition: undefined,
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'xLine',
|
||||
@ -1028,7 +1026,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'xRelative',
|
||||
isConstrained: true,
|
||||
value: '3.14 + 0',
|
||||
sourceRange: [295, 303, true],
|
||||
sourceRange: topLevelRange(295, 303),
|
||||
argPosition: { type: 'singleValue' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'xLine',
|
||||
@ -1042,7 +1040,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'angle',
|
||||
isConstrained: true,
|
||||
value: '3.14 + 0',
|
||||
sourceRange: [345, 353, true],
|
||||
sourceRange: topLevelRange(345, 353),
|
||||
argPosition: { type: 'objectProperty', key: 'angle' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineOfXLength',
|
||||
@ -1051,7 +1049,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'xRelative',
|
||||
isConstrained: true,
|
||||
value: '3.14 + 0',
|
||||
sourceRange: [364, 372, true],
|
||||
sourceRange: topLevelRange(364, 372),
|
||||
argPosition: { type: 'objectProperty', key: 'length' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineOfXLength',
|
||||
@ -1065,7 +1063,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'angle',
|
||||
isConstrained: true,
|
||||
value: '30 + 0',
|
||||
sourceRange: [416, 422, true],
|
||||
sourceRange: topLevelRange(416, 422),
|
||||
argPosition: { type: 'objectProperty', key: 'angle' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineOfYLength',
|
||||
@ -1074,7 +1072,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'yRelative',
|
||||
isConstrained: true,
|
||||
value: '3 + 0',
|
||||
sourceRange: [433, 438, true],
|
||||
sourceRange: topLevelRange(433, 438),
|
||||
argPosition: { type: 'objectProperty', key: 'length' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineOfYLength',
|
||||
@ -1088,7 +1086,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'angle',
|
||||
isConstrained: true,
|
||||
value: '12.14 + 0',
|
||||
sourceRange: [476, 485, true],
|
||||
sourceRange: topLevelRange(476, 485),
|
||||
argPosition: { type: 'objectProperty', key: 'angle' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineToX',
|
||||
@ -1097,7 +1095,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'xAbsolute',
|
||||
isConstrained: true,
|
||||
value: '12 + 0',
|
||||
sourceRange: [492, 498, true],
|
||||
sourceRange: topLevelRange(492, 498),
|
||||
argPosition: { type: 'objectProperty', key: 'to' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineToX',
|
||||
@ -1111,7 +1109,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'angle',
|
||||
isConstrained: true,
|
||||
value: '30 + 0',
|
||||
sourceRange: [536, 542, true],
|
||||
sourceRange: topLevelRange(536, 542),
|
||||
argPosition: { type: 'objectProperty', key: 'angle' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineToY',
|
||||
@ -1120,7 +1118,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'yAbsolute',
|
||||
isConstrained: true,
|
||||
value: '10.14 + 0',
|
||||
sourceRange: [549, 558, true],
|
||||
sourceRange: topLevelRange(549, 558),
|
||||
argPosition: { type: 'objectProperty', key: 'to' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineToY',
|
||||
@ -1134,7 +1132,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'angle',
|
||||
isConstrained: true,
|
||||
value: '3.14 + 0',
|
||||
sourceRange: [616, 624, true],
|
||||
sourceRange: topLevelRange(616, 624),
|
||||
argPosition: { type: 'objectProperty', key: 'angle' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineThatIntersects',
|
||||
@ -1143,7 +1141,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'intersectionOffset',
|
||||
isConstrained: true,
|
||||
value: '0 + 0',
|
||||
sourceRange: [671, 676, true],
|
||||
sourceRange: topLevelRange(671, 676),
|
||||
argPosition: { type: 'objectProperty', key: 'offset' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineThatIntersects',
|
||||
@ -1152,7 +1150,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'intersectionTag',
|
||||
isConstrained: false,
|
||||
value: 'a',
|
||||
sourceRange: [650, 651, true],
|
||||
sourceRange: topLevelRange(650, 651),
|
||||
argPosition: { key: 'intersectTag', type: 'objectProperty' },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'angledLineThatIntersects',
|
||||
@ -1166,7 +1164,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'tangentialWithPrevious',
|
||||
isConstrained: true,
|
||||
value: 'tangentialArcTo',
|
||||
sourceRange: [697, 712, true],
|
||||
sourceRange: topLevelRange(697, 712),
|
||||
argPosition: undefined,
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'tangentialArcTo',
|
||||
@ -1175,7 +1173,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'xAbsolute',
|
||||
isConstrained: true,
|
||||
value: '3.14 + 0',
|
||||
sourceRange: [714, 722, true],
|
||||
sourceRange: topLevelRange(714, 722),
|
||||
argPosition: { type: 'arrayItem', index: 0 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'tangentialArcTo',
|
||||
@ -1184,7 +1182,7 @@ describe('testing getConstraintInfo', () => {
|
||||
type: 'yAbsolute',
|
||||
isConstrained: true,
|
||||
value: '13.14 + 0',
|
||||
sourceRange: [724, 733, true],
|
||||
sourceRange: topLevelRange(724, 733),
|
||||
argPosition: { type: 'arrayItem', index: 1 },
|
||||
pathToNode: expect.any(Array),
|
||||
stdLibFnName: 'tangentialArcTo',
|
||||
@ -1192,11 +1190,11 @@ describe('testing getConstraintInfo', () => {
|
||||
],
|
||||
],
|
||||
])('testing %s when inputs are unconstrained', (functionName, expected) => {
|
||||
const sourceRange: SourceRange = [
|
||||
const ast = assertParse(code)
|
||||
const sourceRange = topLevelRange(
|
||||
code.indexOf(functionName),
|
||||
code.indexOf(functionName) + functionName.length,
|
||||
true,
|
||||
]
|
||||
code.indexOf(functionName) + functionName.length
|
||||
)
|
||||
if (err(ast)) return ast
|
||||
const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
|
||||
const callExp = getNodeFromPath<Node<CallExpression>>(
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
VariableDeclaration,
|
||||
Identifier,
|
||||
sketchFromKclValue,
|
||||
topLevelRange,
|
||||
} from 'lang/wasm'
|
||||
import {
|
||||
getNodeFromPath,
|
||||
@ -222,7 +223,7 @@ const commonConstraintInfoHelper = (
|
||||
code.slice(input1.start, input1.end),
|
||||
stdLibFnName,
|
||||
isArr ? abbreviatedInputs[0].arrayInput : abbreviatedInputs[0].objInput,
|
||||
[input1.start, input1.end, true],
|
||||
topLevelRange(input1.start, input1.end),
|
||||
pathToFirstArg
|
||||
)
|
||||
)
|
||||
@ -234,7 +235,7 @@ const commonConstraintInfoHelper = (
|
||||
code.slice(input2.start, input2.end),
|
||||
stdLibFnName,
|
||||
isArr ? abbreviatedInputs[1].arrayInput : abbreviatedInputs[1].objInput,
|
||||
[input2.start, input2.end, true],
|
||||
topLevelRange(input2.start, input2.end),
|
||||
pathToSecondArg
|
||||
)
|
||||
)
|
||||
@ -266,7 +267,7 @@ const horzVertConstraintInfoHelper = (
|
||||
callee.name,
|
||||
stdLibFnName,
|
||||
undefined,
|
||||
[callee.start, callee.end, true],
|
||||
topLevelRange(callee.start, callee.end),
|
||||
pathToCallee
|
||||
),
|
||||
constrainInfo(
|
||||
@ -275,7 +276,7 @@ const horzVertConstraintInfoHelper = (
|
||||
code.slice(firstArg.start, firstArg.end),
|
||||
stdLibFnName,
|
||||
abbreviatedInput,
|
||||
[firstArg.start, firstArg.end, true],
|
||||
topLevelRange(firstArg.start, firstArg.end),
|
||||
pathToFirstArg
|
||||
),
|
||||
]
|
||||
@ -905,7 +906,7 @@ export const tangentialArcTo: SketchLineHelper = {
|
||||
callee.name,
|
||||
'tangentialArcTo',
|
||||
undefined,
|
||||
[callee.start, callee.end, true],
|
||||
topLevelRange(callee.start, callee.end),
|
||||
pathToCallee
|
||||
),
|
||||
constrainInfo(
|
||||
@ -914,7 +915,7 @@ export const tangentialArcTo: SketchLineHelper = {
|
||||
code.slice(firstArg.elements[0].start, firstArg.elements[0].end),
|
||||
'tangentialArcTo',
|
||||
0,
|
||||
[firstArg.elements[0].start, firstArg.elements[0].end, true],
|
||||
topLevelRange(firstArg.elements[0].start, firstArg.elements[0].end),
|
||||
pathToFirstArg
|
||||
),
|
||||
constrainInfo(
|
||||
@ -923,7 +924,7 @@ export const tangentialArcTo: SketchLineHelper = {
|
||||
code.slice(firstArg.elements[1].start, firstArg.elements[1].end),
|
||||
'tangentialArcTo',
|
||||
1,
|
||||
[firstArg.elements[1].start, firstArg.elements[1].end, true],
|
||||
topLevelRange(firstArg.elements[1].start, firstArg.elements[1].end),
|
||||
pathToSecondArg
|
||||
),
|
||||
]
|
||||
@ -1052,7 +1053,7 @@ export const circle: SketchLineHelper = {
|
||||
code.slice(radiusDetails.expr.start, radiusDetails.expr.end),
|
||||
'circle',
|
||||
'radius',
|
||||
[radiusDetails.expr.start, radiusDetails.expr.end, true],
|
||||
topLevelRange(radiusDetails.expr.start, radiusDetails.expr.end),
|
||||
pathToRadiusLiteral
|
||||
),
|
||||
{
|
||||
@ -1061,11 +1062,10 @@ export const circle: SketchLineHelper = {
|
||||
isConstrained: isNotLiteralArrayOrStatic(
|
||||
centerDetails.expr.elements[0]
|
||||
),
|
||||
sourceRange: [
|
||||
sourceRange: topLevelRange(
|
||||
centerDetails.expr.elements[0].start,
|
||||
centerDetails.expr.elements[0].end,
|
||||
true,
|
||||
],
|
||||
centerDetails.expr.elements[0].end
|
||||
),
|
||||
pathToNode: pathToXArg,
|
||||
value: code.slice(
|
||||
centerDetails.expr.elements[0].start,
|
||||
@ -1083,11 +1083,10 @@ export const circle: SketchLineHelper = {
|
||||
isConstrained: isNotLiteralArrayOrStatic(
|
||||
centerDetails.expr.elements[1]
|
||||
),
|
||||
sourceRange: [
|
||||
sourceRange: topLevelRange(
|
||||
centerDetails.expr.elements[1].start,
|
||||
centerDetails.expr.elements[1].end,
|
||||
true,
|
||||
],
|
||||
centerDetails.expr.elements[1].end
|
||||
),
|
||||
pathToNode: pathToYArg,
|
||||
value: code.slice(
|
||||
centerDetails.expr.elements[1].start,
|
||||
@ -1763,7 +1762,7 @@ export const angledLineThatIntersects: SketchLineHelper = {
|
||||
code.slice(angle.start, angle.end),
|
||||
'angledLineThatIntersects',
|
||||
'angle',
|
||||
[angle.start, angle.end, true],
|
||||
topLevelRange(angle.start, angle.end),
|
||||
pathToAngleProp
|
||||
)
|
||||
)
|
||||
@ -1782,7 +1781,7 @@ export const angledLineThatIntersects: SketchLineHelper = {
|
||||
code.slice(offset.start, offset.end),
|
||||
'angledLineThatIntersects',
|
||||
'offset',
|
||||
[offset.start, offset.end, true],
|
||||
topLevelRange(offset.start, offset.end),
|
||||
pathToOffsetProp
|
||||
)
|
||||
)
|
||||
@ -1801,7 +1800,7 @@ export const angledLineThatIntersects: SketchLineHelper = {
|
||||
code.slice(tag.start, tag.end),
|
||||
'angledLineThatIntersects',
|
||||
'intersectTag',
|
||||
[tag.start, tag.end, true],
|
||||
topLevelRange(tag.start, tag.end),
|
||||
pathToTagProp
|
||||
)
|
||||
returnVal.push(info)
|
||||
|
||||
@ -5,6 +5,7 @@ import {
|
||||
initPromise,
|
||||
sketchFromKclValue,
|
||||
SourceRange,
|
||||
topLevelRange,
|
||||
} from '../wasm'
|
||||
import {
|
||||
ConstraintType,
|
||||
@ -31,10 +32,10 @@ async function testingSwapSketchFnCall({
|
||||
constraintType: ConstraintType
|
||||
}): Promise<{
|
||||
newCode: string
|
||||
originalRange: [number, number, boolean]
|
||||
originalRange: SourceRange
|
||||
}> {
|
||||
const startIndex = inputCode.indexOf(callToSwap)
|
||||
const range: SourceRange = [startIndex, startIndex + callToSwap.length, true]
|
||||
const range = topLevelRange(startIndex, startIndex + callToSwap.length)
|
||||
const ast = assertParse(inputCode)
|
||||
|
||||
const execState = await enginelessExecutor(ast)
|
||||
@ -375,7 +376,10 @@ part001 = startSketchOn('XY')
|
||||
execState.memory.get('part001'),
|
||||
'part001'
|
||||
) as Sketch
|
||||
const _segment = getSketchSegmentFromSourceRange(sg, [index, index, true])
|
||||
const _segment = getSketchSegmentFromSourceRange(
|
||||
sg,
|
||||
topLevelRange(index, index)
|
||||
)
|
||||
if (err(_segment)) throw _segment
|
||||
const { __geoMeta, ...segment } = _segment.segment
|
||||
expect(segment).toEqual({
|
||||
@ -390,7 +394,7 @@ part001 = startSketchOn('XY')
|
||||
const index = code.indexOf('// segment-in-start') - 7
|
||||
const _segment = getSketchSegmentFromSourceRange(
|
||||
sketchFromKclValue(execState.memory.get('part001'), 'part001') as Sketch,
|
||||
[index, index, true]
|
||||
topLevelRange(index, index)
|
||||
)
|
||||
if (err(_segment)) throw _segment
|
||||
const { __geoMeta, ...segment } = _segment.segment
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
Path,
|
||||
PathToNode,
|
||||
Expr,
|
||||
topLevelRange,
|
||||
} from '../wasm'
|
||||
import { err } from 'lib/trap'
|
||||
|
||||
@ -31,7 +32,7 @@ export function getSketchSegmentFromPathToNode(
|
||||
const node = nodeMeta.node
|
||||
if (!node || typeof node.start !== 'number' || !node.end)
|
||||
return new Error('no node found')
|
||||
const sourceRange: SourceRange = [node.start, node.end, true]
|
||||
const sourceRange = topLevelRange(node.start, node.end)
|
||||
return getSketchSegmentFromSourceRange(sketch, sourceRange)
|
||||
}
|
||||
export function getSketchSegmentFromSourceRange(
|
||||
|
||||
@ -1,4 +1,11 @@
|
||||
import { assertParse, Expr, recast, initPromise, Program } from '../wasm'
|
||||
import {
|
||||
assertParse,
|
||||
Expr,
|
||||
recast,
|
||||
initPromise,
|
||||
Program,
|
||||
topLevelRange,
|
||||
} from '../wasm'
|
||||
import {
|
||||
getConstraintType,
|
||||
getTransformInfos,
|
||||
@ -125,7 +132,7 @@ describe('testing transformAstForSketchLines for equal length constraint', () =>
|
||||
)
|
||||
}
|
||||
const start = codeBeforeLine + line.indexOf('|> ' + 5)
|
||||
const range: [number, number, boolean] = [start, start, true]
|
||||
const range = topLevelRange(start, start)
|
||||
return {
|
||||
codeRef: codeRefFromRange(range, ast),
|
||||
}
|
||||
@ -297,7 +304,7 @@ part001 = startSketchOn('XY')
|
||||
const comment = ln.split('//')[1]
|
||||
const start = inputScript.indexOf('//' + comment) - 7
|
||||
return {
|
||||
codeRef: codeRefFromRange([start, start, true], ast),
|
||||
codeRef: codeRefFromRange(topLevelRange(start, start), ast),
|
||||
}
|
||||
})
|
||||
|
||||
@ -386,7 +393,7 @@ part001 = startSketchOn('XY')
|
||||
const comment = ln.split('//')[1]
|
||||
const start = inputScript.indexOf('//' + comment) - 7
|
||||
return {
|
||||
codeRef: codeRefFromRange([start, start, true], ast),
|
||||
codeRef: codeRefFromRange(topLevelRange(start, start), ast),
|
||||
}
|
||||
})
|
||||
|
||||
@ -446,7 +453,7 @@ part001 = startSketchOn('XY')
|
||||
const comment = ln.split('//')[1]
|
||||
const start = inputScript.indexOf('//' + comment) - 7
|
||||
return {
|
||||
codeRef: codeRefFromRange([start, start, true], ast),
|
||||
codeRef: codeRefFromRange(topLevelRange(start, start), ast),
|
||||
}
|
||||
})
|
||||
|
||||
@ -541,7 +548,7 @@ async function helperThing(
|
||||
const comment = ln.split('//')[1]
|
||||
const start = inputScript.indexOf('//' + comment) - 7
|
||||
return {
|
||||
codeRef: codeRefFromRange([start, start, true], ast),
|
||||
codeRef: codeRefFromRange(topLevelRange(start, start), ast),
|
||||
}
|
||||
})
|
||||
|
||||
@ -610,7 +617,7 @@ part001 = startSketchOn('XY')
|
||||
}
|
||||
const offsetIndex = index - 7
|
||||
const expectedConstraintLevel = getConstraintLevelFromSourceRange(
|
||||
[offsetIndex, offsetIndex, true],
|
||||
topLevelRange(offsetIndex, offsetIndex),
|
||||
ast
|
||||
)
|
||||
if (err(expectedConstraintLevel)) {
|
||||
|
||||
@ -20,12 +20,12 @@ import {
|
||||
sketchFromKclValue,
|
||||
Literal,
|
||||
SourceRange,
|
||||
LiteralValue,
|
||||
} from '../wasm'
|
||||
import {
|
||||
getNodeFromPath,
|
||||
getNodeFromPathCurry,
|
||||
getNodePathFromSourceRange,
|
||||
isValueZero,
|
||||
} from '../queryAst'
|
||||
import {
|
||||
createArrayExpression,
|
||||
@ -79,11 +79,32 @@ export type ConstraintType =
|
||||
| 'setAngleBetween'
|
||||
|
||||
const REF_NUM_ERR = new Error('Referenced segment does not have a to value')
|
||||
|
||||
function asNum(val: LiteralValue): number | Error {
|
||||
if (typeof val === 'object') return val.value
|
||||
return REF_NUM_ERR
|
||||
}
|
||||
|
||||
function forceNum(arg: Literal): number {
|
||||
if (typeof arg.value === 'boolean' || typeof arg.value === 'string') {
|
||||
return Number(arg.value)
|
||||
} else {
|
||||
return arg.value.value
|
||||
}
|
||||
}
|
||||
|
||||
function isUndef(val: any): val is undefined {
|
||||
return typeof val === 'undefined'
|
||||
}
|
||||
function isNum(val: any): val is number {
|
||||
return typeof val === 'number'
|
||||
|
||||
function isValueZero(val?: Expr): boolean {
|
||||
return (
|
||||
(val?.type === 'Literal' && forceNum(val) === 0) ||
|
||||
(val?.type === 'UnaryExpression' &&
|
||||
val.operator === '-' &&
|
||||
val.argument.type === 'Literal' &&
|
||||
Number(val.argument.value) === 0)
|
||||
)
|
||||
}
|
||||
|
||||
function createCallWrapper(
|
||||
@ -190,7 +211,7 @@ const xyLineSetLength =
|
||||
: referenceSeg
|
||||
? segRef
|
||||
: args[0].expr
|
||||
const literalARg = getArgLiteralVal(args[0].expr)
|
||||
const literalARg = asNum(args[0].expr.value)
|
||||
if (err(literalARg)) return literalARg
|
||||
return createCallWrapper(xOrY, lineVal, tag, literalARg)
|
||||
}
|
||||
@ -211,13 +232,14 @@ const basicAngledLineCreateNode =
|
||||
referencedSegment: path,
|
||||
}) => {
|
||||
const refAng = path ? getAngle(path?.from, path?.to) : 0
|
||||
if (!isNum(args[0].expr.value)) return REF_NUM_ERR
|
||||
const argValue = asNum(args[0].expr.value)
|
||||
if (err(argValue)) return argValue
|
||||
const nonForcedAng =
|
||||
varValToUse === 'ang'
|
||||
? inputs[0].expr
|
||||
: referenceSeg === 'ang'
|
||||
? getClosesAngleDirection(
|
||||
args[0].expr.value,
|
||||
argValue,
|
||||
refAng,
|
||||
createSegAngle(referenceSegName)
|
||||
)
|
||||
@ -230,8 +252,8 @@ const basicAngledLineCreateNode =
|
||||
: args[1].expr
|
||||
const shouldForceAng = valToForce === 'ang' && forceValueUsedInTransform
|
||||
const shouldForceLen = valToForce === 'len' && forceValueUsedInTransform
|
||||
const literalArg = getArgLiteralVal(
|
||||
valToForce === 'ang' ? args[0].expr : args[1].expr
|
||||
const literalArg = asNum(
|
||||
valToForce === 'ang' ? args[0].expr.value : args[1].expr.value
|
||||
)
|
||||
if (err(literalArg)) return literalArg
|
||||
return createCallWrapper(
|
||||
@ -283,7 +305,7 @@ const getMinAndSegAngVals = (
|
||||
}
|
||||
|
||||
const getSignedLeg = (arg: Literal, legLenVal: BinaryPart) =>
|
||||
Number(arg.value) < 0 ? createUnaryExpression(legLenVal) : legLenVal
|
||||
forceNum(arg) < 0 ? createUnaryExpression(legLenVal) : legLenVal
|
||||
|
||||
const getLegAng = (ang: number, legAngleVal: BinaryPart) => {
|
||||
const normalisedAngle = ((ang % 360) + 360) % 360 // between 0 and 360
|
||||
@ -322,8 +344,7 @@ const setHorzVertDistanceCreateNode =
|
||||
referencedSegment,
|
||||
}) => {
|
||||
const refNum = referencedSegment?.to?.[index]
|
||||
const literalArg = getArgLiteralVal(args?.[index].expr)
|
||||
if (err(literalArg)) return literalArg
|
||||
const literalArg = asNum(args?.[index].expr.value)
|
||||
if (isUndef(refNum) || err(literalArg)) return REF_NUM_ERR
|
||||
|
||||
const valueUsedInTransform = roundOff(literalArg - refNum, 2)
|
||||
@ -352,7 +373,7 @@ const setHorzVertDistanceForAngleLineCreateNode =
|
||||
referencedSegment,
|
||||
}) => {
|
||||
const refNum = referencedSegment?.to?.[index]
|
||||
const literalArg = getArgLiteralVal(args?.[1].expr)
|
||||
const literalArg = asNum(args?.[1].expr.value)
|
||||
if (isUndef(refNum) || err(literalArg)) return REF_NUM_ERR
|
||||
const valueUsedInTransform = roundOff(literalArg - refNum, 2)
|
||||
const binExp = createBinaryExpressionWithUnary([
|
||||
@ -374,8 +395,8 @@ const setAbsDistanceCreateNode =
|
||||
index = xOrY === 'x' ? 0 : 1
|
||||
): CreateStdLibSketchCallExpr =>
|
||||
({ tag, forceValueUsedInTransform, rawArgs: args }) => {
|
||||
const literalArg = getArgLiteralVal(args?.[index].expr)
|
||||
if (err(literalArg)) return REF_NUM_ERR
|
||||
const literalArg = asNum(args?.[index].expr.value)
|
||||
if (err(literalArg)) return literalArg
|
||||
const valueUsedInTransform = roundOff(literalArg, 2)
|
||||
const val = forceValueUsedInTransform || createLiteral(valueUsedInTransform)
|
||||
if (isXOrYLine) {
|
||||
@ -396,8 +417,8 @@ const setAbsDistanceCreateNode =
|
||||
const setAbsDistanceForAngleLineCreateNode =
|
||||
(xOrY: 'x' | 'y'): CreateStdLibSketchCallExpr =>
|
||||
({ tag, forceValueUsedInTransform, inputs, rawArgs: args }) => {
|
||||
const literalArg = getArgLiteralVal(args?.[1].expr)
|
||||
if (err(literalArg)) return REF_NUM_ERR
|
||||
const literalArg = asNum(args?.[1].expr.value)
|
||||
if (err(literalArg)) return literalArg
|
||||
const valueUsedInTransform = roundOff(literalArg, 2)
|
||||
const val = forceValueUsedInTransform || createLiteral(valueUsedInTransform)
|
||||
return createCallWrapper(
|
||||
@ -419,7 +440,7 @@ const setHorVertDistanceForXYLines =
|
||||
}) => {
|
||||
const index = xOrY === 'x' ? 0 : 1
|
||||
const refNum = referencedSegment?.to?.[index]
|
||||
const literalArg = getArgLiteralVal(args?.[index].expr)
|
||||
const literalArg = asNum(args?.[index].expr.value)
|
||||
if (isUndef(refNum) || err(literalArg)) return REF_NUM_ERR
|
||||
const valueUsedInTransform = roundOff(literalArg - refNum, 2)
|
||||
const makeBinExp = createBinaryExpressionWithUnary([
|
||||
@ -445,9 +466,9 @@ const setHorzVertDistanceConstraintLineCreateNode =
|
||||
])
|
||||
|
||||
const makeBinExp = (index: 0 | 1) => {
|
||||
const arg = getArgLiteralVal(args?.[index].expr)
|
||||
const arg = asNum(args?.[index].expr.value)
|
||||
const refNum = referencedSegment?.to?.[index]
|
||||
if (err(arg) || !isNum(refNum)) return REF_NUM_ERR
|
||||
if (err(arg) || isUndef(refNum)) return REF_NUM_ERR
|
||||
return createBinaryExpressionWithUnary([
|
||||
createSegEnd(referenceSegName, isX),
|
||||
createLiteral(roundOff(arg - refNum, 2)),
|
||||
@ -468,9 +489,9 @@ const setAngledIntersectLineForLines: CreateStdLibSketchCallExpr = ({
|
||||
forceValueUsedInTransform,
|
||||
rawArgs: args,
|
||||
}) => {
|
||||
const val = args[1].expr.value,
|
||||
angle = args[0].expr.value
|
||||
if (!isNum(val) || !isNum(angle)) return REF_NUM_ERR
|
||||
const val = asNum(args[1].expr.value),
|
||||
angle = asNum(args[0].expr.value)
|
||||
if (err(val) || err(angle)) return REF_NUM_ERR
|
||||
const valueUsedInTransform = roundOff(val, 2)
|
||||
const varNamMap: { [key: number]: string } = {
|
||||
0: 'ZERO',
|
||||
@ -498,8 +519,8 @@ const setAngledIntersectForAngledLines: CreateStdLibSketchCallExpr = ({
|
||||
inputs,
|
||||
rawArgs: args,
|
||||
}) => {
|
||||
const val = args[1].expr.value
|
||||
if (!isNum(val)) return REF_NUM_ERR
|
||||
const val = asNum(args[1].expr.value)
|
||||
if (err(val)) return val
|
||||
const valueUsedInTransform = roundOff(val, 2)
|
||||
return intersectCallWrapper({
|
||||
fnName: 'angledLineThatIntersects',
|
||||
@ -524,8 +545,8 @@ const setAngleBetweenCreateNode =
|
||||
const refAngle = referencedSegment
|
||||
? getAngle(referencedSegment?.from, referencedSegment?.to)
|
||||
: 0
|
||||
const val = args[0].expr.value
|
||||
if (!isNum(val)) return REF_NUM_ERR
|
||||
const val = asNum(args[0].expr.value)
|
||||
if (err(val)) return val
|
||||
let valueUsedInTransform = roundOff(normaliseAngle(val - refAngle))
|
||||
let firstHalfValue = createSegAngle(referenceSegName)
|
||||
if (Math.abs(valueUsedInTransform) > 90) {
|
||||
@ -706,13 +727,11 @@ const transformMap: TransformMap = {
|
||||
createPipeSubstitution(),
|
||||
]
|
||||
)
|
||||
if (!isNum(args[0].expr.value)) return REF_NUM_ERR
|
||||
const val = asNum(args[0].expr.value)
|
||||
if (err(val)) return val
|
||||
return createCallWrapper(
|
||||
'angledLineToX',
|
||||
[
|
||||
getAngleLengthSign(args[0].expr.value, angleToMatchLengthXCall),
|
||||
inputs[0].expr,
|
||||
],
|
||||
[getAngleLengthSign(val, angleToMatchLengthXCall), inputs[0].expr],
|
||||
tag
|
||||
)
|
||||
},
|
||||
@ -739,13 +758,11 @@ const transformMap: TransformMap = {
|
||||
createPipeSubstitution(),
|
||||
]
|
||||
)
|
||||
if (!isNum(args[0].expr.value)) return REF_NUM_ERR
|
||||
const val = asNum(args[0].expr.value)
|
||||
if (err(val)) return val
|
||||
return createCallWrapper(
|
||||
'angledLineToY',
|
||||
[
|
||||
getAngleLengthSign(args[0].expr.value, angleToMatchLengthYCall),
|
||||
inputs[1].expr,
|
||||
],
|
||||
[getAngleLengthSign(val, angleToMatchLengthYCall), inputs[1].expr],
|
||||
tag
|
||||
)
|
||||
},
|
||||
@ -763,7 +780,7 @@ const transformMap: TransformMap = {
|
||||
forceValueUsedInTransform,
|
||||
rawArgs: args,
|
||||
}) => {
|
||||
const val = getArgLiteralVal(args[0].expr)
|
||||
const val = asNum(args[0].expr.value)
|
||||
if (err(val)) return val
|
||||
return createCallWrapper(
|
||||
'angledLineToY',
|
||||
@ -844,7 +861,7 @@ const transformMap: TransformMap = {
|
||||
tooltip: 'yLine',
|
||||
createNode: ({ inputs, tag, rawArgs: args }) => {
|
||||
const expr = inputs[1].expr
|
||||
if (Number(args[0].expr.value) >= 0)
|
||||
if (forceNum(args[0].expr) >= 0)
|
||||
return createCallWrapper('yLine', expr, tag)
|
||||
if (isExprBinaryPart(expr))
|
||||
return createCallWrapper('yLine', createUnaryExpression(expr), tag)
|
||||
@ -856,7 +873,7 @@ const transformMap: TransformMap = {
|
||||
tooltip: 'xLine',
|
||||
createNode: ({ inputs, tag, rawArgs: args }) => {
|
||||
const expr = inputs[1].expr
|
||||
if (Number(args[0].expr.value) >= 0)
|
||||
if (forceNum(args[0].expr) >= 0)
|
||||
return createCallWrapper('xLine', expr, tag)
|
||||
if (isExprBinaryPart(expr))
|
||||
return createCallWrapper('xLine', createUnaryExpression(expr), tag)
|
||||
@ -900,10 +917,11 @@ const transformMap: TransformMap = {
|
||||
referenceSegName,
|
||||
getInputOfType(inputs, 'xRelative').expr
|
||||
)
|
||||
if (!isNum(args[0].expr.value)) return REF_NUM_ERR
|
||||
const val = asNum(args[0].expr.value)
|
||||
if (err(val)) return val
|
||||
return createCallWrapper(
|
||||
'angledLineOfXLength',
|
||||
[getLegAng(args[0].expr.value, legAngle), minVal],
|
||||
[getLegAng(val, legAngle), minVal],
|
||||
tag
|
||||
)
|
||||
},
|
||||
@ -912,7 +930,7 @@ const transformMap: TransformMap = {
|
||||
tooltip: 'xLine',
|
||||
createNode: ({ inputs, tag, rawArgs: args }) => {
|
||||
const expr = inputs[1].expr
|
||||
if (Number(args[0].expr.value) >= 0)
|
||||
if (forceNum(args[0].expr) >= 0)
|
||||
return createCallWrapper('xLine', expr, tag)
|
||||
if (isExprBinaryPart(expr))
|
||||
return createCallWrapper('xLine', createUnaryExpression(expr), tag)
|
||||
@ -953,10 +971,11 @@ const transformMap: TransformMap = {
|
||||
inputs[1].expr,
|
||||
'legAngY'
|
||||
)
|
||||
if (!isNum(args[0].expr.value)) return REF_NUM_ERR
|
||||
const val = asNum(args[0].expr.value)
|
||||
if (err(val)) return val
|
||||
return createCallWrapper(
|
||||
'angledLineOfXLength',
|
||||
[getLegAng(args[0].expr.value, legAngle), minVal],
|
||||
[getLegAng(val, legAngle), minVal],
|
||||
tag
|
||||
)
|
||||
},
|
||||
@ -965,7 +984,7 @@ const transformMap: TransformMap = {
|
||||
tooltip: 'yLine',
|
||||
createNode: ({ inputs, tag, rawArgs: args }) => {
|
||||
const expr = inputs[1].expr
|
||||
if (Number(args[0].expr.value) >= 0)
|
||||
if (forceNum(args[0].expr) >= 0)
|
||||
return createCallWrapper('yLine', expr, tag)
|
||||
if (isExprBinaryPart(expr))
|
||||
return createCallWrapper('yLine', createUnaryExpression(expr), tag)
|
||||
@ -1005,13 +1024,11 @@ const transformMap: TransformMap = {
|
||||
createPipeSubstitution(),
|
||||
]
|
||||
)
|
||||
if (!isNum(args[0].expr.value)) return REF_NUM_ERR
|
||||
const val = asNum(args[0].expr.value)
|
||||
if (err(val)) return val
|
||||
return createCallWrapper(
|
||||
'angledLineToX',
|
||||
[
|
||||
getAngleLengthSign(args[0].expr.value, angleToMatchLengthXCall),
|
||||
inputs[1].expr,
|
||||
],
|
||||
[getAngleLengthSign(val, angleToMatchLengthXCall), inputs[1].expr],
|
||||
tag
|
||||
)
|
||||
},
|
||||
@ -1057,13 +1074,11 @@ const transformMap: TransformMap = {
|
||||
createPipeSubstitution(),
|
||||
]
|
||||
)
|
||||
if (!isNum(args[0].expr.value)) return REF_NUM_ERR
|
||||
const val = asNum(args[0].expr.value)
|
||||
if (err(val)) return val
|
||||
return createCallWrapper(
|
||||
'angledLineToY',
|
||||
[
|
||||
getAngleLengthSign(args[0].expr.value, angleToMatchLengthXCall),
|
||||
inputs[1].expr,
|
||||
],
|
||||
[getAngleLengthSign(val, angleToMatchLengthXCall), inputs[1].expr],
|
||||
tag
|
||||
)
|
||||
},
|
||||
@ -1080,7 +1095,7 @@ const transformMap: TransformMap = {
|
||||
equalLength: {
|
||||
tooltip: 'xLine',
|
||||
createNode: ({ referenceSegName, tag, rawArgs: args }) => {
|
||||
const argVal = getArgLiteralVal(args[0].expr)
|
||||
const argVal = asNum(args[0].expr.value)
|
||||
if (err(argVal)) return argVal
|
||||
const segLen = createSegLen(referenceSegName)
|
||||
if (argVal > 0) return createCallWrapper('xLine', segLen, tag, argVal)
|
||||
@ -1118,7 +1133,7 @@ const transformMap: TransformMap = {
|
||||
equalLength: {
|
||||
tooltip: 'yLine',
|
||||
createNode: ({ referenceSegName, tag, rawArgs: args }) => {
|
||||
const argVal = getArgLiteralVal(args[0].expr)
|
||||
const argVal = asNum(args[0].expr.value)
|
||||
if (err(argVal)) return argVal
|
||||
let segLen = createSegLen(referenceSegName)
|
||||
if (argVal < 0) segLen = createUnaryExpression(segLen)
|
||||
@ -1714,7 +1729,7 @@ export function transformAstSketchLines({
|
||||
let kclVal = programMemory.get(varName)
|
||||
let sketch
|
||||
if (kclVal?.type === 'Solid') {
|
||||
sketch = kclVal.sketch
|
||||
sketch = kclVal.value.sketch
|
||||
} else {
|
||||
sketch = sketchFromKclValue(kclVal, varName)
|
||||
if (err(sketch)) {
|
||||
@ -1823,11 +1838,6 @@ function createLastSeg(isX: boolean): Node<CallExpression> {
|
||||
])
|
||||
}
|
||||
|
||||
function getArgLiteralVal(arg: Literal): number | Error {
|
||||
if (!isNum(arg.value)) return REF_NUM_ERR
|
||||
return arg.value
|
||||
}
|
||||
|
||||
export type ConstraintLevel = 'free' | 'partial' | 'full'
|
||||
|
||||
export function getConstraintLevelFromSourceRange(
|
||||
|
||||
@ -5,8 +5,9 @@ import {
|
||||
Literal,
|
||||
ArrayExpression,
|
||||
BinaryExpression,
|
||||
ArtifactGraph,
|
||||
} from './wasm'
|
||||
import { ArtifactGraph, filterArtifacts } from 'lang/std/artifactGraph'
|
||||
import { filterArtifacts } from 'lang/std/artifactGraph'
|
||||
import { isOverlap } from 'lib/utils'
|
||||
|
||||
export function updatePathToNodeFromMap(
|
||||
|
||||
111
src/lang/wasm.ts
111
src/lang/wasm.ts
@ -44,17 +44,30 @@ import { EnvironmentRef } from '../wasm-lib/kcl/bindings/EnvironmentRef'
|
||||
import { Environment } from '../wasm-lib/kcl/bindings/Environment'
|
||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
import { CompilationError } from 'wasm-lib/kcl/bindings/CompilationError'
|
||||
import { SourceRange as RustSourceRange } from 'wasm-lib/kcl/bindings/SourceRange'
|
||||
import { SourceRange } from 'wasm-lib/kcl/bindings/SourceRange'
|
||||
import { getAllCurrentSettings } from 'lib/settings/settingsUtils'
|
||||
import { Operation } from 'wasm-lib/kcl/bindings/Operation'
|
||||
import { KclErrorWithOutputs } from 'wasm-lib/kcl/bindings/KclErrorWithOutputs'
|
||||
import { Artifact } from 'wasm-lib/kcl/bindings/Artifact'
|
||||
import { ArtifactId } from 'wasm-lib/kcl/bindings/ArtifactId'
|
||||
import { ArtifactCommand } from 'wasm-lib/kcl/bindings/ArtifactCommand'
|
||||
import { Artifact as RustArtifact } from 'wasm-lib/kcl/bindings/Artifact'
|
||||
import { ArtifactId } from 'wasm-lib/kcl/bindings/Artifact'
|
||||
import { ArtifactCommand } from 'wasm-lib/kcl/bindings/Artifact'
|
||||
import { ArtifactGraph as RustArtifactGraph } from 'wasm-lib/kcl/bindings/Artifact'
|
||||
import { Artifact } from './std/artifactGraph'
|
||||
import { getNodePathFromSourceRange } from './queryAst'
|
||||
|
||||
export type { Artifact } from 'wasm-lib/kcl/bindings/Artifact'
|
||||
export type { ArtifactCommand } from 'wasm-lib/kcl/bindings/ArtifactCommand'
|
||||
export type { ArtifactId } from 'wasm-lib/kcl/bindings/ArtifactId'
|
||||
export type { ArtifactCommand } from 'wasm-lib/kcl/bindings/Artifact'
|
||||
export type { ArtifactId } from 'wasm-lib/kcl/bindings/Artifact'
|
||||
export type { Cap as CapArtifact } from 'wasm-lib/kcl/bindings/Artifact'
|
||||
export type { CodeRef } from 'wasm-lib/kcl/bindings/Artifact'
|
||||
export type { EdgeCut } from 'wasm-lib/kcl/bindings/Artifact'
|
||||
export type { Path as PathArtifact } from 'wasm-lib/kcl/bindings/Artifact'
|
||||
export type { Plane as PlaneArtifact } from 'wasm-lib/kcl/bindings/Artifact'
|
||||
export type { Segment as SegmentArtifact } from 'wasm-lib/kcl/bindings/Artifact'
|
||||
export type { Solid2d as Solid2dArtifact } from 'wasm-lib/kcl/bindings/Artifact'
|
||||
export type { Sweep as SweepArtifact } from 'wasm-lib/kcl/bindings/Artifact'
|
||||
export type { SweepEdge } from 'wasm-lib/kcl/bindings/Artifact'
|
||||
export type { Wall as WallArtifact } from 'wasm-lib/kcl/bindings/Artifact'
|
||||
export type { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
||||
export type { Program } from '../wasm-lib/kcl/bindings/Program'
|
||||
export type { Expr } from '../wasm-lib/kcl/bindings/Expr'
|
||||
@ -76,7 +89,7 @@ export type { BinaryPart } from '../wasm-lib/kcl/bindings/BinaryPart'
|
||||
export type { Literal } from '../wasm-lib/kcl/bindings/Literal'
|
||||
export type { LiteralValue } from '../wasm-lib/kcl/bindings/LiteralValue'
|
||||
export type { ArrayExpression } from '../wasm-lib/kcl/bindings/ArrayExpression'
|
||||
export type { SourceRange as RustSourceRange } from 'wasm-lib/kcl/bindings/SourceRange'
|
||||
export type { SourceRange } from 'wasm-lib/kcl/bindings/SourceRange'
|
||||
|
||||
export type SyntaxType =
|
||||
| 'Program'
|
||||
@ -105,35 +118,36 @@ export type { Solid } from '../wasm-lib/kcl/bindings/Solid'
|
||||
export type { KclValue } from '../wasm-lib/kcl/bindings/KclValue'
|
||||
export type { ExtrudeSurface } from '../wasm-lib/kcl/bindings/ExtrudeSurface'
|
||||
|
||||
/**
|
||||
* The first two items are the start and end points (byte offsets from the start of the file).
|
||||
* The third item is whether the source range belongs to the 'main' file, i.e., the file currently
|
||||
* being rendered/displayed in the editor (TODO we need to handle modules better in the frontend).
|
||||
*/
|
||||
export type SourceRange = [number, number, boolean]
|
||||
|
||||
/**
|
||||
* Convert a SourceRange as used inside the KCL interpreter into the above one for use in the
|
||||
* frontend (essentially we're eagerly checking whether the frontend should care about the SourceRange
|
||||
* so as not to expose details of the interpreter's current representation of module ids throughout
|
||||
* the frontend).
|
||||
*/
|
||||
export function sourceRangeFromRust(s: RustSourceRange): SourceRange {
|
||||
return [s[0], s[1], s[2] === 0]
|
||||
export function sourceRangeFromRust(s: SourceRange): SourceRange {
|
||||
return [s[0], s[1], s[2]]
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a default SourceRange for testing or as a placeholder.
|
||||
*/
|
||||
export function defaultSourceRange(): SourceRange {
|
||||
return [0, 0, true]
|
||||
return [0, 0, 0]
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a default RustSourceRange for testing or as a placeholder.
|
||||
* Create a SourceRange for the top-level module.
|
||||
*/
|
||||
export function defaultRustSourceRange(): RustSourceRange {
|
||||
return [0, 0, 0]
|
||||
export function topLevelRange(start: number, end: number): SourceRange {
|
||||
return [start, end, 0]
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this source range is from the file being executed. Returns
|
||||
* false if it's from a file that was imported.
|
||||
*/
|
||||
export function isTopLevelModule(range: SourceRange): boolean {
|
||||
return range[2] === 0
|
||||
}
|
||||
|
||||
export const wasmUrl = () => {
|
||||
@ -234,7 +248,8 @@ export const parse = (code: string | Error): ParseResult | Error => {
|
||||
parsed.msg,
|
||||
sourceRangeFromRust(parsed.sourceRanges[0]),
|
||||
[],
|
||||
[]
|
||||
[],
|
||||
defaultArtifactGraph()
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -258,8 +273,9 @@ export const isPathToNodeNumber = (
|
||||
export interface ExecState {
|
||||
memory: ProgramMemory
|
||||
operations: Operation[]
|
||||
artifacts: { [key in ArtifactId]?: Artifact }
|
||||
artifacts: { [key in ArtifactId]?: RustArtifact }
|
||||
artifactCommands: ArtifactCommand[]
|
||||
artifactGraph: ArtifactGraph
|
||||
}
|
||||
|
||||
/**
|
||||
@ -272,18 +288,53 @@ export function emptyExecState(): ExecState {
|
||||
operations: [],
|
||||
artifacts: {},
|
||||
artifactCommands: [],
|
||||
artifactGraph: defaultArtifactGraph(),
|
||||
}
|
||||
}
|
||||
|
||||
function execStateFromRust(execOutcome: RustExecOutcome): ExecState {
|
||||
function execStateFromRust(
|
||||
execOutcome: RustExecOutcome,
|
||||
program: Node<Program>
|
||||
): ExecState {
|
||||
const artifactGraph = rustArtifactGraphToMap(execOutcome.artifactGraph)
|
||||
// We haven't ported pathToNode logic to Rust yet, so we need to fill it in.
|
||||
for (const [id, artifact] of artifactGraph) {
|
||||
if (!artifact) continue
|
||||
if (!('codeRef' in artifact)) continue
|
||||
const pathToNode = getNodePathFromSourceRange(
|
||||
program,
|
||||
sourceRangeFromRust(artifact.codeRef.range)
|
||||
)
|
||||
artifact.codeRef.pathToNode = pathToNode
|
||||
}
|
||||
|
||||
return {
|
||||
memory: ProgramMemory.fromRaw(execOutcome.memory),
|
||||
operations: execOutcome.operations,
|
||||
artifacts: execOutcome.artifacts,
|
||||
artifactCommands: execOutcome.artifactCommands,
|
||||
artifactGraph,
|
||||
}
|
||||
}
|
||||
|
||||
export type ArtifactGraph = Map<ArtifactId, Artifact>
|
||||
|
||||
function rustArtifactGraphToMap(
|
||||
rustArtifactGraph: RustArtifactGraph
|
||||
): ArtifactGraph {
|
||||
const map = new Map<ArtifactId, Artifact>()
|
||||
for (const [id, artifact] of Object.entries(rustArtifactGraph.map)) {
|
||||
if (!artifact) continue
|
||||
map.set(id, artifact)
|
||||
}
|
||||
|
||||
return map
|
||||
}
|
||||
|
||||
export function defaultArtifactGraph(): ArtifactGraph {
|
||||
return new Map()
|
||||
}
|
||||
|
||||
interface Memory {
|
||||
[key: string]: KclValue | undefined
|
||||
}
|
||||
@ -488,7 +539,8 @@ export function sketchFromKclValueOptional(
|
||||
): Sketch | Reason {
|
||||
if (obj?.value?.type === 'Sketch') return obj.value
|
||||
if (obj?.value?.type === 'Solid') return obj.value.sketch
|
||||
if (obj?.type === 'Solid') return obj.sketch
|
||||
if (obj?.type === 'Sketch') return obj.value
|
||||
if (obj?.type === 'Solid') return obj.value.sketch
|
||||
if (!varName) {
|
||||
varName = 'a KCL value'
|
||||
}
|
||||
@ -543,7 +595,7 @@ export const executor = async (
|
||||
engineCommandManager,
|
||||
fileSystemManager
|
||||
)
|
||||
return execStateFromRust(execOutcome)
|
||||
return execStateFromRust(execOutcome, node)
|
||||
} catch (e: any) {
|
||||
console.log(e)
|
||||
const parsed: KclErrorWithOutputs = JSON.parse(e.toString())
|
||||
@ -552,7 +604,8 @@ export const executor = async (
|
||||
parsed.error.msg,
|
||||
sourceRangeFromRust(parsed.error.sourceRanges[0]),
|
||||
parsed.operations,
|
||||
parsed.artifactCommands
|
||||
parsed.artifactCommands,
|
||||
rustArtifactGraphToMap(parsed.artifactGraph)
|
||||
)
|
||||
|
||||
return Promise.reject(kclError)
|
||||
@ -613,7 +666,8 @@ export const modifyAstForSketch = async (
|
||||
parsed.msg,
|
||||
sourceRangeFromRust(parsed.sourceRanges[0]),
|
||||
[],
|
||||
[]
|
||||
[],
|
||||
defaultArtifactGraph()
|
||||
)
|
||||
|
||||
console.log(kclError)
|
||||
@ -683,7 +737,8 @@ export function programMemoryInit(): ProgramMemory | Error {
|
||||
parsed.msg,
|
||||
sourceRangeFromRust(parsed.sourceRanges[0]),
|
||||
[],
|
||||
[]
|
||||
[],
|
||||
defaultArtifactGraph()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
40
src/lib/base64.test.ts
Normal file
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
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)
|
||||
}
|
||||
@ -9,7 +9,11 @@ import { Selections } from 'lib/selections'
|
||||
import { kclManager } from 'lib/singletons'
|
||||
import { err } from 'lib/trap'
|
||||
import { modelingMachine, SketchTool } from 'machines/modelingMachine'
|
||||
import { loftValidator, revolveAxisValidator } from './validators'
|
||||
import {
|
||||
loftValidator,
|
||||
revolveAxisValidator,
|
||||
shellValidator,
|
||||
} from './validators'
|
||||
|
||||
type OutputFormat = Models['OutputFormat_type']
|
||||
type OutputTypeKey = OutputFormat['type']
|
||||
@ -276,7 +280,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
args: {
|
||||
selection: {
|
||||
inputType: 'selection',
|
||||
selectionTypes: ['solid2D', 'segment'],
|
||||
selectionTypes: ['solid2d', 'segment'],
|
||||
multiple: false, // TODO: multiple selection
|
||||
required: true,
|
||||
skip: true,
|
||||
@ -308,7 +312,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
args: {
|
||||
profile: {
|
||||
inputType: 'selection',
|
||||
selectionTypes: ['solid2D'],
|
||||
selectionTypes: ['solid2d'],
|
||||
required: true,
|
||||
skip: true,
|
||||
multiple: false,
|
||||
@ -333,7 +337,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
args: {
|
||||
selection: {
|
||||
inputType: 'selection',
|
||||
selectionTypes: ['solid2D'],
|
||||
selectionTypes: ['solid2d'],
|
||||
multiple: true,
|
||||
required: true,
|
||||
skip: false,
|
||||
@ -351,12 +355,13 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
selectionTypes: ['cap', 'wall'],
|
||||
multiple: true,
|
||||
required: true,
|
||||
skip: false,
|
||||
validation: shellValidator,
|
||||
},
|
||||
thickness: {
|
||||
inputType: 'kcl',
|
||||
defaultValue: KCL_DEFAULT_LENGTH,
|
||||
required: true,
|
||||
// TODO: add dry-run validation on thickness param
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -368,7 +373,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
args: {
|
||||
selection: {
|
||||
inputType: 'selection',
|
||||
selectionTypes: ['solid2D', 'segment'],
|
||||
selectionTypes: ['solid2d', 'segment'],
|
||||
multiple: false, // TODO: multiple selection
|
||||
required: true,
|
||||
skip: true,
|
||||
@ -573,7 +578,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
selection: {
|
||||
inputType: 'selection',
|
||||
selectionTypes: [
|
||||
'solid2D',
|
||||
'solid2d',
|
||||
'segment',
|
||||
'sweepEdge',
|
||||
'cap',
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
|
||||
import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarning'
|
||||
import { StateMachineCommandSetConfig } from 'lib/commandTypes'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { baseUnitLabels, baseUnitsUnion } from 'lib/settings/settingsTypes'
|
||||
import { projectsMachine } from 'machines/projectsMachine'
|
||||
|
||||
export type ProjectsCommandSchema = {
|
||||
@ -17,6 +20,13 @@ export type ProjectsCommandSchema = {
|
||||
oldName: string
|
||||
newName: string
|
||||
}
|
||||
'Import file from URL': {
|
||||
name: string
|
||||
code?: string
|
||||
units: UnitLength_type
|
||||
method: 'newProject' | 'existingProject'
|
||||
projectName?: string
|
||||
}
|
||||
}
|
||||
|
||||
export const projectsCommandBarConfig: StateMachineCommandSetConfig<
|
||||
@ -26,6 +36,7 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
|
||||
'Open project': {
|
||||
icon: 'arrowRight',
|
||||
description: 'Open a project',
|
||||
status: isDesktop() ? 'active' : 'inactive',
|
||||
args: {
|
||||
name: {
|
||||
inputType: 'options',
|
||||
@ -42,6 +53,7 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
|
||||
'Create project': {
|
||||
icon: 'folderPlus',
|
||||
description: 'Create a project',
|
||||
status: isDesktop() ? 'active' : 'inactive',
|
||||
args: {
|
||||
name: {
|
||||
inputType: 'string',
|
||||
@ -53,6 +65,7 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
|
||||
'Delete project': {
|
||||
icon: 'close',
|
||||
description: 'Delete a project',
|
||||
status: isDesktop() ? 'active' : 'inactive',
|
||||
needsReview: true,
|
||||
reviewMessage: ({ argumentsToSubmit }) =>
|
||||
CommandBarOverwriteWarning({
|
||||
@ -75,6 +88,7 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
|
||||
icon: 'folder',
|
||||
description: 'Rename a project',
|
||||
needsReview: true,
|
||||
status: isDesktop() ? 'active' : 'inactive',
|
||||
args: {
|
||||
oldName: {
|
||||
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.`
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -116,16 +116,16 @@ export const loftValidator = async ({
|
||||
}
|
||||
const { selection } = data
|
||||
|
||||
if (selection.graphSelections.some((s) => s.artifact?.type !== 'solid2D')) {
|
||||
return 'Unable to loft, some selection are not solid2Ds'
|
||||
if (selection.graphSelections.some((s) => s.artifact?.type !== 'solid2d')) {
|
||||
return 'Unable to loft, some selection are not solid2ds'
|
||||
}
|
||||
|
||||
const sectionIds = data.selection.graphSelections.flatMap((s) =>
|
||||
s.artifact?.type === 'solid2D' ? s.artifact.pathId : []
|
||||
s.artifact?.type === 'solid2d' ? s.artifact.pathId : []
|
||||
)
|
||||
|
||||
if (sectionIds.length < 2) {
|
||||
return 'Unable to loft, selection contains less than two solid2Ds'
|
||||
return 'Unable to loft, selection contains less than two solid2ds'
|
||||
}
|
||||
|
||||
const loftCommand = async () => {
|
||||
@ -153,3 +153,57 @@ export const loftValidator = async ({
|
||||
return 'Unable to loft with selected sketches'
|
||||
}
|
||||
}
|
||||
|
||||
export const shellValidator = async ({
|
||||
data,
|
||||
}: {
|
||||
data: { selection: Selections }
|
||||
}): Promise<boolean | string> => {
|
||||
if (!isSelections(data.selection)) {
|
||||
return 'Unable to shell, selections are missing'
|
||||
}
|
||||
|
||||
// No validation on the faces, filtering is done upstream and we have the dry run validation just below
|
||||
const face_ids = data.selection.graphSelections.flatMap((s) =>
|
||||
s.artifact ? s.artifact.id : []
|
||||
)
|
||||
|
||||
// We don't have the concept of solid3ds in TS yet.
|
||||
// So we're listing out the sweeps as if they were solids and taking the first one, just like in Rust for Shell:
|
||||
// https://github.com/KittyCAD/modeling-app/blob/e61fff115b9fa94aaace6307b1842cc15d41655e/src/wasm-lib/kcl/src/std/shell.rs#L237-L238
|
||||
// TODO: This is one cheap way to make sketch-on-face supported now but will likely fail multiple solids
|
||||
const object_id = engineCommandManager.artifactGraph
|
||||
.values()
|
||||
.find((v) => v.type === 'sweep')?.pathId
|
||||
|
||||
if (!object_id) {
|
||||
return "Unable to shell, couldn't find the solid"
|
||||
}
|
||||
|
||||
const shellCommand = async () => {
|
||||
// TODO: figure out something better than an arbitrarily small value
|
||||
const DEFAULT_THICKNESS: Models['LengthUnit_type'] = 1e-9
|
||||
const DEFAULT_HOLLOW = false
|
||||
const cmdArgs = {
|
||||
face_ids,
|
||||
object_id,
|
||||
hollow: DEFAULT_HOLLOW,
|
||||
shell_thickness: DEFAULT_THICKNESS,
|
||||
}
|
||||
return await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'solid3d_shell_face',
|
||||
...cmdArgs,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const attemptShell = await dryRunWrapper(shellCommand)
|
||||
if (attemptShell?.success) {
|
||||
return true
|
||||
}
|
||||
|
||||
return 'Unable to shell 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 DEFAULT_HOST = 'https://api.zoo.dev'
|
||||
export const PROD_APP_URL = 'https://app.zoo.dev'
|
||||
export const SETTINGS_FILE_NAME = 'settings.toml'
|
||||
export const TOKEN_FILE_NAME = 'token.txt'
|
||||
export const PROJECT_SETTINGS_FILE_NAME = 'project.toml'
|
||||
@ -110,6 +111,9 @@ export const KCL_SAMPLES_MANIFEST_URLS = {
|
||||
localFallback: '/kcl-samples-manifest-fallback.json',
|
||||
} as const
|
||||
|
||||
/** URL parameter to create a file */
|
||||
export const CREATE_FILE_URL_PARAM = 'create-file'
|
||||
|
||||
/** Toast id for the app auto-updater toast */
|
||||
export const AUTO_UPDATER_TOAST_ID = 'auto-updater-toast'
|
||||
|
||||
@ -139,3 +143,12 @@ export const VIEW_NAMES_SEMANTIC = {
|
||||
} as const
|
||||
/** The modeling sidebar buttons' IDs get a suffix to prevent collisions */
|
||||
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'
|
||||
|
||||
58
src/lib/desktopFS.test.ts
Normal file
58
src/lib/desktopFS.test.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { getUniqueProjectName } from './desktopFS'
|
||||
import { FileEntry } from './project'
|
||||
|
||||
/** Create a dummy project */
|
||||
function project(name: string, children?: FileEntry[]): FileEntry {
|
||||
return {
|
||||
name,
|
||||
children: children || [
|
||||
{ name: 'main.kcl', children: null, path: 'main.kcl' },
|
||||
],
|
||||
path: `/projects/${name}`,
|
||||
}
|
||||
}
|
||||
|
||||
describe(`Getting unique project names`, () => {
|
||||
it(`should return the same name if no conflicts`, () => {
|
||||
const projectName = 'new-project'
|
||||
const projects = [project('existing-project'), project('another-project')]
|
||||
const result = getUniqueProjectName(projectName, projects)
|
||||
expect(result).toBe(projectName)
|
||||
})
|
||||
it(`should return a unique name if there is a conflict`, () => {
|
||||
const projectName = 'existing-project'
|
||||
const projects = [project('existing-project'), project('another-project')]
|
||||
const result = getUniqueProjectName(projectName, projects)
|
||||
expect(result).toBe('existing-project-1')
|
||||
})
|
||||
it(`should increment an ending index until a unique one is found`, () => {
|
||||
const projectName = 'existing-project-1'
|
||||
const projects = [
|
||||
project('existing-project'),
|
||||
project('existing-project-1'),
|
||||
project('existing-project-2'),
|
||||
]
|
||||
const result = getUniqueProjectName(projectName, projects)
|
||||
expect(result).toBe('existing-project-3')
|
||||
})
|
||||
it(`should prefer the formatting of the index identifier if present`, () => {
|
||||
const projectName = 'existing-project-$nn'
|
||||
const projects = [
|
||||
project('existing-project'),
|
||||
project('existing-project-1'),
|
||||
project('existing-project-2'),
|
||||
]
|
||||
const result = getUniqueProjectName(projectName, projects)
|
||||
expect(result).toBe('existing-project-03')
|
||||
})
|
||||
it(`be able to get an incrementing index regardless of padding zeroes`, () => {
|
||||
const projectName = 'existing-project-$nn'
|
||||
const projects = [
|
||||
project('existing-project'),
|
||||
project('existing-project-01'),
|
||||
project('existing-project-2'),
|
||||
]
|
||||
const result = getUniqueProjectName(projectName, projects)
|
||||
expect(result).toBe('existing-project-03')
|
||||
})
|
||||
})
|
||||
@ -54,8 +54,10 @@ export function getNextProjectIndex(
|
||||
const matches = projects.map((project) => project.name?.match(regex))
|
||||
const indices = matches
|
||||
.filter(Boolean)
|
||||
.map((match) => match![1])
|
||||
.map(Number)
|
||||
.map((match) => (match !== null ? match[1] : '-1'))
|
||||
.map((maybeMatchIndex) => {
|
||||
return parseInt(maybeMatchIndex || '0', 10)
|
||||
})
|
||||
const maxIndex = Math.max(...indices, -1)
|
||||
return maxIndex + 1
|
||||
}
|
||||
@ -83,6 +85,33 @@ export function doesProjectNameNeedInterpolated(projectName: string) {
|
||||
return projectName.includes(INDEX_IDENTIFIER)
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a target name, which may include our magic index interpolation string,
|
||||
* and a list of projects, return a unique name that doesn't conflict with any
|
||||
* of the existing projects, incrementing any ending number if necessary.
|
||||
* @param name
|
||||
* @param projects
|
||||
* @returns
|
||||
*/
|
||||
export function getUniqueProjectName(name: string, projects: FileEntry[]) {
|
||||
// The name may have our magic index interpolation string in it
|
||||
const needsInterpolation = doesProjectNameNeedInterpolated(name)
|
||||
|
||||
if (needsInterpolation) {
|
||||
const nextIndex = getNextProjectIndex(name, projects)
|
||||
return interpolateProjectNameWithIndex(name, nextIndex)
|
||||
} else {
|
||||
let newName = name
|
||||
while (projects.some((project) => project.name === newName)) {
|
||||
const nameEndsWithNumber = newName.match(/\d+$/)
|
||||
newName = nameEndsWithNumber
|
||||
? newName.replace(/\d+$/, (num) => `${parseInt(num, 10) + 1}`)
|
||||
: `${name}-1`
|
||||
}
|
||||
return newName
|
||||
}
|
||||
}
|
||||
|
||||
function escapeRegExpChars(string: string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
}
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarning'
|
||||
import { Command, CommandArgumentOption } from './commandTypes'
|
||||
import { kclManager } from './singletons'
|
||||
import { codeManager, kclManager } from './singletons'
|
||||
import { isDesktop } from './isDesktop'
|
||||
import { FILE_EXT, PROJECT_SETTINGS_FILE_NAME } from './constants'
|
||||
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
|
||||
import { parseProjectSettings } from 'lang/wasm'
|
||||
import { err, reportRejection } from './trap'
|
||||
import { projectConfigurationToSettingsPayload } from './settings/settingsUtils'
|
||||
import { copyFileShareLink } from './links'
|
||||
import { IndexLoaderData } from './types'
|
||||
|
||||
interface OnSubmitProps {
|
||||
sampleName: string
|
||||
@ -15,10 +17,21 @@ interface OnSubmitProps {
|
||||
method: 'overwrite' | 'newFile'
|
||||
}
|
||||
|
||||
export function kclCommands(
|
||||
onSubmit: (p: OnSubmitProps) => Promise<void>,
|
||||
providedOptions: CommandArgumentOption<string>[]
|
||||
): Command[] {
|
||||
interface KclCommandConfig {
|
||||
// TODO: find a different approach that doesn't require
|
||||
// special props for a single 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 [
|
||||
{
|
||||
name: 'format-code',
|
||||
@ -107,7 +120,9 @@ export function kclCommands(
|
||||
)
|
||||
.then((props) => {
|
||||
if (props?.code) {
|
||||
onSubmit(props).catch(reportError)
|
||||
commandProps.specialPropsForSampleCommand
|
||||
.onSubmit(props)
|
||||
.catch(reportError)
|
||||
}
|
||||
})
|
||||
.catch(reportError)
|
||||
@ -149,9 +164,25 @@ export function kclCommands(
|
||||
}
|
||||
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
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
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()
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { defaultRustSourceRange } from 'lang/wasm'
|
||||
import { defaultSourceRange } from 'lang/wasm'
|
||||
import { filterOperations } from './operations'
|
||||
import { Operation } from 'wasm-lib/kcl/bindings/Operation'
|
||||
|
||||
@ -8,7 +8,7 @@ function stdlib(name: string): Operation {
|
||||
name,
|
||||
unlabeledArg: null,
|
||||
labeledArgs: {},
|
||||
sourceRange: defaultRustSourceRange(),
|
||||
sourceRange: defaultSourceRange(),
|
||||
isError: false,
|
||||
}
|
||||
}
|
||||
@ -17,10 +17,10 @@ function userCall(name: string): Operation {
|
||||
return {
|
||||
type: 'UserDefinedFunctionCall',
|
||||
name,
|
||||
functionSourceRange: defaultRustSourceRange(),
|
||||
functionSourceRange: defaultSourceRange(),
|
||||
unlabeledArg: null,
|
||||
labeledArgs: {},
|
||||
sourceRange: defaultRustSourceRange(),
|
||||
sourceRange: defaultSourceRange(),
|
||||
}
|
||||
}
|
||||
function userReturn(): Operation {
|
||||
|
||||
@ -3,8 +3,8 @@ import { VITE_KC_API_BASE_URL } from 'env'
|
||||
import crossPlatformFetch from './crossPlatformFetch'
|
||||
import { err, reportRejection } from './trap'
|
||||
import { Selections } from './selections'
|
||||
import { ArtifactGraph, getArtifactOfTypes } from 'lang/std/artifactGraph'
|
||||
import { SourceRange } from 'lang/wasm'
|
||||
import { getArtifactOfTypes } from 'lang/std/artifactGraph'
|
||||
import { ArtifactGraph, SourceRange, topLevelRange } from 'lang/wasm'
|
||||
import toast from 'react-hot-toast'
|
||||
import { codeManager, editorManager, kclManager } from './singletons'
|
||||
import { ToastPromptToEditCadSuccess } from 'components/ToastTextToCad'
|
||||
@ -334,7 +334,7 @@ const reBuildNewCodeWithRanges = (
|
||||
} else if (change.added && !change.removed) {
|
||||
const start = newCodeWithRanges.length
|
||||
const end = start + change.value.length
|
||||
insertRanges.push([start, end, true])
|
||||
insertRanges.push(topLevelRange(start, end))
|
||||
newCodeWithRanges += change.value
|
||||
}
|
||||
}
|
||||
|
||||
@ -114,7 +114,7 @@ export const fileLoader: LoaderFunction = async (
|
||||
return redirect(
|
||||
`${PATHS.FILE}/${encodeURIComponent(
|
||||
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
|
||||
// and returns them to the Home route, along with any errors that occurred
|
||||
export const homeLoader: LoaderFunction = async (): Promise<
|
||||
HomeLoaderData | Response
|
||||
> => {
|
||||
export const homeLoader: LoaderFunction = async ({
|
||||
request,
|
||||
}): Promise<HomeLoaderData | Response> => {
|
||||
const url = new URL(request.url)
|
||||
if (!isDesktop()) {
|
||||
return redirect(PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME)
|
||||
return redirect(
|
||||
PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME + (url.search || '')
|
||||
)
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
SourceRange,
|
||||
Expr,
|
||||
defaultSourceRange,
|
||||
topLevelRange,
|
||||
} from 'lang/wasm'
|
||||
import { ModelingMachineEvent } from 'machines/modelingMachine'
|
||||
import { isNonNullable, uuidv4 } from 'lib/utils'
|
||||
@ -63,7 +64,7 @@ type Selection__old =
|
||||
| 'line-end'
|
||||
| 'line-mid'
|
||||
| 'extrude-wall'
|
||||
| 'solid2D'
|
||||
| 'solid2d'
|
||||
| 'start-cap'
|
||||
| 'end-cap'
|
||||
| 'point'
|
||||
@ -103,13 +104,13 @@ function convertSelectionToOld(selection: Selection): Selection__old | null {
|
||||
// return {} as Selection__old
|
||||
// TODO implementation
|
||||
const _artifact = selection.artifact
|
||||
if (_artifact?.type === 'solid2D') {
|
||||
if (_artifact?.type === 'solid2d') {
|
||||
const codeRef = getSolid2dCodeRef(
|
||||
_artifact,
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
if (err(codeRef)) return null
|
||||
return { range: codeRef.range, type: 'solid2D' }
|
||||
return { range: codeRef.range, type: 'solid2d' }
|
||||
}
|
||||
if (_artifact?.type === 'cap') {
|
||||
const codeRef = getCapCodeRef(_artifact, engineCommandManager.artifactGraph)
|
||||
@ -269,7 +270,7 @@ export function getEventForSegmentSelection(
|
||||
selectionType: 'singleCodeCursor',
|
||||
selection: {
|
||||
codeRef: {
|
||||
range: [node.node.start, node.node.end, true],
|
||||
range: topLevelRange(node.node.start, node.node.end),
|
||||
pathToNode: group.userData.pathToNode,
|
||||
},
|
||||
},
|
||||
@ -381,10 +382,13 @@ export function processCodeMirrorRanges({
|
||||
if (!isChange) return null
|
||||
const codeBasedSelections: Selections['graphSelections'] =
|
||||
codeMirrorRanges.map(({ from, to }) => {
|
||||
const pathToNode = getNodePathFromSourceRange(ast, [from, to, true])
|
||||
const pathToNode = getNodePathFromSourceRange(
|
||||
ast,
|
||||
topLevelRange(from, to)
|
||||
)
|
||||
return {
|
||||
codeRef: {
|
||||
range: [from, to, true],
|
||||
range: topLevelRange(from, to),
|
||||
pathToNode,
|
||||
},
|
||||
}
|
||||
@ -447,7 +451,10 @@ function updateSceneObjectColors(codeBasedSelections: Selection[]) {
|
||||
if (err(nodeMeta)) return
|
||||
const node = nodeMeta.node
|
||||
const groupHasCursor = codeBasedSelections.some((selection) => {
|
||||
return isOverlap(selection?.codeRef?.range, [node.start, node.end, true])
|
||||
return isOverlap(
|
||||
selection?.codeRef?.range,
|
||||
topLevelRange(node.start, node.end)
|
||||
)
|
||||
})
|
||||
|
||||
const color = groupHasCursor
|
||||
@ -575,7 +582,7 @@ export function getSelectionTypeDisplayText(
|
||||
([type, count]) =>
|
||||
`${count} ${type
|
||||
.replace('wall', 'face')
|
||||
.replace('solid2D', 'face')
|
||||
.replace('solid2d', 'face')
|
||||
.replace('segment', 'face')}${count > 1 ? 's' : ''}`
|
||||
)
|
||||
.toArray()
|
||||
@ -650,7 +657,7 @@ export function codeToIdSelections(
|
||||
const artifact = engineCommandManager.artifactGraph.get(
|
||||
entry.artifact.solid2dId || ''
|
||||
)
|
||||
if (artifact?.type !== 'solid2D') {
|
||||
if (artifact?.type !== 'solid2d') {
|
||||
bestCandidate = {
|
||||
artifact: entry.artifact,
|
||||
selection,
|
||||
@ -873,7 +880,7 @@ export function updateSelections(
|
||||
return {
|
||||
artifact: artifact,
|
||||
codeRef: {
|
||||
range: [node.start, node.end, true],
|
||||
range: topLevelRange(node.start, node.end),
|
||||
pathToNode: pathToNode,
|
||||
},
|
||||
}
|
||||
@ -887,7 +894,7 @@ export function updateSelections(
|
||||
if (err(node)) return node
|
||||
pathToNodeBasedSelections.push({
|
||||
codeRef: {
|
||||
range: [node.node.start, node.node.end, true],
|
||||
range: topLevelRange(node.node.start, node.node.end),
|
||||
pathToNode: pathToNode,
|
||||
},
|
||||
})
|
||||
|
||||
@ -195,7 +195,7 @@ export async function submitAndAwaitTextToKcl({
|
||||
.toLowerCase()}${FILE_EXT}`
|
||||
|
||||
if (isDesktop()) {
|
||||
// We have to pre-emptively run our unique file name logic,
|
||||
// We have to preemptively run our unique file name logic,
|
||||
// so that we can pass the unique file name to the toast,
|
||||
// and by extension the file-deletion-on-reject logic.
|
||||
newFileName = getNextFileName({
|
||||
|
||||
@ -280,7 +280,12 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
status: 'available',
|
||||
title: 'Offset plane',
|
||||
description: 'Create a plane parallel to an existing plane.',
|
||||
links: [],
|
||||
links: [
|
||||
{
|
||||
label: 'KCL docs',
|
||||
url: 'https://zoo.dev/docs/kcl/offsetPlane',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'plane-points',
|
||||
@ -305,7 +310,12 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
status: 'available',
|
||||
title: 'Text-to-CAD',
|
||||
description: 'Generate geometry from a text prompt.',
|
||||
links: [],
|
||||
links: [
|
||||
{
|
||||
label: 'API docs',
|
||||
url: 'https://zoo.dev/docs/api/ml/generate-a-cad-model-from-text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'prompt-to-edit',
|
||||
|
||||
@ -6,16 +6,16 @@ import {
|
||||
hasLeadingZero,
|
||||
hasDigitsLeftOfDecimal,
|
||||
} from './utils'
|
||||
import { SourceRange } from '../lang/wasm'
|
||||
import { SourceRange, topLevelRange } from '../lang/wasm'
|
||||
|
||||
describe('testing isOverlapping', () => {
|
||||
testBothOrders([0, 3, true], [3, 10, true])
|
||||
testBothOrders([0, 5, true], [3, 4, true])
|
||||
testBothOrders([0, 5, true], [5, 10, true])
|
||||
testBothOrders([0, 5, true], [6, 10, true], false)
|
||||
testBothOrders([0, 5, true], [-1, 1, true])
|
||||
testBothOrders([0, 5, true], [-1, 0, true])
|
||||
testBothOrders([0, 5, true], [-2, -1, true], false)
|
||||
testBothOrders(topLevelRange(0, 3), topLevelRange(3, 10))
|
||||
testBothOrders(topLevelRange(0, 5), topLevelRange(3, 4))
|
||||
testBothOrders(topLevelRange(0, 5), topLevelRange(5, 10))
|
||||
testBothOrders(topLevelRange(0, 5), topLevelRange(6, 10), false)
|
||||
testBothOrders(topLevelRange(0, 5), topLevelRange(-1, 1))
|
||||
testBothOrders(topLevelRange(0, 5), topLevelRange(-1, 0))
|
||||
testBothOrders(topLevelRange(0, 5), topLevelRange(-2, -1), false)
|
||||
})
|
||||
|
||||
function testBothOrders(a: SourceRange, b: SourceRange, result = true) {
|
||||
|
||||
@ -25,6 +25,10 @@ export const projectsMachine = setup({
|
||||
type: 'Delete project'
|
||||
data: ProjectsCommandSchema['Delete project']
|
||||
}
|
||||
| {
|
||||
type: 'Import file from URL'
|
||||
data: ProjectsCommandSchema['Import file from URL']
|
||||
}
|
||||
| { type: 'navigate'; data: { name: string } }
|
||||
| {
|
||||
type: 'xstate.done.actor.read-projects'
|
||||
@ -42,6 +46,10 @@ export const projectsMachine = setup({
|
||||
type: 'xstate.done.actor.rename-project'
|
||||
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 } },
|
||||
input: {} as {
|
||||
projects: Project[]
|
||||
@ -60,6 +68,7 @@ export const projectsMachine = setup({
|
||||
toastError: () => {},
|
||||
navigateToProject: () => {},
|
||||
navigateToProjectIfNeeded: () => {},
|
||||
navigateToFile: () => {},
|
||||
},
|
||||
actors: {
|
||||
readProjects: fromPromise(() => Promise.resolve([] as Project[])),
|
||||
@ -90,12 +99,22 @@ export const projectsMachine = setup({
|
||||
name: '',
|
||||
})
|
||||
),
|
||||
createFile: fromPromise(
|
||||
(_: {
|
||||
input: ProjectsCommandSchema['Import file from URL'] & {
|
||||
projects: Project[]
|
||||
}
|
||||
}) => Promise.resolve({ message: '', projectName: '', fileName: '' })
|
||||
),
|
||||
},
|
||||
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({
|
||||
/** @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',
|
||||
|
||||
initial: 'Reading projects',
|
||||
@ -111,6 +130,8 @@ export const projectsMachine = setup({
|
||||
})),
|
||||
target: '.Reading projects',
|
||||
},
|
||||
|
||||
'Import file from URL': '.Creating file',
|
||||
},
|
||||
states: {
|
||||
'Has no projects': {
|
||||
@ -155,7 +176,10 @@ export const projectsMachine = setup({
|
||||
id: 'create-project',
|
||||
src: 'createProject',
|
||||
input: ({ event, context }) => {
|
||||
if (event.type !== 'Create project') {
|
||||
if (
|
||||
event.type !== 'Create project' &&
|
||||
event.type !== 'Import file from URL'
|
||||
) {
|
||||
return {
|
||||
name: '',
|
||||
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',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
79
src/main.ts
79
src/main.ts
@ -21,6 +21,7 @@ import minimist from 'minimist'
|
||||
import getCurrentProjectFile from 'lib/getCurrentProjectFile'
|
||||
import os from 'node:os'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { ZOO_STUDIO_PROTOCOL } from 'lib/constants'
|
||||
import argvFromYargs from './commandLineArgs'
|
||||
|
||||
import * as packageJSON from '../package.json'
|
||||
@ -42,15 +43,13 @@ if (!process.env.NODE_ENV)
|
||||
dotenv.config({ path: [`.env.${NODE_ENV}.local`, `.env.${NODE_ENV}`] })
|
||||
|
||||
process.env.VITE_KC_API_WS_MODELING_URL ??=
|
||||
'wss://api.zoo.dev/ws/modeling/commands'
|
||||
process.env.VITE_KC_API_BASE_URL ??= 'https://api.zoo.dev'
|
||||
process.env.VITE_KC_SITE_BASE_URL ??= 'https://zoo.dev'
|
||||
'wss://api.dev.zoo.dev/ws/modeling/commands'
|
||||
process.env.VITE_KC_API_BASE_URL ??= 'https://api.dev.zoo.dev'
|
||||
process.env.VITE_KC_SITE_BASE_URL ??= 'https://dev.zoo.dev'
|
||||
process.env.VITE_KC_SKIP_AUTH ??= 'false'
|
||||
process.env.VITE_KC_CONNECTION_TIMEOUT_MS ??= '15000'
|
||||
|
||||
const ZOO_STUDIO_PROTOCOL = 'zoo-studio'
|
||||
|
||||
/// Register our application to handle all "electron-fiddle://" protocols.
|
||||
/// Register our application to handle all "zoo-studio:" protocols.
|
||||
if (process.defaultApp) {
|
||||
if (process.argv.length >= 2) {
|
||||
app.setAsDefaultProtocolClient(ZOO_STUDIO_PROTOCOL, process.execPath, [
|
||||
@ -65,7 +64,7 @@ if (process.defaultApp) {
|
||||
// Must be done before ready event.
|
||||
registerStartupListeners()
|
||||
|
||||
const createWindow = (filePath?: string, reuse?: boolean): BrowserWindow => {
|
||||
const createWindow = (pathToOpen?: string, reuse?: boolean): BrowserWindow => {
|
||||
let newWindow
|
||||
|
||||
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.
|
||||
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 {
|
||||
getProjectPathAtStartup(filePath)
|
||||
.then(async (projectPath) => {
|
||||
const startIndex = path.join(
|
||||
__dirname,
|
||||
`../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`
|
||||
)
|
||||
|
||||
if (projectPath === null) {
|
||||
await newWindow.loadFile(startIndex)
|
||||
return
|
||||
}
|
||||
|
||||
console.log('Loading file', projectPath)
|
||||
|
||||
const fullUrl = `/file/${encodeURIComponent(projectPath)}`
|
||||
console.log('Full URL', fullUrl)
|
||||
|
||||
await newWindow.loadFile(startIndex, {
|
||||
hash: fullUrl,
|
||||
if (pathIsCustomProtocolLink && pathToOpen) {
|
||||
// We're trying to open a custom protocol link
|
||||
const filteredPath = pathToOpen
|
||||
? decodeURI(pathToOpen.replace(ZOO_STUDIO_PROTOCOL, ''))
|
||||
: ''
|
||||
const startIndex = path.join(
|
||||
__dirname,
|
||||
`../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`
|
||||
)
|
||||
newWindow
|
||||
.loadFile(startIndex, {
|
||||
hash: filteredPath,
|
||||
})
|
||||
})
|
||||
.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.
|
||||
|
||||
@ -25,6 +25,7 @@ import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
||||
import { useProjectsLoader } from 'hooks/useProjectsLoader'
|
||||
import { useProjectsContext } from 'hooks/useProjectsContext'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { useCreateFileLinkQuery } from 'hooks/useCreateFileLinkQueryWatcher'
|
||||
|
||||
// This route only opens in the desktop context for now,
|
||||
// as defined in Router.tsx, so we can use the desktop APIs and types.
|
||||
@ -34,6 +35,18 @@ const Home = () => {
|
||||
const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0)
|
||||
const { projectsDir } = useProjectsLoader([projectsLoaderTrigger])
|
||||
|
||||
// Keep a lookout for a URL query string that invokes the 'import file from URL' command
|
||||
useCreateFileLinkQuery((argDefaultValues) => {
|
||||
commandBarSend({
|
||||
type: 'Find and select command',
|
||||
data: {
|
||||
groupId: 'projects',
|
||||
name: 'Import file from URL',
|
||||
argDefaultValues,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
useRefreshSettings(PATHS.HOME + 'SETTINGS')
|
||||
const navigate = useNavigate()
|
||||
const {
|
||||
@ -148,7 +161,7 @@ const Home = () => {
|
||||
}}
|
||||
data-testid="home-new-file"
|
||||
>
|
||||
New project
|
||||
Create project
|
||||
</ActionButton>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
|
||||
7
src/wasm-lib/Cargo.lock
generated
7
src/wasm-lib/Cargo.lock
generated
@ -1382,12 +1382,6 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iai"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71a816c97c42258aa5834d07590b718b4c9a598944cd39a52dc25b351185d678"
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.61"
|
||||
@ -1739,7 +1733,6 @@ dependencies = [
|
||||
"gltf-json",
|
||||
"handlebars",
|
||||
"http 1.2.0",
|
||||
"iai",
|
||||
"image",
|
||||
"indexmap 2.7.0",
|
||||
"insta",
|
||||
|
||||
@ -17,6 +17,7 @@ use kittycad_modeling_cmds::{
|
||||
websocket::{ModelingBatch, ModelingCmdReq, OkWebSocketResponseData, WebSocketRequest, WebSocketResponse},
|
||||
};
|
||||
use tokio::sync::RwLock;
|
||||
use uuid::Uuid;
|
||||
|
||||
const CPP_PREFIX: &str = "const double scaleFactor = 100;\n";
|
||||
const NEED_PLANES: bool = true;
|
||||
@ -369,6 +370,10 @@ impl kcl_lib::EngineManager for EngineConnection {
|
||||
self.batch_end.clone()
|
||||
}
|
||||
|
||||
fn responses(&self) -> IndexMap<Uuid, WebSocketResponse> {
|
||||
IndexMap::new()
|
||||
}
|
||||
|
||||
fn take_artifact_commands(&self) -> Vec<ArtifactCommand> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
@ -113,7 +113,6 @@ base64 = "0.22.1"
|
||||
criterion = { version = "0.5.1", features = ["async_tokio"] }
|
||||
expectorate = "1.1.0"
|
||||
handlebars = "6.3.0"
|
||||
iai = "0.1"
|
||||
image = { version = "0.25.5", default-features = false, features = ["png"] }
|
||||
insta = { version = "1.41.1", features = ["json", "filters", "redactions"] }
|
||||
itertools = "0.13.0"
|
||||
@ -129,10 +128,6 @@ workspace = true
|
||||
name = "compiler_benchmark_criterion"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "compiler_benchmark_iai"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "digest_benchmark"
|
||||
harness = false
|
||||
@ -142,15 +137,7 @@ name = "lsp_semantic_tokens_benchmark_criterion"
|
||||
harness = false
|
||||
required-features = ["lsp-test-util"]
|
||||
|
||||
[[bench]]
|
||||
name = "lsp_semantic_tokens_benchmark_iai"
|
||||
harness = false
|
||||
required-features = ["lsp-test-util"]
|
||||
|
||||
[[bench]]
|
||||
name = "executor_benchmark_criterion"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "executor_benchmark_iai"
|
||||
harness = false
|
||||
|
||||
@ -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");
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user