Compare commits
20 Commits
achalmers/
...
jtran/fix-
| Author | SHA1 | Date | |
|---|---|---|---|
| cbfe3db203 | |||
| 615b7feabb | |||
| 5743b9ced0 | |||
| 8896d06028 | |||
| 1f217ef50b | |||
| 5ef5c6280c | |||
| aac95e1e2e | |||
| 18f4a1303c | |||
| ded97eda61 | |||
| f6b06520ee | |||
| dcfcdc98ce | |||
| 9ab3325580 | |||
| cb5ad3ab27 | |||
| 1e539cc134 | |||
| 4297dc43ae | |||
| 935b4ee7f5 | |||
| 2523242bb1 | |||
| 309e4fadf0 | |||
| 842054de09 | |||
| af146284b6 |
31
.github/workflows/build-apps.yml
vendored
@ -7,14 +7,11 @@ on:
|
||||
- main
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
schedule:
|
||||
- cron: '0 4 * * *'
|
||||
# Daily at 04:00 AM UTC
|
||||
# Will checkout the last commit from the default branch (main as of 2023-10-04)
|
||||
- 'nightly-v[0-9]+.[0-9]+.[0-9]+'
|
||||
|
||||
env:
|
||||
IS_RELEASE: ${{ github.ref_type == 'tag' }}
|
||||
IS_NIGHTLY: ${{ github.event_name == 'schedule' }}
|
||||
IS_RELEASE: ${{ github.ref_type == 'tag' && startsWith(github.ref_name, 'v') }}
|
||||
IS_NIGHTLY: ${{ github.ref_type == 'tag' && startsWith(github.ref_name, 'nightly-v') }}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
@ -53,7 +50,10 @@ jobs:
|
||||
|
||||
- name: Set nightly version, product name, release notes, and icons
|
||||
if: ${{ env.IS_NIGHTLY == 'true' }}
|
||||
run: yarn files:flip-to-nightly
|
||||
run: |
|
||||
export VERSION=${GITHUB_REF_NAME#nightly-v}
|
||||
yarn files:set-version
|
||||
yarn files:flip-to-nightly
|
||||
|
||||
- name: Set release version
|
||||
if: ${{ env.IS_RELEASE == 'true' }}
|
||||
@ -273,7 +273,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
contents: write
|
||||
if: ${{ github.ref_type == 'tag' || github.event_name == 'schedule' }}
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
env:
|
||||
VERSION_NO_V: ${{ needs.prepare-files.outputs.version }}
|
||||
VERSION: ${{ format('v{0}', needs.prepare-files.outputs.version) }}
|
||||
@ -330,8 +330,8 @@ jobs:
|
||||
env:
|
||||
NOTES: ${{ needs.prepare-files.outputs.notes }}
|
||||
PUB_DATE: ${{ github.event.repository.updated_at }}
|
||||
WEBSITE_DIR: ${{ github.event_name == 'schedule' && 'dl.zoo.dev/releases/modeling-app/nightly' || 'dl.zoo.dev/releases/modeling-app' }}
|
||||
URL_CODED_NAME: ${{ github.event_name == 'schedule' && 'Zoo%20Modeling%20App%20%28Nightly%29' || 'Zoo%20Modeling%20App' }}
|
||||
WEBSITE_DIR: ${{ env.IS_NIGHTLY == 'true' && 'dl.zoo.dev/releases/modeling-app/nightly' || 'dl.zoo.dev/releases/modeling-app' }}
|
||||
URL_CODED_NAME: ${{ env.IS_NIGHTLY == 'true' && 'Zoo%20Modeling%20App%20%28Nightly%29' || 'Zoo%20Modeling%20App' }}
|
||||
run: |
|
||||
RELEASE_DIR=https://${WEBSITE_DIR}
|
||||
jq --null-input \
|
||||
@ -414,14 +414,3 @@ jobs:
|
||||
- name: Invalidate bucket cache on latest*.yml and last_download.json files
|
||||
if: ${{ env.IS_NIGHTLY == 'true' }}
|
||||
run: yarn files:invalidate-bucket:nightly
|
||||
|
||||
- name: Tag nightly commit
|
||||
if: ${{ env.IS_NIGHTLY == 'true' }}
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { VERSION } = process.env
|
||||
const { owner, repo } = context.repo
|
||||
const { sha } = context
|
||||
const ref = `refs/tags/nightly-${VERSION}`
|
||||
github.rest.git.createRef({ owner, repo, sha, ref })
|
||||
|
||||
4
.github/workflows/e2e-tests.yml
vendored
@ -142,7 +142,7 @@ jobs:
|
||||
# TODO: break this in its own job, for now it's not slowing down the overall execution as ubuntu is the quickest,
|
||||
# but we could do better. This forces a large 1/1 shard of all 20 snapshot tests that runs in about 3 minutes.
|
||||
run: |
|
||||
PLATFORM=web yarn playwright test --config=playwright.config.ts --retries="3" --update-snapshots --grep=@snapshot --shard=1/1
|
||||
PLATFORM=web yarn playwright test --config=playwright.config.ts --retries="3" --update-snapshots --grep=@snapshot --trace=on --shard=1/1
|
||||
env:
|
||||
CI: true
|
||||
NODE_ENV: development
|
||||
@ -153,7 +153,7 @@ jobs:
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() && (success() || failure()) }}
|
||||
with:
|
||||
name: playwright-report-${{ matrix.os }}-snapshot-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
name: playwright-report-snapshots-${{ matrix.os }}-snapshot-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
path: playwright-report/
|
||||
include-hidden-files: true
|
||||
retention-days: 30
|
||||
|
||||
39
.github/workflows/tag-nightly.yml
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
name: tag-nightly
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 4 * * *'
|
||||
# Daily at 04:00 AM UTC
|
||||
# Will checkout the last commit from the default branch (main as of 2023-10-04)
|
||||
|
||||
jobs:
|
||||
tag-nightly:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/create-github-app-token@v1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.MODELING_APP_GH_APP_ID }}
|
||||
private-key: ${{ secrets.MODELING_APP_GH_APP_PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- run: yarn install
|
||||
|
||||
- name: Push tag
|
||||
run: |
|
||||
VERSION_NO_V=$(date +'%-y.%-m.%-d')
|
||||
TAG="nightly-v$VERSION_NO_V"
|
||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git tag $TAG
|
||||
git push origin tag $TAG
|
||||
@ -9,7 +9,7 @@ Set the appearance of a solid. This only works on solids, not sketches or indivi
|
||||
This will work on any solid, including extruded solids, revolved solids, and shelled solids.
|
||||
|
||||
```js
|
||||
appearance(solid_set: SolidSet, color: String, metalness?: number, roughness?: number) -> SolidSet
|
||||
appearance(solidSet: SolidSet, color: String, metalness?: number, roughness?: number) -> SolidSet
|
||||
```
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ appearance(solid_set: SolidSet, color: String, metalness?: number, roughness?: n
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `solid_set` | [`SolidSet`](/docs/kcl/types/SolidSet) | The solid(s) whose appearance is being set | Yes |
|
||||
| `solidSet` | [`SolidSet`](/docs/kcl/types/SolidSet) | The solid(s) whose appearance is being set | Yes |
|
||||
| `color` | `String` | Color of the new material, a hex string like '#ff0000' | Yes |
|
||||
| `metalness` | `number` | Metalness of the new material, a percentage like 95.7. | No |
|
||||
| `roughness` | `number` | Roughness of the new material, a percentage like 95.7. | No |
|
||||
|
||||
@ -9,7 +9,7 @@ Construct a 2-dimensional circle, of the specified radius, centered at
|
||||
the provided (x, y) origin point.
|
||||
|
||||
```js
|
||||
circle(data: CircleData, sketch_surface_or_group: SketchOrSurface, tag?: TagDeclarator) -> Sketch
|
||||
circle(data: CircleData, sketchSurfaceOrGroup: SketchOrSurface, tag?: TagDeclarator) -> Sketch
|
||||
```
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@ circle(data: CircleData, sketch_surface_or_group: SketchOrSurface, tag?: TagDecl
|
||||
| Name | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `data` | [`CircleData`](/docs/kcl/types/CircleData) | Data for drawing an circle | Yes |
|
||||
| `sketch_surface_or_group` | [`SketchOrSurface`](/docs/kcl/types/SketchOrSurface) | A sketch surface or a sketch. | Yes |
|
||||
| `sketchSurfaceOrGroup` | [`SketchOrSurface`](/docs/kcl/types/SketchOrSurface) | A sketch surface or a sketch. | Yes |
|
||||
| `tag` | [`TagDeclarator`](/docs/kcl/types#tag-declaration) | | No |
|
||||
|
||||
### Returns
|
||||
|
||||
@ -9,7 +9,7 @@ Construct a circle derived from 3 points.
|
||||
|
||||
|
||||
```js
|
||||
circleThreePoint(p1: [number], p2: [number], p3: [number], sketch_surface_or_group: SketchOrSurface, tag?: TagDeclarator) -> Sketch
|
||||
circleThreePoint(p1: [number], p2: [number], p3: [number], sketchSurfaceOrGroup: SketchOrSurface, tag?: TagDeclarator) -> Sketch
|
||||
```
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@ circleThreePoint(p1: [number], p2: [number], p3: [number], sketch_surface_or_gro
|
||||
| `p1` | `[number]` | 1st point to derive the circle. | Yes |
|
||||
| `p2` | `[number]` | 2nd point to derive the circle. | Yes |
|
||||
| `p3` | `[number]` | 3rd point to derive the circle. | Yes |
|
||||
| `sketch_surface_or_group` | [`SketchOrSurface`](/docs/kcl/types/SketchOrSurface) | Plane or surface to sketch on. | Yes |
|
||||
| `sketchSurfaceOrGroup` | [`SketchOrSurface`](/docs/kcl/types/SketchOrSurface) | Plane or surface to sketch on. | Yes |
|
||||
| `tag` | [`TagDeclarator`](/docs/kcl/types#tag-declaration) | Identifier for the circle to reference elsewhere. | No |
|
||||
|
||||
### Returns
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
---
|
||||
title: "std::prelude::HALF_TURN"
|
||||
excerpt: ""
|
||||
layout: manual
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
```js
|
||||
std::prelude::HALF_TURN: number(deg) = 180deg
|
||||
```
|
||||
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
---
|
||||
title: "std::prelude::QUARTER_TURN"
|
||||
excerpt: ""
|
||||
layout: manual
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
```js
|
||||
std::prelude::QUARTER_TURN: number(deg) = 90deg
|
||||
```
|
||||
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
---
|
||||
title: "std::prelude::THREE_QUARTER_TURN"
|
||||
excerpt: ""
|
||||
layout: manual
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
```js
|
||||
std::prelude::THREE_QUARTER_TURN: number(deg) = 270deg
|
||||
```
|
||||
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
---
|
||||
title: "std::prelude::ZERO"
|
||||
excerpt: ""
|
||||
layout: manual
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
```js
|
||||
std::prelude::ZERO: number = 0
|
||||
```
|
||||
|
||||
|
||||
15
docs/kcl/const_std-HALF_TURN.md
Normal file
@ -0,0 +1,15 @@
|
||||
---
|
||||
title: "std::HALF_TURN"
|
||||
excerpt: ""
|
||||
layout: manual
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
```js
|
||||
std::HALF_TURN: number(deg) = 180deg
|
||||
```
|
||||
|
||||
|
||||
15
docs/kcl/const_std-QUARTER_TURN.md
Normal file
@ -0,0 +1,15 @@
|
||||
---
|
||||
title: "std::QUARTER_TURN"
|
||||
excerpt: ""
|
||||
layout: manual
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
```js
|
||||
std::QUARTER_TURN: number(deg) = 90deg
|
||||
```
|
||||
|
||||
|
||||
15
docs/kcl/const_std-THREE_QUARTER_TURN.md
Normal file
@ -0,0 +1,15 @@
|
||||
---
|
||||
title: "std::THREE_QUARTER_TURN"
|
||||
excerpt: ""
|
||||
layout: manual
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
```js
|
||||
std::THREE_QUARTER_TURN: number(deg) = 270deg
|
||||
```
|
||||
|
||||
|
||||
15
docs/kcl/const_std-ZERO.md
Normal file
@ -0,0 +1,15 @@
|
||||
---
|
||||
title: "std::ZERO"
|
||||
excerpt: ""
|
||||
layout: manual
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
```js
|
||||
std::ZERO: number = 0
|
||||
```
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ Extend a 2-dimensional sketch through a third dimension in order to
|
||||
create new 3-dimensional volume, or if extruded into an existing volume, cut into an existing solid.
|
||||
|
||||
```js
|
||||
extrude(sketch_set: SketchSet, length: number) -> SolidSet
|
||||
extrude(sketchSet: SketchSet, length: number) -> SolidSet
|
||||
```
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ extrude(sketch_set: SketchSet, length: number) -> SolidSet
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `sketch_set` | [`SketchSet`](/docs/kcl/types/SketchSet) | Which sketches should be extruded | Yes |
|
||||
| `sketchSet` | [`SketchSet`](/docs/kcl/types/SketchSet) | Which sketches should be extruded | Yes |
|
||||
| `length` | `number` | How far to extrude the given sketches | Yes |
|
||||
|
||||
### Returns
|
||||
|
||||
@ -9,7 +9,7 @@ Create a helix.
|
||||
|
||||
|
||||
```js
|
||||
helix(revolutions: number, angle_start: number, ccw?: bool, radius: number, axis: Axis3dOrEdgeReference, length?: number) -> HelixValue
|
||||
helix(revolutions: number, angleStart: number, ccw?: bool, radius: number, axis: Axis3dOrEdgeReference, length?: number) -> HelixValue
|
||||
```
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@ helix(revolutions: number, angle_start: number, ccw?: bool, radius: number, axis
|
||||
| Name | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `revolutions` | `number` | Number of revolutions. | Yes |
|
||||
| `angle_start` | `number` | Start angle (in degrees). | Yes |
|
||||
| `angleStart` | `number` | Start angle (in degrees). | Yes |
|
||||
| `ccw` | `bool` | Is the helix rotation counter clockwise? The default is `false`. | No |
|
||||
| `radius` | `number` | Radius of the helix. | Yes |
|
||||
| `axis` | [`Axis3dOrEdgeReference`](/docs/kcl/types/Axis3dOrEdgeReference) | Axis to use for the helix. | Yes |
|
||||
|
||||
@ -9,7 +9,7 @@ Use a 2-dimensional sketch to cut a hole in another 2-dimensional sketch.
|
||||
|
||||
|
||||
```js
|
||||
hole(hole_sketch: SketchSet, sketch: Sketch) -> Sketch
|
||||
hole(holeSketch: SketchSet, sketch: Sketch) -> Sketch
|
||||
```
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ hole(hole_sketch: SketchSet, sketch: Sketch) -> Sketch
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `hole_sketch` | [`SketchSet`](/docs/kcl/types/SketchSet) | A sketch or a group of sketches. | Yes |
|
||||
| `holeSketch` | [`SketchSet`](/docs/kcl/types/SketchSet) | A sketch or a group of sketches. | Yes |
|
||||
| `sketch` | [`Sketch`](/docs/kcl/types/Sketch) | A sketch is a collection of paths. | Yes |
|
||||
|
||||
### Returns
|
||||
|
||||
@ -15,7 +15,7 @@ For formats lacking unit data (such as STL, OBJ, or PLY files), the default unit
|
||||
Note: The import command currently only works when using the native Modeling App.
|
||||
|
||||
```js
|
||||
import(file_path: String, options?: ImportFormat) -> ImportedGeometry
|
||||
import(filePath: String, options?: ImportFormat) -> ImportedGeometry
|
||||
```
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@ import(file_path: String, options?: ImportFormat) -> ImportedGeometry
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `file_path` | `String` | | Yes |
|
||||
| `filePath` | `String` | | Yes |
|
||||
| `options` | [`ImportFormat`](/docs/kcl/types/ImportFormat) | Import format specifier | No |
|
||||
|
||||
### Returns
|
||||
|
||||
@ -10,6 +10,10 @@ layout: manual
|
||||
* [Modules](kcl/modules)
|
||||
* [Known Issues](kcl/KNOWN-ISSUES)
|
||||
* **`std`**
|
||||
* [`HALF_TURN`](kcl/const_std-HALF_TURN)
|
||||
* [`QUARTER_TURN`](kcl/const_std-QUARTER_TURN)
|
||||
* [`THREE_QUARTER_TURN`](kcl/const_std-THREE_QUARTER_TURN)
|
||||
* [`ZERO`](kcl/const_std-ZERO)
|
||||
* [`abs`](kcl/abs)
|
||||
* [`acos`](kcl/acos)
|
||||
* [`angleToMatchLengthX`](kcl/angleToMatchLengthX)
|
||||
@ -118,8 +122,3 @@ layout: manual
|
||||
* [`cos`](kcl/std-math-cos)
|
||||
* [`sin`](kcl/std-math-sin)
|
||||
* [`tan`](kcl/std-math-tan)
|
||||
* **`std::prelude`**
|
||||
* [`HALF_TURN`](kcl/const_std-prelude-HALF_TURN)
|
||||
* [`QUARTER_TURN`](kcl/const_std-prelude-QUARTER_TURN)
|
||||
* [`THREE_QUARTER_TURN`](kcl/const_std-prelude-THREE_QUARTER_TURN)
|
||||
* [`ZERO`](kcl/const_std-prelude-ZERO)
|
||||
|
||||
@ -9,7 +9,7 @@ Extend the current sketch with a new straight line.
|
||||
|
||||
|
||||
```js
|
||||
line(sketch: Sketch, end_absolute?: [number], end?: [number], tag?: TagDeclarator) -> Sketch
|
||||
line(sketch: Sketch, endAbsolute?: [number], end?: [number], tag?: TagDeclarator) -> Sketch
|
||||
```
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@ line(sketch: Sketch, end_absolute?: [number], end?: [number], tag?: TagDeclarato
|
||||
| Name | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `sketch` | [`Sketch`](/docs/kcl/types/Sketch) | Which sketch should this path be added to? | Yes |
|
||||
| `end_absolute` | `[number]` | Which absolute point should this line go to? Incompatible with `end`. | No |
|
||||
| `endAbsolute` | `[number]` | Which absolute point should this line go to? Incompatible with `end`. | No |
|
||||
| `end` | `[number]` | How far away (along the X and Y axes) should this line go? Incompatible with `endAbsolute`. | No |
|
||||
| `tag` | [`TagDeclarator`](/docs/kcl/types#tag-declaration) | Create a new tag which refers to this line | No |
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ Create a 3D surface or solid by interpolating between two or more sketches.
|
||||
The sketches need to closed and on the same plane.
|
||||
|
||||
```js
|
||||
loft(sketches: [Sketch], v_degree: NonZeroU32, bez_approximate_rational: bool, base_curve_index?: integer, tolerance?: number) -> Solid
|
||||
loft(sketches: [Sketch], vDegree: NonZeroU32, bezApproximateRational: bool, baseCurveIndex?: integer, tolerance?: number) -> Solid
|
||||
```
|
||||
|
||||
|
||||
@ -18,9 +18,9 @@ loft(sketches: [Sketch], v_degree: NonZeroU32, bez_approximate_rational: bool, b
|
||||
| Name | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `sketches` | [`[Sketch]`](/docs/kcl/types/Sketch) | Which sketches to loft. Must include at least 2 sketches. | Yes |
|
||||
| `v_degree` | `NonZeroU32` | Degree of the interpolation. Must be greater than zero. For example, use 2 for quadratic, or 3 for cubic interpolation in the V direction. This defaults to 2, if not specified. | Yes |
|
||||
| `bez_approximate_rational` | `bool` | Attempt to approximate rational curves (such as arcs) using a bezier. This will remove banding around interpolations between arcs and non-arcs. It may produce errors in other scenarios Over time, this field won't be necessary. | Yes |
|
||||
| `base_curve_index` | `integer` | This can be set to override the automatically determined topological base curve, which is usually the first section encountered. | No |
|
||||
| `vDegree` | `NonZeroU32` | Degree of the interpolation. Must be greater than zero. For example, use 2 for quadratic, or 3 for cubic interpolation in the V direction. This defaults to 2, if not specified. | Yes |
|
||||
| `bezApproximateRational` | `bool` | Attempt to approximate rational curves (such as arcs) using a bezier. This will remove banding around interpolations between arcs and non-arcs. It may produce errors in other scenarios Over time, this field won't be necessary. | Yes |
|
||||
| `baseCurveIndex` | `integer` | This can be set to override the automatically determined topological base curve, which is usually the first section encountered. | No |
|
||||
| `tolerance` | `number` | Tolerance for the loft operation. | No |
|
||||
|
||||
### Returns
|
||||
|
||||
@ -9,7 +9,7 @@ Apply a function to every element of a list.
|
||||
Given a list like `[a, b, c]`, and a function like `f`, returns `[f(a), f(b), f(c)]`
|
||||
|
||||
```js
|
||||
map(array: [KclValue], map_fn: FunctionSource) -> [KclValue]
|
||||
map(array: [KclValue], mapFn: FunctionSource) -> [KclValue]
|
||||
```
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@ map(array: [KclValue], map_fn: FunctionSource) -> [KclValue]
|
||||
| Name | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `array` | [`[KclValue]`](/docs/kcl/types/KclValue) | | Yes |
|
||||
| `map_fn` | `FunctionSource` | | Yes |
|
||||
| `mapFn` | `FunctionSource` | | Yes |
|
||||
|
||||
### Returns
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ Only works on unclosed sketches for now.
|
||||
Mirror occurs around a local sketch axis rather than a global axis.
|
||||
|
||||
```js
|
||||
mirror2d(data: Mirror2dData, sketch_set: SketchSet) -> [Sketch]
|
||||
mirror2d(data: Mirror2dData, sketchSet: SketchSet) -> [Sketch]
|
||||
```
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@ mirror2d(data: Mirror2dData, sketch_set: SketchSet) -> [Sketch]
|
||||
| Name | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `data` | [`Mirror2dData`](/docs/kcl/types/Mirror2dData) | Data for a mirror. | Yes |
|
||||
| `sketch_set` | [`SketchSet`](/docs/kcl/types/SketchSet) | A sketch or a group of sketches. | Yes |
|
||||
| `sketchSet` | [`SketchSet`](/docs/kcl/types/SketchSet) | A sketch or a group of sketches. | Yes |
|
||||
|
||||
### Returns
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ Repeat a 2-dimensional sketch some number of times along a partial or
|
||||
complete circle some specified number of times. Each object may additionally be rotated along the circle, ensuring orentation of the solid with respect to the center of the circle is maintained.
|
||||
|
||||
```js
|
||||
patternCircular2d(sketch_set: SketchSet, instances: integer, center: [number], arc_degrees: number, rotate_duplicates: bool, use_original?: bool) -> [Sketch]
|
||||
patternCircular2d(sketchSet: SketchSet, instances: integer, center: [number], arcDegrees: number, rotateDuplicates: bool, useOriginal?: bool) -> [Sketch]
|
||||
```
|
||||
|
||||
|
||||
@ -17,12 +17,12 @@ patternCircular2d(sketch_set: SketchSet, instances: integer, center: [number], a
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `sketch_set` | [`SketchSet`](/docs/kcl/types/SketchSet) | Which sketch(es) to pattern | Yes |
|
||||
| `sketchSet` | [`SketchSet`](/docs/kcl/types/SketchSet) | Which sketch(es) to pattern | Yes |
|
||||
| `instances` | `integer` | The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | Yes |
|
||||
| `center` | `[number]` | The center about which to make the pattern. This is a 2D vector. | Yes |
|
||||
| `arc_degrees` | `number` | The arc angle (in degrees) to place the repetitions. Must be greater than 0. | Yes |
|
||||
| `rotate_duplicates` | `bool` | Whether or not to rotate the duplicates as they are copied. | Yes |
|
||||
| `use_original` | `bool` | If the target was sketched on an extrusion, setting this will use the original sketch as the target, not the entire joined solid. Defaults to false. | No |
|
||||
| `arcDegrees` | `number` | The arc angle (in degrees) to place the repetitions. Must be greater than 0. | Yes |
|
||||
| `rotateDuplicates` | `bool` | Whether or not to rotate the duplicates as they are copied. | Yes |
|
||||
| `useOriginal` | `bool` | If the target was sketched on an extrusion, setting this will use the original sketch as the target, not the entire joined solid. Defaults to false. | No |
|
||||
|
||||
### Returns
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ Repeat a 3-dimensional solid some number of times along a partial or
|
||||
complete circle some specified number of times. Each object may additionally be rotated along the circle, ensuring orentation of the solid with respect to the center of the circle is maintained.
|
||||
|
||||
```js
|
||||
patternCircular3d(solid_set: SolidSet, instances: integer, axis: [number], center: [number], arc_degrees: number, rotate_duplicates: bool, use_original?: bool) -> [Solid]
|
||||
patternCircular3d(solidSet: SolidSet, instances: integer, axis: [number], center: [number], arcDegrees: number, rotateDuplicates: bool, useOriginal?: bool) -> [Solid]
|
||||
```
|
||||
|
||||
|
||||
@ -17,13 +17,13 @@ patternCircular3d(solid_set: SolidSet, instances: integer, axis: [number], cente
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `solid_set` | [`SolidSet`](/docs/kcl/types/SolidSet) | Which solid(s) to pattern | Yes |
|
||||
| `solidSet` | [`SolidSet`](/docs/kcl/types/SolidSet) | Which solid(s) to pattern | Yes |
|
||||
| `instances` | `integer` | The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | Yes |
|
||||
| `axis` | `[number]` | The axis around which to make the pattern. This is a 3D vector | Yes |
|
||||
| `center` | `[number]` | The center about which to make the pattern. This is a 3D vector. | Yes |
|
||||
| `arc_degrees` | `number` | The arc angle (in degrees) to place the repetitions. Must be greater than 0. | Yes |
|
||||
| `rotate_duplicates` | `bool` | Whether or not to rotate the duplicates as they are copied. | Yes |
|
||||
| `use_original` | `bool` | If the target was sketched on an extrusion, setting this will use the original sketch as the target, not the entire joined solid. Defaults to false. | No |
|
||||
| `arcDegrees` | `number` | The arc angle (in degrees) to place the repetitions. Must be greater than 0. | Yes |
|
||||
| `rotateDuplicates` | `bool` | Whether or not to rotate the duplicates as they are copied. | Yes |
|
||||
| `useOriginal` | `bool` | If the target was sketched on an extrusion, setting this will use the original sketch as the target, not the entire joined solid. Defaults to false. | No |
|
||||
|
||||
### Returns
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ Repeat a 2-dimensional sketch along some dimension, with a dynamic amount
|
||||
of distance between each repetition, some specified number of times.
|
||||
|
||||
```js
|
||||
patternLinear2d(sketch_set: SketchSet, instances: integer, distance: number, axis: [number], use_original?: bool) -> [Sketch]
|
||||
patternLinear2d(sketchSet: SketchSet, instances: integer, distance: number, axis: [number], useOriginal?: bool) -> [Sketch]
|
||||
```
|
||||
|
||||
|
||||
@ -17,11 +17,11 @@ patternLinear2d(sketch_set: SketchSet, instances: integer, distance: number, axi
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `sketch_set` | [`SketchSet`](/docs/kcl/types/SketchSet) | The sketch(es) to duplicate | Yes |
|
||||
| `sketchSet` | [`SketchSet`](/docs/kcl/types/SketchSet) | The sketch(es) to duplicate | Yes |
|
||||
| `instances` | `integer` | The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | Yes |
|
||||
| `distance` | `number` | Distance between each repetition. Also known as 'spacing'. | Yes |
|
||||
| `axis` | `[number]` | The axis of the pattern. A 2D vector. | Yes |
|
||||
| `use_original` | `bool` | If the target was sketched on an extrusion, setting this will use the original sketch as the target, not the entire joined solid. Defaults to false. | No |
|
||||
| `useOriginal` | `bool` | If the target was sketched on an extrusion, setting this will use the original sketch as the target, not the entire joined solid. Defaults to false. | No |
|
||||
|
||||
### Returns
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ Repeat a 3-dimensional solid along a linear path, with a dynamic amount
|
||||
of distance between each repetition, some specified number of times.
|
||||
|
||||
```js
|
||||
patternLinear3d(solid_set: SolidSet, instances: integer, distance: number, axis: [number], use_original?: bool) -> [Solid]
|
||||
patternLinear3d(solidSet: SolidSet, instances: integer, distance: number, axis: [number], useOriginal?: bool) -> [Solid]
|
||||
```
|
||||
|
||||
|
||||
@ -17,11 +17,11 @@ patternLinear3d(solid_set: SolidSet, instances: integer, distance: number, axis:
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `solid_set` | [`SolidSet`](/docs/kcl/types/SolidSet) | The solid(s) to duplicate | Yes |
|
||||
| `solidSet` | [`SolidSet`](/docs/kcl/types/SolidSet) | The solid(s) to duplicate | Yes |
|
||||
| `instances` | `integer` | The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | Yes |
|
||||
| `distance` | `number` | Distance between each repetition. Also known as 'spacing'. | Yes |
|
||||
| `axis` | `[number]` | The axis of the pattern. A 2D vector. | Yes |
|
||||
| `use_original` | `bool` | If the target was sketched on an extrusion, setting this will use the original sketch as the target, not the entire joined solid. Defaults to false. | No |
|
||||
| `useOriginal` | `bool` | If the target was sketched on an extrusion, setting this will use the original sketch as the target, not the entire joined solid. Defaults to false. | No |
|
||||
|
||||
### Returns
|
||||
|
||||
|
||||
@ -35,7 +35,7 @@ The transform function returns a transform object. All properties of the object
|
||||
- `rotation.origin` (either "local" i.e. rotate around its own center, "global" i.e. rotate around the scene's center, or a 3D point, defaults to "local")
|
||||
|
||||
```js
|
||||
patternTransform(solid_set: SolidSet, instances: integer, transform: FunctionSource, use_original?: bool) -> [Solid]
|
||||
patternTransform(solidSet: SolidSet, instances: integer, transform: FunctionSource, useOriginal?: bool) -> [Solid]
|
||||
```
|
||||
|
||||
|
||||
@ -43,10 +43,10 @@ patternTransform(solid_set: SolidSet, instances: integer, transform: FunctionSou
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `solid_set` | [`SolidSet`](/docs/kcl/types/SolidSet) | The solid(s) to duplicate | Yes |
|
||||
| `solidSet` | [`SolidSet`](/docs/kcl/types/SolidSet) | The solid(s) to duplicate | Yes |
|
||||
| `instances` | `integer` | The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | Yes |
|
||||
| `transform` | `FunctionSource` | How each replica should be transformed. The transform function takes a single parameter: an integer representing which number replication the transform is for. E.g. the first replica to be transformed will be passed the argument `1`. This simplifies your math: the transform function can rely on id `0` being the original instance passed into the `patternTransform`. See the examples. | Yes |
|
||||
| `use_original` | `bool` | If the target was sketched on an extrusion, setting this will use the original sketch as the target, not the entire joined solid. Defaults to false. | No |
|
||||
| `useOriginal` | `bool` | If the target was sketched on an extrusion, setting this will use the original sketch as the target, not the entire joined solid. Defaults to false. | No |
|
||||
|
||||
### Returns
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ Just like patternTransform, but works on 2D sketches not 3D solids.
|
||||
|
||||
|
||||
```js
|
||||
patternTransform2d(sketch_set: SketchSet, instances: integer, transform: FunctionSource, use_original?: bool) -> [Sketch]
|
||||
patternTransform2d(sketchSet: SketchSet, instances: integer, transform: FunctionSource, useOriginal?: bool) -> [Sketch]
|
||||
```
|
||||
|
||||
|
||||
@ -17,10 +17,10 @@ patternTransform2d(sketch_set: SketchSet, instances: integer, transform: Functio
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `sketch_set` | [`SketchSet`](/docs/kcl/types/SketchSet) | The sketch(es) to duplicate | Yes |
|
||||
| `sketchSet` | [`SketchSet`](/docs/kcl/types/SketchSet) | The sketch(es) to duplicate | Yes |
|
||||
| `instances` | `integer` | The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | Yes |
|
||||
| `transform` | `FunctionSource` | How each replica should be transformed. The transform function takes a single parameter: an integer representing which number replication the transform is for. E.g. the first replica to be transformed will be passed the argument `1`. This simplifies your math: the transform function can rely on id `0` being the original instance passed into the `patternTransform`. See the examples. | Yes |
|
||||
| `use_original` | `bool` | If the target was sketched on an extrusion, setting this will use the original sketch as the target, not the entire joined solid. Defaults to false. | No |
|
||||
| `useOriginal` | `bool` | If the target was sketched on an extrusion, setting this will use the original sketch as the target, not the entire joined solid. Defaults to false. | No |
|
||||
|
||||
### Returns
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ Create a regular polygon with the specified number of sides that is either inscr
|
||||
|
||||
|
||||
```js
|
||||
polygon(data: PolygonData, sketch_surface_or_group: SketchOrSurface, tag?: TagDeclarator) -> Sketch
|
||||
polygon(data: PolygonData, sketchSurfaceOrGroup: SketchOrSurface, tag?: TagDeclarator) -> Sketch
|
||||
```
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@ polygon(data: PolygonData, sketch_surface_or_group: SketchOrSurface, tag?: TagDe
|
||||
| Name | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `data` | [`PolygonData`](/docs/kcl/types/PolygonData) | Data for drawing a polygon | Yes |
|
||||
| `sketch_surface_or_group` | [`SketchOrSurface`](/docs/kcl/types/SketchOrSurface) | A sketch surface or a sketch. | Yes |
|
||||
| `sketchSurfaceOrGroup` | [`SketchOrSurface`](/docs/kcl/types/SketchOrSurface) | A sketch surface or a sketch. | Yes |
|
||||
| `tag` | [`TagDeclarator`](/docs/kcl/types#tag-declaration) | | No |
|
||||
|
||||
### Returns
|
||||
|
||||
@ -9,7 +9,7 @@ Take a starting value. Then, for each element of an array, calculate the next va
|
||||
using the previous value and the element.
|
||||
|
||||
```js
|
||||
reduce(array: [KclValue], start: KclValue, reduce_fn: FunctionSource) -> KclValue
|
||||
reduce(array: [KclValue], start: KclValue, reduceFn: FunctionSource) -> KclValue
|
||||
```
|
||||
|
||||
|
||||
@ -19,7 +19,7 @@ reduce(array: [KclValue], start: KclValue, reduce_fn: FunctionSource) -> KclValu
|
||||
|----------|------|-------------|----------|
|
||||
| `array` | [`[KclValue]`](/docs/kcl/types/KclValue) | | Yes |
|
||||
| `start` | [`KclValue`](/docs/kcl/types/KclValue) | Any KCL value. | Yes |
|
||||
| `reduce_fn` | `FunctionSource` | | Yes |
|
||||
| `reduceFn` | `FunctionSource` | | Yes |
|
||||
|
||||
### Returns
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ Remove volume from a 3-dimensional shape such that a wall of the
|
||||
provided thickness remains, taking volume starting at the provided face, leaving it open in that direction.
|
||||
|
||||
```js
|
||||
shell(solid_set: SolidSet, thickness: number, faces: [FaceTag]) -> SolidSet
|
||||
shell(solidSet: SolidSet, thickness: number, faces: [FaceTag]) -> SolidSet
|
||||
```
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ shell(solid_set: SolidSet, thickness: number, faces: [FaceTag]) -> SolidSet
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `solid_set` | [`SolidSet`](/docs/kcl/types/SolidSet) | Which solid (or solids) to shell out | Yes |
|
||||
| `solidSet` | [`SolidSet`](/docs/kcl/types/SolidSet) | Which solid (or solids) to shell out | Yes |
|
||||
| `thickness` | `number` | The thickness of the shell | Yes |
|
||||
| `faces` | [`[FaceTag]`](/docs/kcl/types/FaceTag) | The faces you want removed | Yes |
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ Start a new profile at a given point.
|
||||
|
||||
|
||||
```js
|
||||
startProfileAt(to: [number], sketch_surface: SketchSurface, tag?: TagDeclarator) -> Sketch
|
||||
startProfileAt(to: [number], sketchSurface: SketchSurface, tag?: TagDeclarator) -> Sketch
|
||||
```
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@ startProfileAt(to: [number], sketch_surface: SketchSurface, tag?: TagDeclarator)
|
||||
| Name | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `to` | `[number]` | | Yes |
|
||||
| `sketch_surface` | [`SketchSurface`](/docs/kcl/types/SketchSurface) | A sketch type. | Yes |
|
||||
| `sketchSurface` | [`SketchSurface`](/docs/kcl/types/SketchSurface) | A sketch type. | Yes |
|
||||
| `tag` | [`TagDeclarator`](/docs/kcl/types#tag-declaration) | | No |
|
||||
|
||||
### Returns
|
||||
|
||||
@ -38863,7 +38863,7 @@
|
||||
"keywordArguments": true,
|
||||
"args": [
|
||||
{
|
||||
"name": "solid_set",
|
||||
"name": "solidSet",
|
||||
"type": "SolidSet",
|
||||
"schema": {
|
||||
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
|
||||
@ -70936,7 +70936,7 @@
|
||||
"labelRequired": true
|
||||
},
|
||||
{
|
||||
"name": "sketch_surface_or_group",
|
||||
"name": "sketchSurfaceOrGroup",
|
||||
"type": "SketchOrSurface",
|
||||
"schema": {
|
||||
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
|
||||
@ -75887,7 +75887,7 @@
|
||||
"labelRequired": true
|
||||
},
|
||||
{
|
||||
"name": "sketch_surface_or_group",
|
||||
"name": "sketchSurfaceOrGroup",
|
||||
"type": "SketchOrSurface",
|
||||
"schema": {
|
||||
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
|
||||
@ -85796,7 +85796,7 @@
|
||||
"keywordArguments": true,
|
||||
"args": [
|
||||
{
|
||||
"name": "sketch_set",
|
||||
"name": "sketchSet",
|
||||
"type": "SketchSet",
|
||||
"schema": {
|
||||
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
|
||||
@ -104094,7 +104094,7 @@
|
||||
"labelRequired": true
|
||||
},
|
||||
{
|
||||
"name": "angle_start",
|
||||
"name": "angleStart",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
|
||||
@ -110158,7 +110158,7 @@
|
||||
"keywordArguments": false,
|
||||
"args": [
|
||||
{
|
||||
"name": "hole_sketch",
|
||||
"name": "holeSketch",
|
||||
"type": "SketchSet",
|
||||
"schema": {
|
||||
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
|
||||
@ -118587,7 +118587,7 @@
|
||||
"keywordArguments": false,
|
||||
"args": [
|
||||
{
|
||||
"name": "file_path",
|
||||
"name": "filePath",
|
||||
"type": "String",
|
||||
"schema": {
|
||||
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
|
||||
@ -124304,7 +124304,7 @@
|
||||
"labelRequired": false
|
||||
},
|
||||
{
|
||||
"name": "end_absolute",
|
||||
"name": "endAbsolute",
|
||||
"type": "[number]",
|
||||
"schema": {
|
||||
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
|
||||
@ -132449,7 +132449,7 @@
|
||||
"labelRequired": false
|
||||
},
|
||||
{
|
||||
"name": "v_degree",
|
||||
"name": "vDegree",
|
||||
"type": "NonZeroU32",
|
||||
"schema": {
|
||||
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
|
||||
@ -134051,7 +134051,7 @@
|
||||
"labelRequired": true
|
||||
},
|
||||
{
|
||||
"name": "bez_approximate_rational",
|
||||
"name": "bezApproximateRational",
|
||||
"type": "bool",
|
||||
"schema": {
|
||||
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
|
||||
@ -135651,7 +135651,7 @@
|
||||
"labelRequired": true
|
||||
},
|
||||
{
|
||||
"name": "base_curve_index",
|
||||
"name": "baseCurveIndex",
|
||||
"type": "integer",
|
||||
"schema": {
|
||||
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
|
||||
@ -143301,7 +143301,7 @@
|
||||
"labelRequired": true
|
||||
},
|
||||
{
|
||||
"name": "map_fn",
|
||||
"name": "mapFn",
|
||||
"type": "FunctionSource",
|
||||
"schema": {
|
||||
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
|
||||
@ -149807,7 +149807,7 @@
|
||||
"labelRequired": true
|
||||
},
|
||||
{
|
||||
"name": "sketch_set",
|
||||
"name": "sketchSet",
|
||||
"type": "SketchSet",
|
||||
"schema": {
|
||||
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
|
||||
@ -153703,7 +153703,7 @@
|
||||
"keywordArguments": true,
|
||||
"args": [
|
||||
{
|
||||
"name": "sketch_set",
|
||||
"name": "sketchSet",
|
||||
"type": "SketchSet",
|
||||
"schema": {
|
||||
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
|
||||
@ -158613,7 +158613,7 @@
|
||||
"labelRequired": true
|
||||
},
|
||||
{
|
||||
"name": "arc_degrees",
|
||||
"name": "arcDegrees",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
|
||||
@ -160214,7 +160214,7 @@
|
||||
"labelRequired": true
|
||||
},
|
||||
{
|
||||
"name": "rotate_duplicates",
|
||||
"name": "rotateDuplicates",
|
||||
"type": "bool",
|
||||
"schema": {
|
||||
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
|
||||
@ -161814,7 +161814,7 @@
|
||||
"labelRequired": true
|
||||
},
|
||||
{
|
||||
"name": "use_original",
|
||||
"name": "useOriginal",
|
||||
"type": "bool",
|
||||
"schema": {
|
||||
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
|
||||
@ -165030,7 +165030,7 @@
|
||||
"keywordArguments": true,
|
||||
"args": [
|
||||
{
|
||||
"name": "solid_set",
|
||||
"name": "solidSet",
|
||||
"type": "SolidSet",
|
||||
"schema": {
|
||||
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
|
||||
@ -171550,7 +171550,7 @@
|
||||
"labelRequired": true
|
||||
},
|
||||
{
|
||||
"name": "arc_degrees",
|
||||
"name": "arcDegrees",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
|
||||
@ -173151,7 +173151,7 @@
|
||||
"labelRequired": true
|
||||
},
|
||||
{
|
||||
"name": "rotate_duplicates",
|
||||
"name": "rotateDuplicates",
|
||||
"type": "bool",
|
||||
"schema": {
|
||||
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
|
||||
@ -174751,7 +174751,7 @@
|
||||
"labelRequired": true
|
||||
},
|
||||
{
|
||||
"name": "use_original",
|
||||
"name": "useOriginal",
|
||||
"type": "bool",
|
||||
"schema": {
|
||||
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
|
||||
@ -177967,7 +177967,7 @@
|
||||
"keywordArguments": true,
|
||||
"args": [
|
||||
{
|
||||
"name": "sketch_set",
|
||||
"name": "sketchSet",
|
||||
"type": "SketchSet",
|
||||
"schema": {
|
||||
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
|
||||
@ -184478,7 +184478,7 @@
|
||||
"labelRequired": true
|
||||
},
|
||||
{
|
||||
"name": "use_original",
|
||||
"name": "useOriginal",
|
||||
"type": "bool",
|
||||
"schema": {
|
||||
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
|
||||
@ -187694,7 +187694,7 @@
|
||||
"keywordArguments": true,
|
||||
"args": [
|
||||
{
|
||||
"name": "solid_set",
|
||||
"name": "solidSet",
|
||||
"type": "SolidSet",
|
||||
"schema": {
|
||||
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
|
||||
@ -194209,7 +194209,7 @@
|
||||
"labelRequired": true
|
||||
},
|
||||
{
|
||||
"name": "use_original",
|
||||
"name": "useOriginal",
|
||||
"type": "bool",
|
||||
"schema": {
|
||||
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
|
||||
@ -197425,7 +197425,7 @@
|
||||
"keywordArguments": true,
|
||||
"args": [
|
||||
{
|
||||
"name": "solid_set",
|
||||
"name": "solidSet",
|
||||
"type": "SolidSet",
|
||||
"schema": {
|
||||
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
|
||||
@ -202333,7 +202333,7 @@
|
||||
"labelRequired": true
|
||||
},
|
||||
{
|
||||
"name": "use_original",
|
||||
"name": "useOriginal",
|
||||
"type": "bool",
|
||||
"schema": {
|
||||
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
|
||||
@ -205554,7 +205554,7 @@
|
||||
"keywordArguments": true,
|
||||
"args": [
|
||||
{
|
||||
"name": "sketch_set",
|
||||
"name": "sketchSet",
|
||||
"type": "SketchSet",
|
||||
"schema": {
|
||||
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
|
||||
@ -210458,7 +210458,7 @@
|
||||
"labelRequired": true
|
||||
},
|
||||
{
|
||||
"name": "use_original",
|
||||
"name": "useOriginal",
|
||||
"type": "bool",
|
||||
"schema": {
|
||||
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
|
||||
@ -214182,7 +214182,7 @@
|
||||
"labelRequired": true
|
||||
},
|
||||
{
|
||||
"name": "sketch_surface_or_group",
|
||||
"name": "sketchSurfaceOrGroup",
|
||||
"type": "SketchOrSurface",
|
||||
"schema": {
|
||||
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
|
||||
@ -244378,7 +244378,7 @@
|
||||
"labelRequired": true
|
||||
},
|
||||
{
|
||||
"name": "reduce_fn",
|
||||
"name": "reduceFn",
|
||||
"type": "FunctionSource",
|
||||
"schema": {
|
||||
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
|
||||
@ -263874,7 +263874,7 @@
|
||||
"keywordArguments": true,
|
||||
"args": [
|
||||
{
|
||||
"name": "solid_set",
|
||||
"name": "solidSet",
|
||||
"type": "SolidSet",
|
||||
"schema": {
|
||||
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
|
||||
@ -270603,7 +270603,7 @@
|
||||
"labelRequired": true
|
||||
},
|
||||
{
|
||||
"name": "sketch_surface",
|
||||
"name": "sketchSurface",
|
||||
"type": "SketchSurface",
|
||||
"schema": {
|
||||
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
|
||||
|
||||
@ -171,4 +171,22 @@ export class EditorFixture {
|
||||
{ text, placeCursor }
|
||||
)
|
||||
}
|
||||
async selectText(text: string) {
|
||||
// First make sure the code pane is open
|
||||
const wasPaneOpen = await this.checkIfPaneIsOpen()
|
||||
if (!wasPaneOpen) {
|
||||
await this.openPane()
|
||||
}
|
||||
|
||||
// Use Playwright's built-in text selection on the code content
|
||||
// it seems to only select whole divs, which works out to align with syntax highlighting
|
||||
// for code mirror, so you can probably select "sketch002 = startSketchOn('XZ')"
|
||||
// but less so for exactly "sketch002 = startS"
|
||||
await this.codeContent.getByText(text).first().selectText()
|
||||
|
||||
// Reset pane state if needed
|
||||
if (!wasPaneOpen) {
|
||||
await this.closePane()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -101,7 +101,8 @@ export class AuthenticatedTronApp {
|
||||
// running against electron applications.
|
||||
// The timeline is still broken but failure screenshots work again.
|
||||
this.context = context
|
||||
Object.assign(this.browserContext, this.context)
|
||||
// TODO: try to get this to work again for screenshots, but it messed with test ends when enabled
|
||||
// Object.assign(this.browserContext, this.context)
|
||||
|
||||
this.electronApp = electronApp
|
||||
this.dir = dir
|
||||
|
||||
@ -82,6 +82,16 @@ export class ToolbarFixture {
|
||||
startSketchPlaneSelection = async () =>
|
||||
doAndWaitForImageDiff(this.page, () => this.startSketchBtn.click(), 500)
|
||||
|
||||
exitSketch = async () => {
|
||||
await this.exitSketchBtn.click()
|
||||
await expect(
|
||||
this.page.getByRole('button', { name: 'Start Sketch' })
|
||||
).toBeVisible()
|
||||
await expect(
|
||||
this.page.getByRole('button', { name: 'Start Sketch' })
|
||||
).not.toBeDisabled()
|
||||
}
|
||||
|
||||
editSketch = async () => {
|
||||
await this.editSketchBtn.first().click()
|
||||
// One of the rare times we want to allow a arbitrary wait
|
||||
|
||||
@ -170,8 +170,7 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
|
||||
})
|
||||
|
||||
await test.step('Clean up so that `_sketchOnAChamfer` util can be called again', async () => {
|
||||
await toolbar.exitSketchBtn.click()
|
||||
await scene.waitForExecutionDone()
|
||||
await toolbar.exitSketch()
|
||||
})
|
||||
await test.step('Check there is no errors after code created in previous steps executes', async () => {
|
||||
await editor.expectState({
|
||||
@ -202,7 +201,9 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
|
||||
}, file)
|
||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||
await homePage.goToModelingScene()
|
||||
await scene.waitForExecutionDone()
|
||||
await expect(
|
||||
page.getByTestId('model-state-indicator-receive-reliable')
|
||||
).toBeVisible()
|
||||
|
||||
const sketchOnAChamfer = _sketchOnAChamfer(page, editor, toolbar, scene)
|
||||
|
||||
@ -390,6 +391,7 @@ profile001 = startProfileAt([205.96, 254.59], sketch002)
|
||||
}, file)
|
||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||
await homePage.goToModelingScene()
|
||||
|
||||
await scene.waitForExecutionDone()
|
||||
|
||||
const sketchOnAChamfer = _sketchOnAChamfer(page, editor, toolbar, scene)
|
||||
@ -1064,7 +1066,7 @@ openSketch = startSketchOn('XY')
|
||||
0
|
||||
)
|
||||
await operationButton.click({ button: 'left' })
|
||||
await page.keyboard.press('Backspace')
|
||||
await page.keyboard.press('Delete')
|
||||
await scene.expectPixelColor([50, 51, 96], testPoint, 15)
|
||||
})
|
||||
})
|
||||
@ -1171,7 +1173,7 @@ openSketch = startSketchOn('XY')
|
||||
await editor.closePane()
|
||||
const operationButton = await toolbar.getFeatureTreeOperation('Helix', 0)
|
||||
await operationButton.click({ button: 'left' })
|
||||
await page.keyboard.press('Backspace')
|
||||
await page.keyboard.press('Delete')
|
||||
// Red plane is back
|
||||
await scene.expectPixelColor([96, 52, 52], testPoint, 15)
|
||||
})
|
||||
@ -1263,7 +1265,7 @@ openSketch = startSketchOn('XY')
|
||||
await editor.closePane()
|
||||
const operationButton = await toolbar.getFeatureTreeOperation('Loft', 0)
|
||||
await operationButton.click({ button: 'left' })
|
||||
await page.keyboard.press('Backspace')
|
||||
await page.keyboard.press('Delete')
|
||||
await scene.expectPixelColor([254, 254, 254], testPoint, 15)
|
||||
})
|
||||
})
|
||||
@ -1306,7 +1308,7 @@ loft001 = loft([sketch001, sketch002])
|
||||
await expect(page.locator('.cm-activeLine')).toHaveText(`
|
||||
|> circle({ center = [0, 0], radius = 30 }, %)
|
||||
`)
|
||||
await page.keyboard.press('Backspace')
|
||||
await page.keyboard.press('Delete')
|
||||
// Check for sketch 1
|
||||
await scene.expectPixelColor([254, 254, 254], testPoint, 15)
|
||||
})
|
||||
@ -1317,7 +1319,7 @@ loft001 = loft([sketch001, sketch002])
|
||||
await expect(page.locator('.cm-activeLine')).toHaveText(`
|
||||
|> circle({ center = [0, 0], radius = 20 }, %)
|
||||
`)
|
||||
await page.keyboard.press('Backspace')
|
||||
await page.keyboard.press('Delete')
|
||||
// Check for plane001
|
||||
await scene.expectPixelColor([228, 228, 228], testPoint, 15)
|
||||
})
|
||||
@ -1328,7 +1330,7 @@ loft001 = loft([sketch001, sketch002])
|
||||
await expect(page.locator('.cm-activeLine')).toHaveText(`
|
||||
plane001 = offsetPlane('XZ', offset = 50)
|
||||
`)
|
||||
await page.keyboard.press('Backspace')
|
||||
await page.keyboard.press('Delete')
|
||||
// Check for sketch 1
|
||||
await scene.expectPixelColor([254, 254, 254], testPoint, 15)
|
||||
})
|
||||
@ -1422,7 +1424,7 @@ sketch002 = startSketchOn('XZ')
|
||||
await page.waitForTimeout(500)
|
||||
const operationButton = await toolbar.getFeatureTreeOperation('Sweep', 0)
|
||||
await operationButton.click({ button: 'left' })
|
||||
await page.keyboard.press('Backspace')
|
||||
await page.keyboard.press('Delete')
|
||||
await page.waitForTimeout(500)
|
||||
await toolbar.closePane('feature-tree')
|
||||
await scene.expectPixelColor([53, 53, 53], testPoint, 15)
|
||||
@ -1728,7 +1730,7 @@ extrude001 = extrude(sketch001, length = -12)
|
||||
1
|
||||
)
|
||||
await operationButton.click({ button: 'left' })
|
||||
await page.keyboard.press('Backspace')
|
||||
await page.keyboard.press('Delete')
|
||||
await page.waitForTimeout(500)
|
||||
await scene.expectPixelColor(edgeColorWhite, secondEdgeLocation, 15) // deleted
|
||||
await editor.expectEditor.not.toContain(secondFilletDeclaration)
|
||||
@ -1830,7 +1832,7 @@ fillet04 = fillet(extrude001, radius = 5, tags = [getOppositeEdge(seg02)])
|
||||
0
|
||||
)
|
||||
await operationButton.click({ button: 'left' })
|
||||
await page.keyboard.press('Backspace')
|
||||
await page.keyboard.press('Delete')
|
||||
await page.waitForTimeout(500)
|
||||
})
|
||||
await test.step('Verify piped fillet is deleted but other fillets are not (in the editor)', async () => {
|
||||
@ -1860,7 +1862,7 @@ fillet04 = fillet(extrude001, radius = 5, tags = [getOppositeEdge(seg02)])
|
||||
1
|
||||
)
|
||||
await operationButton.click({ button: 'left' })
|
||||
await page.keyboard.press('Backspace')
|
||||
await page.keyboard.press('Delete')
|
||||
await page.waitForTimeout(500)
|
||||
})
|
||||
await test.step('Verify non-piped fillet is deleted but other two fillets are not (in the editor)', async () => {
|
||||
@ -2099,7 +2101,7 @@ extrude001 = extrude(sketch001, length = -12)
|
||||
1
|
||||
)
|
||||
await operationButton.click({ button: 'left' })
|
||||
await page.keyboard.press('Backspace')
|
||||
await page.keyboard.press('Delete')
|
||||
await page.waitForTimeout(500)
|
||||
await scene.expectPixelColor(edgeColorWhite, secondEdgeLocation, 15) // deleted
|
||||
await scene.expectPixelColor(chamferColor, firstEdgeLocation, 15) // stayed
|
||||
@ -2202,7 +2204,7 @@ chamfer04 = chamfer(extrude001, length = 5, tags = [getOppositeEdge(seg02)])
|
||||
0
|
||||
)
|
||||
await operationButton.click({ button: 'left' })
|
||||
await page.keyboard.press('Backspace')
|
||||
await page.keyboard.press('Delete')
|
||||
await page.waitForTimeout(500)
|
||||
})
|
||||
await test.step('Verify piped chamfer is deleted but other chamfers are not (in the editor)', async () => {
|
||||
@ -2234,7 +2236,7 @@ chamfer04 = chamfer(extrude001, length = 5, tags = [getOppositeEdge(seg02)])
|
||||
1
|
||||
)
|
||||
await operationButton.click({ button: 'left' })
|
||||
await page.keyboard.press('Backspace')
|
||||
await page.keyboard.press('Delete')
|
||||
await page.waitForTimeout(500)
|
||||
})
|
||||
await test.step('Verify non-piped chamfer is deleted but other two chamfers are not (in the editor)', async () => {
|
||||
@ -2438,7 +2440,7 @@ extrude001 = extrude(sketch001, length = 40)
|
||||
await editor.closePane()
|
||||
const operationButton = await toolbar.getFeatureTreeOperation('Shell', 0)
|
||||
await operationButton.click({ button: 'left' })
|
||||
await page.keyboard.press('Backspace')
|
||||
await page.keyboard.press('Delete')
|
||||
await scene.expectPixelColor([99, 99, 99], testPoint, 15)
|
||||
})
|
||||
})
|
||||
@ -2577,7 +2579,7 @@ profile001 = startProfileAt([-20, 20], sketch001)
|
||||
const deleteOperation = async (operationButton: Locator) => {
|
||||
if (shouldUseKeyboard) {
|
||||
await operationButton.click({ button: 'left' })
|
||||
await page.keyboard.press('Backspace')
|
||||
await page.keyboard.press('Delete')
|
||||
} else {
|
||||
await operationButton.click({ button: 'right' })
|
||||
const editButton = page.getByTestId('context-menu-delete')
|
||||
|
||||
@ -36,7 +36,7 @@ extrude003 = extrude(sketch003, length = 20)
|
||||
`
|
||||
|
||||
test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
|
||||
test.fixme('Check the happy path, for basic changing color', () => {
|
||||
test.describe('Check the happy path, for basic changing color', () => {
|
||||
const cases = [
|
||||
{
|
||||
desc: 'User accepts change',
|
||||
@ -70,7 +70,7 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
|
||||
body1CapCoords.y
|
||||
)
|
||||
const yellow: [number, number, number] = [179, 179, 131]
|
||||
const green: [number, number, number] = [108, 152, 75]
|
||||
const green: [number, number, number] = [128, 194, 88]
|
||||
const notGreen: [number, number, number] = [132, 132, 132]
|
||||
const body2NotGreen: [number, number, number] = [88, 88, 88]
|
||||
const submittingToast = page.getByText(
|
||||
@ -109,7 +109,7 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
|
||||
})
|
||||
|
||||
await test.step('verify initial change', async () => {
|
||||
await scene.expectPixelColor(green, greenCheckCoords, 15)
|
||||
await scene.expectPixelColor(green, greenCheckCoords, 20)
|
||||
await scene.expectPixelColor(body2NotGreen, body2WallCoords, 15)
|
||||
await editor.expectEditor.toContain('appearance(')
|
||||
})
|
||||
@ -142,7 +142,7 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
|
||||
}
|
||||
})
|
||||
|
||||
test(`bad edit prompt`, async ({
|
||||
test('bad edit prompt', async ({
|
||||
context,
|
||||
homePage,
|
||||
cmdBar,
|
||||
@ -195,4 +195,150 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
|
||||
await expect(failToast).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test(`manual code selection rename`, async ({
|
||||
context,
|
||||
homePage,
|
||||
cmdBar,
|
||||
editor,
|
||||
page,
|
||||
scene,
|
||||
}) => {
|
||||
const body1CapCoords = { x: 571, y: 351 }
|
||||
|
||||
await context.addInitScript((file) => {
|
||||
localStorage.setItem('persistCode', file)
|
||||
}, file)
|
||||
await homePage.goToModelingScene()
|
||||
await scene.waitForExecutionDone()
|
||||
|
||||
const submittingToast = page.getByText('Submitting to Text-to-CAD API...')
|
||||
const successToast = page.getByText('Prompt to edit successful')
|
||||
const acceptBtn = page.getByRole('button', { name: 'checkmark Accept' })
|
||||
|
||||
await test.step('wait for scene to load and select code in editor', async () => {
|
||||
// Find and select the text "sketch002" in the editor
|
||||
await editor.selectText('sketch002')
|
||||
|
||||
// Verify the selection was made
|
||||
await editor.expectState({
|
||||
highlightedCode: '',
|
||||
activeLines: ["sketch002 = startSketchOn('XZ')"],
|
||||
diagnostics: [],
|
||||
})
|
||||
})
|
||||
|
||||
await test.step('fire off edit prompt', async () => {
|
||||
await scene.expectPixelColor([134, 134, 134], body1CapCoords, 15)
|
||||
await cmdBar.openCmdBar('promptToEdit')
|
||||
await page
|
||||
.getByTestId('cmd-bar-arg-value')
|
||||
.fill('Please rename to mySketch')
|
||||
await page.waitForTimeout(100)
|
||||
await cmdBar.progressCmdBar()
|
||||
await expect(submittingToast).toBeVisible()
|
||||
await expect(submittingToast).not.toBeVisible({
|
||||
timeout: 2 * 60_000,
|
||||
})
|
||||
await expect(successToast).toBeVisible()
|
||||
})
|
||||
|
||||
await test.step('verify rename change and accept it', async () => {
|
||||
await editor.expectEditor.toContain('mySketch = startSketchOn')
|
||||
await editor.expectEditor.not.toContain('sketch002 = startSketchOn')
|
||||
await editor.expectEditor.toContain(
|
||||
'extrude002 = extrude(mySketch, length = 50)'
|
||||
)
|
||||
|
||||
await acceptBtn.click()
|
||||
await expect(successToast).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test('multiple body selections', async ({
|
||||
context,
|
||||
homePage,
|
||||
cmdBar,
|
||||
editor,
|
||||
page,
|
||||
scene,
|
||||
}) => {
|
||||
const body1CapCoords = { x: 571, y: 351 }
|
||||
const body2WallCoords = { x: 620, y: 152 }
|
||||
const [clickBody1Cap] = scene.makeMouseHelpers(
|
||||
body1CapCoords.x,
|
||||
body1CapCoords.y
|
||||
)
|
||||
const [clickBody2Cap] = scene.makeMouseHelpers(
|
||||
body2WallCoords.x,
|
||||
body2WallCoords.y
|
||||
)
|
||||
const grey: [number, number, number] = [132, 132, 132]
|
||||
|
||||
await context.addInitScript((file) => {
|
||||
localStorage.setItem('persistCode', file)
|
||||
}, file)
|
||||
await homePage.goToModelingScene()
|
||||
await scene.waitForExecutionDone()
|
||||
|
||||
const submittingToast = page.getByText('Submitting to Text-to-CAD API...')
|
||||
const successToast = page.getByText('Prompt to edit successful')
|
||||
const acceptBtn = page.getByRole('button', { name: 'checkmark Accept' })
|
||||
|
||||
await test.step('select multiple bodies and fire prompt', async () => {
|
||||
// Initial color check
|
||||
await scene.expectPixelColor(grey, body1CapCoords, 15)
|
||||
|
||||
// Open command bar first (without selection)
|
||||
await cmdBar.openCmdBar('promptToEdit')
|
||||
|
||||
// Select first body
|
||||
await page.waitForTimeout(100)
|
||||
await clickBody1Cap()
|
||||
|
||||
// Hold shift and select second body
|
||||
await editor.expectState({
|
||||
highlightedCode: '',
|
||||
activeLines: ['|>startProfileAt([-73.64,-42.89],%)'],
|
||||
diagnostics: [],
|
||||
})
|
||||
await page.keyboard.down('Shift')
|
||||
await page.waitForTimeout(100)
|
||||
await clickBody2Cap()
|
||||
await editor.expectState({
|
||||
highlightedCode:
|
||||
'line(end=[121.13,56.63],tag=$seg02)extrude(profile001,length=200)',
|
||||
activeLines: [
|
||||
'|>line(end=[121.13,56.63],tag=$seg02)',
|
||||
'|>startProfileAt([-73.64,-42.89],%)',
|
||||
],
|
||||
diagnostics: [],
|
||||
})
|
||||
await page.keyboard.up('Shift')
|
||||
await page.waitForTimeout(100)
|
||||
await cmdBar.progressCmdBar()
|
||||
|
||||
// Enter prompt and submit
|
||||
await page
|
||||
.getByTestId('cmd-bar-arg-value')
|
||||
.fill('make these neon green please, use #39FF14')
|
||||
await page.waitForTimeout(100)
|
||||
await cmdBar.progressCmdBar()
|
||||
|
||||
// Wait for API response
|
||||
await expect(submittingToast).toBeVisible()
|
||||
await expect(submittingToast).not.toBeVisible({
|
||||
timeout: 2 * 60_000,
|
||||
})
|
||||
await expect(successToast).toBeVisible()
|
||||
})
|
||||
|
||||
await test.step('verify code changed', async () => {
|
||||
await editor.expectEditor.toContain('appearance(')
|
||||
|
||||
// Accept changes
|
||||
await acceptBtn.click()
|
||||
await expect(successToast).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
@ -94,6 +94,8 @@ test.describe('Testing Camera Movement', { tag: ['@skipWin'] }, () => {
|
||||
await bakeInRetries(async () => {
|
||||
await page.mouse.move(700, 200)
|
||||
await page.mouse.down({ button: 'right' })
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
const appLogoBBox = await page.getByTestId('app-logo').boundingBox()
|
||||
expect(appLogoBBox).not.toBeNull()
|
||||
if (!appLogoBBox) throw new Error('app logo not found')
|
||||
@ -101,7 +103,9 @@ test.describe('Testing Camera Movement', { tag: ['@skipWin'] }, () => {
|
||||
appLogoBBox.x + appLogoBBox.width / 2,
|
||||
appLogoBBox.y + appLogoBBox.height / 2
|
||||
)
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.move(600, 303)
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.up({ button: 'right' })
|
||||
}, [4, -10.5, -120])
|
||||
|
||||
|
||||
@ -382,7 +382,7 @@ profile003 = startProfileAt([40.16, -120.48], sketch006)
|
||||
'|> line(end = [0, -pipeLength])'
|
||||
)
|
||||
await u.clearCommandLogs()
|
||||
await page.keyboard.press('Backspace')
|
||||
await page.keyboard.press('Delete')
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
@ -439,7 +439,7 @@ profile003 = startProfileAt([40.16, -120.48], sketch006)
|
||||
'|> startProfileAt([23.24, 136.52], %)'
|
||||
)
|
||||
await u.clearCommandLogs()
|
||||
await page.keyboard.press('Backspace')
|
||||
await page.keyboard.press('Delete')
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
|
||||
await page.waitForTimeout(200)
|
||||
await expect(u.codeLocator).not.toContainText(`sketch005 = startSketchOn({`)
|
||||
@ -453,7 +453,7 @@ profile003 = startProfileAt([40.16, -120.48], sketch006)
|
||||
' |> line(end = [20.91, -28.61])'
|
||||
)
|
||||
await u.clearCommandLogs()
|
||||
await page.keyboard.press('Backspace')
|
||||
await page.keyboard.press('Delete')
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
|
||||
await page.waitForTimeout(200)
|
||||
await expect(u.codeLocator).not.toContainText(codeToBeDeletedSnippet)
|
||||
@ -518,7 +518,7 @@ profile003 = startProfileAt([40.16, -120.48], sketch006)
|
||||
'|> line(end = [170.36, -121.61], tag = $seg01)'
|
||||
)
|
||||
await u.clearCommandLogs()
|
||||
await page.keyboard.press('Backspace')
|
||||
await page.keyboard.press('Delete')
|
||||
|
||||
await expect(page.getByText('Unable to delete selection')).toBeVisible()
|
||||
}
|
||||
@ -775,7 +775,7 @@ profile003 = startProfileAt([40.16, -120.48], sketch006)
|
||||
)
|
||||
`)
|
||||
await expect(
|
||||
page.getByTestId('model-state-indicator-execution-done')
|
||||
page.getByTestId('model-state-indicator-receive-reliable')
|
||||
).toBeVisible()
|
||||
|
||||
await u.openAndClearDebugPanel()
|
||||
|
||||
1
exp
@ -1 +0,0 @@
|
||||
sketch001=startSketchOn('XZ')|>startProfileAt([75.8,317.2],%)//[$startCapTag,$EndCapTag]|>angledLine([0,268.43],%,$rectangleSegmentA001)|>angledLine([segAng(rectangleSegmentA001)-90,217.26],%,$seg01)|>angledLine([segAng(rectangleSegmentA001),-segLen(rectangleSegmentA001)],%,$yo)|>line(endAbsolute=[profileStartX(%),profileStartY(%)],tag=$seg02)|>close()extrude001=extrude(sketch001,length=100)|>chamfer(length=30,tags=[getOppositeEdge(seg01)],tag=$seg03)|>chamfer(length=30,tags=[seg01],tag=$seg04)|>chamfer(length=30,tags=[getNextAdjacentEdge(seg02)],tag=$seg05)|>chamfer(length=30,tags=[getNextAdjacentEdge(yo)],tag=$seg06)sketch004=startSketchOn(extrude001,seg05)profile003=startProfileAt([82.57,322.96],sketch004)|>angledLine([0,11.16],%,$rectangleSegmentA004)|>angledLine([segAng(rectangleSegmentA004)-90,103.07],%)|>angledLine([segAng(rectangleSegmentA004),-segLen(rectangleSegmentA004)],%)|>line(endAbsolute=[profileStartX(%),profileStartY(%)])|>close()sketch003=startSketchOn(extrude001,seg04)profile002=startProfileAt([-209.64,255.28],sketch003)|>angledLine([0,11.56],%,$rectangleSegmentA003)|>angledLine([segAng(rectangleSegmentA003)-90,106.84],%)|>angledLine([segAng(rectangleSegmentA003),-segLen(rectangleSegmentA003)],%)|>line(endAbsolute=[profileStartX(%),profileStartY(%)])|>close()sketch002=startSketchOn(extrude001,seg03)profile001=startProfileAt([205.96,254.59],sketch002)|>angledLine([0,11.39],%,$rectangleSegmentA002)|>angledLine([segAng(rectangleSegmentA002)-90,105.26],%)|>angledLine([segAng(rectangleSegmentA002),-segLen(rectangleSegmentA002)],%)|>line(endAbsolute=[profileStartX(%),profileStartY(%)])|>close()
|
||||
1
got
@ -1 +0,0 @@
|
||||
sketch001=startSketchOn('XZ')|>startProfileAt([75.8,317.2],%)//[$startCapTag,$EndCapTag]|>angledLine([0,268.43],%,$rectangleSegmentA001)|>angledLine([segAng(rectangleSegmentA001)-90,217.26],%,$seg01)|>angledLine([segAng(rectangleSegmentA001),-segLen(rectangleSegmentA001)],%,$yo)|>line(endAbsolute=[profileStartX(%),profileStartY(%)],tag=$seg02)|>close()extrude001=extrude(sketch001,length=100)|>chamfer(length=30,tags=[getOppositeEdge(seg01)],tag=$seg03)|>chamfer(length=30,tags=[seg01],tag=$seg04)|>chamfer(length=30,tags=[getNextAdjacentEdge(seg02)],tag=$seg05)|>chamfer(length=30,tags=[getNextAdjacentEdge(yo)],tag=$seg06)sketch005=startSketchOn(extrude001,seg06)profile004=startProfileAt([-23.43,19.69],sketch005)|>angledLine([0,9.1],%,$rectangleSegmentA005)|>angledLine([segAng(rectangleSegmentA005)-90,84.07],%)|>angledLine([segAng(rectangleSegmentA005),-segLen(rectangleSegmentA005)],%)|>line(endAbsolute=[profileStartX(%),profileStartY(%)])|>close()sketch004=startSketchOn(extrude001,seg05)profile003=startProfileAt([82.57,322.96],sketch004)|>angledLine([0,11.16],%,$rectangleSegmentA004)|>angledLine([segAng(rectangleSegmentA004)-90,103.07],%)|>angledLine([segAng(rectangleSegmentA004),-segLen(rectangleSegmentA004)],%)|>line(endAbsolute=[profileStartX(%),profileStartY(%)])|>close()sketch003=startSketchOn(extrude001,seg04)profile002=startProfileAt([-209.64,255.28],sketch003)|>angledLine([0,11.56],%,$rectangleSegmentA003)|>angledLine([segAng(rectangleSegmentA003)-90,106.84],%)|>angledLine([segAng(rectangleSegmentA003),-segLen(rectangleSegmentA003)],%)|>line(endAbsolute=[profileStartX(%),profileStartY(%)])|>close()sketch002=startSketchOn(extrude001,seg03)profile001=startProfileAt([205.96,254.59],sketch002)|>angledLine([0,11.39],%,$rectangleSegmentA002)|>angledLine([segAng(rectangleSegmentA002)-90,105.26],%)|>angledLine([segAng(rectangleSegmentA002),-segLen(rectangleSegmentA002)],%)|>line(endAbsolute=[profileStartX(%),profileStartY(%)])|>close()
|
||||
@ -91,11 +91,11 @@
|
||||
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages",
|
||||
"fetch:wasm": "./scripts/get-latest-wasm-bundle.sh",
|
||||
"fetch:wasm:windows": "./scripts/get-latest-wasm-bundle.ps1",
|
||||
"fetch:samples": "echo \"Fetching latest KCL samples...\" && curl -o public/kcl-samples-manifest-fallback.json https://raw.githubusercontent.com/KittyCAD/next/manifest.json",
|
||||
"fetch:samples": "echo \"Fetching latest KCL samples...\" && curl -o public/kcl-samples-manifest-fallback.json https://raw.githubusercontent.com/KittyCAD/kcl-samples/next/manifest.json",
|
||||
"build:wasm-dev": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && yarn isomorphic-copy-wasm && yarn fmt",
|
||||
"build:wasm:nocopy": "yarn wasm-prep && cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings",
|
||||
"build:wasm": "yarn build:wasm:nocopy && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt",
|
||||
"build:wasm:windows": "yarn install:wasm-pack:cargo && yarn build:wasm:nocopy && copy src\\wasm-lib\\pkg\\wasm_lib_bg.wasm public && yarn fmt",
|
||||
"build:wasm:windows": "yarn install:wasm-pack:cargo && yarn build:wasm:nocopy && ./scripts/copy-wasm.ps1 && yarn fmt",
|
||||
"remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
|
||||
"wasm-prep": "rimraf src/wasm-lib/pkg && mkdirp src/wasm-lib/pkg && rimraf src/wasm-lib/kcl/bindings",
|
||||
"lint-fix": "eslint --fix --ext .ts --ext .tsx src e2e packages/codemirror-lsp-client/src",
|
||||
|
||||
1
scripts/copy-wasm.ps1
Normal file
@ -0,0 +1 @@
|
||||
copy src\wasm-lib\pkg\wasm_lib_bg.wasm public
|
||||
@ -1,10 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
export VERSION=$(date +'%-y.%-m.%-d')
|
||||
export COMMIT=$(git rev-parse --short HEAD)
|
||||
|
||||
# package.json
|
||||
yarn files:set-version
|
||||
PACKAGE=$(jq '.productName="Zoo Modeling App (Nightly)" | .name="zoo-modeling-app-nightly"' package.json --indent 2)
|
||||
echo "$PACKAGE" > package.json
|
||||
|
||||
@ -14,7 +12,7 @@ yq -i '.appId = "dev.zoo.modeling-app-nightly"' electron-builder.yml
|
||||
yq -i '.nsis.include = "./scripts/installer-nightly.nsh"' electron-builder.yml
|
||||
|
||||
# Release notes
|
||||
echo "Nightly build $VERSION (commit $COMMIT)" > release-notes.md
|
||||
echo "Nightly build (commit $COMMIT)" > release-notes.md
|
||||
|
||||
# icons
|
||||
cp assets/icon-nightly.png assets/icon.png
|
||||
|
||||
@ -24,12 +24,7 @@ import ModelingMachineProvider from 'components/ModelingMachineProvider'
|
||||
import FileMachineProvider from 'components/FileMachineProvider'
|
||||
import { MachineManagerProvider } from 'components/MachineManagerProvider'
|
||||
import { PATHS } from 'lib/paths'
|
||||
import {
|
||||
fileLoader,
|
||||
homeLoader,
|
||||
onboardingRedirectLoader,
|
||||
telemetryLoader,
|
||||
} from 'lib/routeLoaders'
|
||||
import { fileLoader, homeLoader, telemetryLoader } from 'lib/routeLoaders'
|
||||
import LspProvider from 'components/LspProvider'
|
||||
import { KclContextProvider } from 'lang/KclProvider'
|
||||
import { ASK_TO_OPEN_QUERY_PARAM, BROWSER_PROJECT_NAME } from 'lib/constants'
|
||||
@ -113,11 +108,6 @@ const router = createRouter([
|
||||
{
|
||||
id: PATHS.FILE + 'SETTINGS',
|
||||
children: [
|
||||
{
|
||||
loader: onboardingRedirectLoader,
|
||||
index: true,
|
||||
element: <></>,
|
||||
},
|
||||
{
|
||||
path: makeUrlPathRelative(PATHS.SETTINGS),
|
||||
element: <Settings />,
|
||||
|
||||
@ -22,7 +22,7 @@ import {
|
||||
UnreliableSubscription,
|
||||
} from 'lang/std/engineConnection'
|
||||
import { EngineCommand } from 'lang/std/artifactGraph'
|
||||
import { toSync, uuidv4 } from 'lib/utils'
|
||||
import { toSync, uuidv4, getNormalisedCoordinates } from 'lib/utils'
|
||||
import { deg2Rad } from 'lib/utils2d'
|
||||
import { isReducedMotion, roundOff, throttle } from 'lib/utils'
|
||||
import * as TWEEN from '@tweenjs/tween.js'
|
||||
@ -109,6 +109,7 @@ export class CameraControls {
|
||||
interactionGuards: MouseGuard = cameraMouseDragGuards.Zoo
|
||||
isFovAnimationInProgress = false
|
||||
perspectiveFovBeforeOrtho = 45
|
||||
|
||||
// NOTE: Duplicated state across Provider and singleton. Mapped from settingsMachine
|
||||
_setting_allowOrbitInSketchMode = false
|
||||
get isPerspective() {
|
||||
@ -456,11 +457,19 @@ export class CameraControls {
|
||||
if (this.syncDirection === 'engineToClient') {
|
||||
const newCmdId = uuidv4()
|
||||
|
||||
// Nonsense to do anything until the video stream is established.
|
||||
if (!this.engineCommandManager.elVideo) return
|
||||
|
||||
const { x, y } = getNormalisedCoordinates(
|
||||
event,
|
||||
this.engineCommandManager.elVideo,
|
||||
this.engineCommandManager.streamDimensions
|
||||
)
|
||||
this.throttledEngCmd({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'highlight_set_entity',
|
||||
selected_at_window: { x: event.clientX, y: event.clientY },
|
||||
selected_at_window: { x, y },
|
||||
},
|
||||
cmd_id: newCmdId,
|
||||
})
|
||||
|
||||
@ -133,9 +133,11 @@ function DisplayObj({
|
||||
}}
|
||||
onClick={(e) => {
|
||||
const range = topLevelRange(obj?.start || 0, obj.end || 0)
|
||||
const idInfo = codeToIdSelections([
|
||||
{ codeRef: codeRefFromRange(range, kclManager.ast) },
|
||||
])[0]
|
||||
const idInfo = codeToIdSelections(
|
||||
[{ codeRef: codeRefFromRange(range, kclManager.ast) }],
|
||||
engineCommandManager.artifactGraph,
|
||||
engineCommandManager.artifactIndex
|
||||
)[0]
|
||||
const artifact = engineCommandManager.artifactGraph.get(
|
||||
idInfo?.id || ''
|
||||
)
|
||||
|
||||
@ -17,7 +17,9 @@ export const CommandBar = () => {
|
||||
const {
|
||||
context: { selectedCommand, currentArgument, commands },
|
||||
} = commandBarState
|
||||
const isSelectionArgument = currentArgument?.inputType === 'selection'
|
||||
const isSelectionArgument =
|
||||
currentArgument?.inputType === 'selection' ||
|
||||
currentArgument?.inputType === 'selectionMixed'
|
||||
const WrapperComponent = isSelectionArgument ? Popover : Dialog
|
||||
|
||||
// Close the command bar when navigating
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import CommandArgOptionInput from './CommandArgOptionInput'
|
||||
import CommandBarBasicInput from './CommandBarBasicInput'
|
||||
import CommandBarSelectionInput from './CommandBarSelectionInput'
|
||||
import CommandBarSelectionMixedInput from './CommandBarSelectionMixedInput'
|
||||
import { CommandArgument } from 'lib/commandTypes'
|
||||
import CommandBarHeader from './CommandBarHeader'
|
||||
import CommandBarKclInput from './CommandBarKclInput'
|
||||
@ -84,6 +85,14 @@ function ArgumentInput({
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
)
|
||||
case 'selectionMixed':
|
||||
return (
|
||||
<CommandBarSelectionMixedInput
|
||||
arg={arg}
|
||||
stepBack={stepBack}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
)
|
||||
case 'kcl':
|
||||
return (
|
||||
<CommandBarKclInput arg={arg} stepBack={stepBack} onSubmit={onSubmit} />
|
||||
|
||||
@ -124,7 +124,8 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
|
||||
<span className="sr-only">: </span>
|
||||
<span data-testid="header-arg-value">
|
||||
{argValue ? (
|
||||
arg.inputType === 'selection' ? (
|
||||
arg.inputType === 'selection' ||
|
||||
arg.inputType === 'selectionMixed' ? (
|
||||
getSelectionTypeDisplayText(argValue as Selections)
|
||||
) : arg.inputType === 'kcl' ? (
|
||||
roundOff(
|
||||
|
||||
135
src/components/CommandBar/CommandBarSelectionMixedInput.tsx
Normal file
@ -0,0 +1,135 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { CommandArgument } from 'lib/commandTypes'
|
||||
import {
|
||||
Selections,
|
||||
canSubmitSelectionArg,
|
||||
getSelectionCountByType,
|
||||
getSelectionTypeDisplayText,
|
||||
} from 'lib/selections'
|
||||
import { useSelector } from '@xstate/react'
|
||||
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||
|
||||
const selectionSelector = (snapshot: any) => snapshot?.context.selectionRanges
|
||||
|
||||
export default function CommandBarSelectionMixedInput({
|
||||
arg,
|
||||
stepBack,
|
||||
onSubmit,
|
||||
}: {
|
||||
arg: CommandArgument<unknown> & { inputType: 'selectionMixed'; name: string }
|
||||
stepBack: () => void
|
||||
onSubmit: (data: unknown) => void
|
||||
}) {
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const commandBarState = useCommandBarState()
|
||||
const [hasSubmitted, setHasSubmitted] = useState(false)
|
||||
const [hasAutoSkipped, setHasAutoSkipped] = useState(false)
|
||||
const selection: Selections = useSelector(arg.machineActor, selectionSelector)
|
||||
|
||||
const selectionsByType = useMemo(() => {
|
||||
return getSelectionCountByType(selection)
|
||||
}, [selection])
|
||||
|
||||
const canSubmitSelection = useMemo<boolean>(() => {
|
||||
if (!selection) return false
|
||||
const isNonZeroRange = selection.graphSelections.some((sel) => {
|
||||
const range = sel.codeRef.range
|
||||
return range[1] - range[0] !== 0 // Non-zero range is always valid
|
||||
})
|
||||
if (isNonZeroRange) return true
|
||||
return canSubmitSelectionArg(selectionsByType, arg)
|
||||
}, [selectionsByType, selection])
|
||||
|
||||
useEffect(() => {
|
||||
inputRef.current?.focus()
|
||||
}, [selection, inputRef])
|
||||
|
||||
// Only auto-skip on initial mount if we have a valid selection
|
||||
// different from the component CommandBarSelectionInput in the the dependency array
|
||||
// is empty
|
||||
useEffect(() => {
|
||||
if (!hasAutoSkipped && canSubmitSelection && arg.skip) {
|
||||
const argValue = commandBarState.context.argumentsToSubmit[arg.name]
|
||||
if (argValue === undefined) {
|
||||
handleSubmit()
|
||||
setHasAutoSkipped(true)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
function handleChange() {
|
||||
inputRef.current?.focus()
|
||||
}
|
||||
|
||||
function handleSubmit(e?: React.FormEvent<HTMLFormElement>) {
|
||||
e?.preventDefault()
|
||||
|
||||
if (!canSubmitSelection) {
|
||||
setHasSubmitted(true)
|
||||
return
|
||||
}
|
||||
|
||||
onSubmit(selection)
|
||||
}
|
||||
|
||||
const isMixedSelection = arg.inputType === 'selectionMixed'
|
||||
const allowNoSelection = isMixedSelection && arg.allowNoSelection
|
||||
const showSceneSelection =
|
||||
isMixedSelection && arg.selectionSource?.allowSceneSelection
|
||||
|
||||
return (
|
||||
<form id="arg-form" onSubmit={handleSubmit}>
|
||||
<label
|
||||
className={
|
||||
'relative flex flex-col mx-4 my-4 ' +
|
||||
(!hasSubmitted || canSubmitSelection || 'text-destroy-50')
|
||||
}
|
||||
>
|
||||
{canSubmitSelection
|
||||
? 'Select objects in the scene'
|
||||
: 'Select code or objects in the scene'}
|
||||
|
||||
{showSceneSelection && (
|
||||
<div className="scene-selection mt-2">
|
||||
<p className="text-sm text-chalkboard-60">
|
||||
Select objects in the scene
|
||||
</p>
|
||||
{/* Scene selection UI will be handled by the parent component */}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{allowNoSelection && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSubmit(null)}
|
||||
className="mt-2 px-4 py-2 rounded border border-chalkboard-30 text-chalkboard-90 dark:text-chalkboard-10 hover:bg-chalkboard-10 dark:hover:bg-chalkboard-90 transition-colors"
|
||||
>
|
||||
Continue without selection
|
||||
</button>
|
||||
)}
|
||||
|
||||
<span data-testid="cmd-bar-arg-name" className="sr-only">
|
||||
{arg.name}
|
||||
</span>
|
||||
<input
|
||||
id="selection"
|
||||
name="selection"
|
||||
ref={inputRef}
|
||||
required
|
||||
data-testid="cmd-bar-arg-value"
|
||||
placeholder="Select an entity with your mouse"
|
||||
className="absolute inset-0 w-full h-full opacity-0 cursor-default"
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Backspace') {
|
||||
stepBack()
|
||||
} else if (event.key === 'Escape') {
|
||||
commandBarActor.send({ type: 'Close' })
|
||||
}
|
||||
}}
|
||||
onChange={handleChange}
|
||||
value={JSON.stringify(selection || {})}
|
||||
/>
|
||||
</label>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@ -130,6 +130,8 @@ export const FileMachineProvider = ({
|
||||
navigateToFile: ({ context, event }) => {
|
||||
if (event.type !== 'xstate.done.actor.create-and-open-file') return
|
||||
if (event.output && 'name' in event.output) {
|
||||
// TODO: Technically this is not the same as the FileTree Onclick even if they are in the same page
|
||||
// What is "Open file?"
|
||||
commandBarActor.send({ type: 'Close' })
|
||||
navigate(
|
||||
`..${PATHS.FILE}/${encodeURIComponent(
|
||||
|
||||
@ -23,6 +23,8 @@ import { FileEntry } from 'lib/project'
|
||||
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
||||
import { normalizeLineEndings } from 'lib/codeEditor'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { useKclContext } from 'lang/KclProvider'
|
||||
import { kclErrorsByFilename, KCLError } from 'lang/errors'
|
||||
|
||||
function getIndentationCSS(level: number) {
|
||||
return `calc(1rem * ${level + 1})`
|
||||
@ -158,6 +160,7 @@ const FileTreeItem = ({
|
||||
level = 0,
|
||||
treeSelection,
|
||||
setTreeSelection,
|
||||
runtimeErrors,
|
||||
}: {
|
||||
parentDir: FileEntry | undefined
|
||||
project?: IndexLoaderData['project']
|
||||
@ -177,6 +180,7 @@ const FileTreeItem = ({
|
||||
level?: number
|
||||
treeSelection: FileEntry | undefined
|
||||
setTreeSelection: Dispatch<React.SetStateAction<FileEntry | undefined>>
|
||||
runtimeErrors: Map<string, KCLError[]>
|
||||
}) => {
|
||||
const { send: fileSend, context: fileContext } = useFileContext()
|
||||
const { onFileOpen, onFileClose } = useLspContext()
|
||||
@ -186,6 +190,8 @@ const FileTreeItem = ({
|
||||
const isFileOrDirHighlighted = treeSelection?.path === fileOrDir?.path
|
||||
const itemRef = useRef(null)
|
||||
|
||||
const hasRuntimeError = runtimeErrors.has(fileOrDir.path)
|
||||
|
||||
// Since every file or directory gets its own FileTreeItem, we can do this.
|
||||
// Because subtrees only render when they are opened, that means this
|
||||
// only listens when they open. Because this acts like a useEffect, when
|
||||
@ -292,7 +298,7 @@ const FileTreeItem = ({
|
||||
>
|
||||
{!isRenaming ? (
|
||||
<button
|
||||
className="flex gap-1 items-center py-0.5 rounded-none border-none p-0 m-0 text-sm w-full hover:!bg-transparent text-left !text-inherit"
|
||||
className="relative flex gap-1 items-center py-0.5 rounded-none border-none p-0 m-0 text-sm w-full hover:!bg-transparent text-left !text-inherit"
|
||||
style={{ paddingInlineStart: getIndentationCSS(level) }}
|
||||
onClick={(e) => {
|
||||
e.currentTarget.focus()
|
||||
@ -300,11 +306,21 @@ const FileTreeItem = ({
|
||||
}}
|
||||
onKeyUp={handleKeyUp}
|
||||
>
|
||||
{hasRuntimeError && (
|
||||
<p
|
||||
className={
|
||||
'absolute m-0 p-0 bottom-3 left-6 w-3 h-3 flex items-center justify-center text-[9px] font-semibold text-white bg-primary hue-rotate-90 rounded-full border border-chalkboard-10 dark:border-chalkboard-80 z-50 hover:cursor-pointer hover:scale-[2] transition-transform duration-200'
|
||||
}
|
||||
title={`Click to view notifications`}
|
||||
>
|
||||
<span>x</span>
|
||||
</p>
|
||||
)}
|
||||
<CustomIcon
|
||||
name={fileOrDir.name?.endsWith(FILE_EXT) ? 'kcl' : 'file'}
|
||||
className="inline-block w-3 text-current"
|
||||
/>
|
||||
{fileOrDir.name}
|
||||
<span className="pl-1">{fileOrDir.name}</span>
|
||||
</button>
|
||||
) : (
|
||||
<RenameForm
|
||||
@ -414,6 +430,7 @@ const FileTreeItem = ({
|
||||
key={level + '-' + child.path}
|
||||
treeSelection={treeSelection}
|
||||
setTreeSelection={setTreeSelection}
|
||||
runtimeErrors={runtimeErrors}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
@ -660,6 +677,8 @@ export const FileTreeInner = ({
|
||||
const loaderData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
||||
const { send: fileSend, context: fileContext } = useFileContext()
|
||||
const { send: modelingSend } = useModelingContext()
|
||||
const { errors } = useKclContext()
|
||||
const runtimeErrors = kclErrorsByFilename(errors)
|
||||
|
||||
const [lastDirectoryClicked, setLastDirectoryClicked] = useState<
|
||||
FileEntry | undefined
|
||||
@ -769,6 +788,7 @@ export const FileTreeInner = ({
|
||||
key={fileOrDir.path}
|
||||
treeSelection={treeSelection}
|
||||
setTreeSelection={setTreeSelection}
|
||||
runtimeErrors={runtimeErrors}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
||||
@ -110,6 +110,7 @@ import { commandBarActor } from 'machines/commandBarMachine'
|
||||
import { useToken } from 'machines/appMachine'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
import { useSettings } from 'machines/appMachine'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
@ -802,7 +803,7 @@ export const ModelingMachineProvider = ({
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
if (err(plane)) return Promise.reject(plane)
|
||||
// if the user selected a segment, make sure we enter the right sketch as there can be multiple on a plan
|
||||
// if the user selected a segment, make sure we enter the right sketch as there can be multiple on a plane
|
||||
// but still works if the user selected a plane/face by defaulting to the first path
|
||||
const mainPath =
|
||||
artifact?.type === 'segment' || artifact?.type === 'solid2d'
|
||||
@ -1717,8 +1718,12 @@ export const ModelingMachineProvider = ({
|
||||
previousAllowOrbitInSketchMode.current = allowOrbitInSketchMode.current
|
||||
}, [allowOrbitInSketchMode])
|
||||
|
||||
// Allow using the delete key to delete solids
|
||||
useHotkeys(['backspace', 'delete', 'del'], () => {
|
||||
// Allow using the delete key to delete solids. Backspace only on macOS as Windows and Linux have dedicated Delete
|
||||
const deleteKeys =
|
||||
isDesktop() && window.electron.os.isMac
|
||||
? ['backspace', 'delete', 'del']
|
||||
: ['delete', 'del']
|
||||
useHotkeys(deleteKeys, () => {
|
||||
modelingSend({ type: 'Delete selection' })
|
||||
})
|
||||
|
||||
|
||||
@ -18,6 +18,7 @@ import { editorManager } from 'lib/singletons'
|
||||
import { ContextFrom } from 'xstate'
|
||||
import { settingsMachine } from 'machines/settingsMachine'
|
||||
import { FeatureTreePane } from './FeatureTreePane'
|
||||
import { kclErrorsByFilename } from 'lang/errors'
|
||||
|
||||
export type SidebarType =
|
||||
| 'code'
|
||||
@ -30,8 +31,10 @@ export type SidebarType =
|
||||
| 'variables'
|
||||
|
||||
export interface BadgeInfo {
|
||||
value: (props: PaneCallbackProps) => boolean | number
|
||||
value: (props: PaneCallbackProps) => boolean | number | string
|
||||
onClick?: MouseEventHandler<any>
|
||||
className?: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@ -152,6 +155,25 @@ export const sidebarPanes: SidebarPane[] = [
|
||||
},
|
||||
keybinding: 'Shift + F',
|
||||
hide: ({ platform }) => platform === 'web',
|
||||
showBadge: {
|
||||
value: (context) => {
|
||||
// Only compute runtime errors! Compilation errors are not tracked here.
|
||||
const errors = kclErrorsByFilename(context.kclContext.errors)
|
||||
return errors.size > 0 ? 'x' : ''
|
||||
},
|
||||
onClick: (e) => {
|
||||
e.preventDefault()
|
||||
// TODO: When we have generic file open
|
||||
// If badge is pressed
|
||||
// Open the first error in the array of errors
|
||||
// Then scroll to error
|
||||
// Do you automatically open the project files
|
||||
// editorManager.scrollToFirstErrorDiagnosticIfExists()
|
||||
},
|
||||
className:
|
||||
'absolute m-0 p-0 bottom-4 left-4 w-3 h-3 flex items-center justify-center text-[9px] font-semibold text-white bg-primary hue-rotate-90 rounded-full border border-chalkboard-10 dark:border-chalkboard-80 z-50 hover:cursor-pointer hover:scale-[2] transition-transform duration-200',
|
||||
title: 'Project files have runtime errors',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'variables',
|
||||
|
||||
@ -27,8 +27,10 @@ interface ModelingSidebarProps {
|
||||
}
|
||||
|
||||
interface BadgeInfoComputed {
|
||||
value: number | boolean
|
||||
value: number | boolean | string
|
||||
onClick?: MouseEventHandler<any>
|
||||
className?: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
function getPlatformString(): 'web' | 'desktop' {
|
||||
@ -116,6 +118,8 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
||||
acc[pane.id] = {
|
||||
value: pane.showBadge.value(paneCallbackProps),
|
||||
onClick: pane.showBadge.onClick,
|
||||
className: pane.showBadge.className,
|
||||
title: pane.showBadge.title,
|
||||
}
|
||||
}
|
||||
return acc
|
||||
@ -125,6 +129,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
||||
// Clear any hidden panes from the `openPanes` array
|
||||
useEffect(() => {
|
||||
const panesToReset: SidebarType[] = []
|
||||
|
||||
sidebarPanes.forEach((pane) => {
|
||||
if (
|
||||
pane.hide === true ||
|
||||
@ -339,22 +344,31 @@ function ModelingPaneButton({
|
||||
<p
|
||||
id={`${paneConfig.id}-badge`}
|
||||
className={
|
||||
'absolute m-0 p-0 bottom-4 left-4 w-3 h-3 flex items-center justify-center text-[10px] font-semibold text-white bg-primary hue-rotate-90 rounded-full border border-chalkboard-10 dark:border-chalkboard-80 z-50 hover:cursor-pointer hover:scale-[2] transition-transform duration-200'
|
||||
showBadge.className
|
||||
? showBadge.className
|
||||
: 'absolute m-0 p-0 bottom-4 left-4 w-3 h-3 flex items-center justify-center text-[10px] font-semibold text-white bg-primary hue-rotate-90 rounded-full border border-chalkboard-10 dark:border-chalkboard-80 z-50 hover:cursor-pointer hover:scale-[2] transition-transform duration-200'
|
||||
}
|
||||
onClick={showBadge.onClick}
|
||||
title={`Click to view ${showBadge.value} notification${
|
||||
Number(showBadge.value) > 1 ? 's' : ''
|
||||
}`}
|
||||
title={
|
||||
showBadge.title
|
||||
? showBadge.title
|
||||
: `Click to view ${showBadge.value} notification${
|
||||
Number(showBadge.value) > 1 ? 's' : ''
|
||||
}`
|
||||
}
|
||||
>
|
||||
<span className="sr-only"> has </span>
|
||||
{typeof showBadge.value === 'number' ? (
|
||||
{typeof showBadge.value === 'number' ||
|
||||
typeof showBadge.value === 'string' ? (
|
||||
<span>{showBadge.value}</span>
|
||||
) : (
|
||||
<span className="sr-only">a</span>
|
||||
)}
|
||||
<span className="sr-only">
|
||||
notification{Number(showBadge.value) > 1 ? 's' : ''}
|
||||
</span>
|
||||
{typeof showBadge.value === 'number' && (
|
||||
<span className="sr-only">
|
||||
notification{Number(showBadge.value) > 1 ? 's' : ''}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -4,11 +4,12 @@ import {
|
||||
useLocation,
|
||||
useNavigate,
|
||||
useRouteLoaderData,
|
||||
redirect,
|
||||
} from 'react-router-dom'
|
||||
import { PATHS } from 'lib/paths'
|
||||
import { markOnce } from 'lib/performance'
|
||||
import { useAuthNavigation } from 'hooks/useAuthNavigation'
|
||||
import { useAuthState } from 'machines/appMachine'
|
||||
import { useAuthState, useSettings } from 'machines/appMachine'
|
||||
import { IndexLoaderData } from 'lib/types'
|
||||
import { getAppSettingsFilePath } from 'lib/desktop'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
@ -16,6 +17,9 @@ import { trap } from 'lib/trap'
|
||||
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
||||
import { loadAndValidateSettings } from 'lib/settings/settingsUtils'
|
||||
import { settingsActor } from 'machines/appMachine'
|
||||
import makeUrlPathRelative from 'lib/makeUrlPathRelative'
|
||||
import { OnboardingStatus } from 'wasm-lib/kcl/bindings/OnboardingStatus'
|
||||
import { SnapshotFrom } from 'xstate'
|
||||
|
||||
export const RouteProviderContext = createContext({})
|
||||
|
||||
@ -29,6 +33,7 @@ export function RouteProvider({ children }: { children: ReactNode }) {
|
||||
const navigation = useNavigation()
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const settings = useSettings()
|
||||
|
||||
const authState = useAuthState()
|
||||
useEffect(() => {
|
||||
@ -43,6 +48,32 @@ export function RouteProvider({ children }: { children: ReactNode }) {
|
||||
markOnce('code/willLoadHome')
|
||||
} else if (isFile) {
|
||||
markOnce('code/willLoadFile')
|
||||
|
||||
/**
|
||||
* TODO: Move to XState. This block has been moved from routerLoaders
|
||||
* and is borrowing the `isFile` logic from the rest of this
|
||||
* telemetry-focused `useEffect`. Once `appMachine` knows about
|
||||
* the current route and navigation, this can be moved into settingsMachine
|
||||
* to fire as soon as the user settings have been read.
|
||||
*/
|
||||
const onboardingStatus: OnboardingStatus =
|
||||
settings.app.onboardingStatus.current || ''
|
||||
// '' is the initial state, 'completed' and 'dismissed' are the final states
|
||||
const needsToOnboard =
|
||||
onboardingStatus.length === 0 ||
|
||||
!(onboardingStatus === 'completed' || onboardingStatus === 'dismissed')
|
||||
const shouldRedirectToOnboarding = isFile && needsToOnboard
|
||||
|
||||
if (
|
||||
shouldRedirectToOnboarding &&
|
||||
settingsActor.getSnapshot().matches('idle')
|
||||
) {
|
||||
navigate(
|
||||
(first ? location.pathname : navigation.location?.pathname) +
|
||||
PATHS.ONBOARDING.INDEX +
|
||||
onboardingStatus.slice(1)
|
||||
)
|
||||
}
|
||||
}
|
||||
setFirstState(false)
|
||||
}, [navigation])
|
||||
|
||||
@ -47,6 +47,8 @@ export const Stream = () => {
|
||||
overallState === NetworkHealthState.Ok ||
|
||||
overallState === NetworkHealthState.Weak
|
||||
|
||||
engineCommandManager.elVideo = videoRef.current
|
||||
|
||||
/**
|
||||
* Execute code and show a "building scene message"
|
||||
* in Stream.tsx in the meantime.
|
||||
@ -272,7 +274,7 @@ export const Stream = () => {
|
||||
|
||||
if (btnName(e.nativeEvent).left) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
sendSelectEventToEngine(e, videoRef.current)
|
||||
sendSelectEventToEngine(e)
|
||||
}
|
||||
}
|
||||
|
||||
@ -294,7 +296,7 @@ export const Stream = () => {
|
||||
return
|
||||
}
|
||||
|
||||
sendSelectEventToEngine(e, videoRef.current)
|
||||
sendSelectEventToEngine(e)
|
||||
.then(({ entity_id }) => {
|
||||
if (!entity_id) {
|
||||
// No entity selected. This is benign
|
||||
|
||||
@ -374,6 +374,7 @@ export default class EditorManager {
|
||||
selectionRanges: this._selectionRanges,
|
||||
isShiftDown: this._isShiftDown,
|
||||
ast: kclManager.ast,
|
||||
artifactGraph: engineCommandManager.artifactGraph,
|
||||
})
|
||||
|
||||
if (!eventInfo) {
|
||||
|
||||
@ -101,10 +101,7 @@ export function useSetupEngineManager(
|
||||
streamRef?.current?.offsetWidth ?? 0,
|
||||
streamRef?.current?.offsetHeight ?? 0
|
||||
)
|
||||
engineCommandManager.handleResize({
|
||||
streamWidth: width,
|
||||
streamHeight: height,
|
||||
})
|
||||
engineCommandManager.handleResize(engineCommandManager.streamDimensions)
|
||||
}, 500)
|
||||
|
||||
const onOnline = () => {
|
||||
|
||||
@ -293,6 +293,13 @@ export class KclManager {
|
||||
return null
|
||||
}
|
||||
|
||||
// GOTCHA:
|
||||
// When we safeParse this is tied to execution because they clicked a new file to load
|
||||
// Clear all previous errors and logs because they are old since they executed a new file
|
||||
// If we decouple safeParse from execution we need to move this application logic.
|
||||
this._kclErrorsCallBack([])
|
||||
this._logsCallBack([])
|
||||
|
||||
this.addDiagnostics(complilationErrorsToDiagnostics(result.errors))
|
||||
this.addDiagnostics(complilationErrorsToDiagnostics(result.warnings))
|
||||
if (result.errors.length > 0) {
|
||||
|
||||
@ -13,6 +13,7 @@ describe('test kclErrToDiagnostic', () => {
|
||||
operations: [],
|
||||
artifactCommands: [],
|
||||
artifactGraph: defaultArtifactGraph(),
|
||||
filenames: {},
|
||||
},
|
||||
{
|
||||
name: '',
|
||||
@ -23,6 +24,7 @@ describe('test kclErrToDiagnostic', () => {
|
||||
operations: [],
|
||||
artifactCommands: [],
|
||||
artifactGraph: defaultArtifactGraph(),
|
||||
filenames: {},
|
||||
},
|
||||
]
|
||||
const diagnostics = kclErrorsToDiagnostics(errors)
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
|
||||
import {
|
||||
KclError,
|
||||
KclError as RustKclError,
|
||||
} from '../wasm-lib/kcl/bindings/KclError'
|
||||
import { CompilationError } from 'wasm-lib/kcl/bindings/CompilationError'
|
||||
import { Diagnostic as CodeMirrorDiagnostic } from '@codemirror/lint'
|
||||
import { posToOffset } from '@kittycad/codemirror-lsp-client'
|
||||
@ -13,6 +16,7 @@ import {
|
||||
SourceRange,
|
||||
} from 'lang/wasm'
|
||||
import { Operation } from 'wasm-lib/kcl/bindings/Operation'
|
||||
import { ModulePath } from 'wasm-lib/kcl/bindings/ModulePath'
|
||||
|
||||
type ExtractKind<T> = T extends { kind: infer K } ? K : never
|
||||
export class KCLError extends Error {
|
||||
@ -22,6 +26,7 @@ export class KCLError extends Error {
|
||||
operations: Operation[]
|
||||
artifactCommands: ArtifactCommand[]
|
||||
artifactGraph: ArtifactGraph
|
||||
filenames: { [x: number]: ModulePath | undefined }
|
||||
|
||||
constructor(
|
||||
kind: ExtractKind<RustKclError> | 'name',
|
||||
@ -29,7 +34,8 @@ export class KCLError extends Error {
|
||||
sourceRange: SourceRange,
|
||||
operations: Operation[],
|
||||
artifactCommands: ArtifactCommand[],
|
||||
artifactGraph: ArtifactGraph
|
||||
artifactGraph: ArtifactGraph,
|
||||
filenames: { [x: number]: ModulePath | undefined }
|
||||
) {
|
||||
super()
|
||||
this.kind = kind
|
||||
@ -38,6 +44,7 @@ export class KCLError extends Error {
|
||||
this.operations = operations
|
||||
this.artifactCommands = artifactCommands
|
||||
this.artifactGraph = artifactGraph
|
||||
this.filenames = filenames
|
||||
Object.setPrototypeOf(this, KCLError.prototype)
|
||||
}
|
||||
}
|
||||
@ -48,7 +55,8 @@ export class KCLLexicalError extends KCLError {
|
||||
sourceRange: SourceRange,
|
||||
operations: Operation[],
|
||||
artifactCommands: ArtifactCommand[],
|
||||
artifactGraph: ArtifactGraph
|
||||
artifactGraph: ArtifactGraph,
|
||||
filenames: { [x: number]: ModulePath | undefined }
|
||||
) {
|
||||
super(
|
||||
'lexical',
|
||||
@ -56,7 +64,8 @@ export class KCLLexicalError extends KCLError {
|
||||
sourceRange,
|
||||
operations,
|
||||
artifactCommands,
|
||||
artifactGraph
|
||||
artifactGraph,
|
||||
filenames
|
||||
)
|
||||
Object.setPrototypeOf(this, KCLSyntaxError.prototype)
|
||||
}
|
||||
@ -68,7 +77,8 @@ export class KCLInternalError extends KCLError {
|
||||
sourceRange: SourceRange,
|
||||
operations: Operation[],
|
||||
artifactCommands: ArtifactCommand[],
|
||||
artifactGraph: ArtifactGraph
|
||||
artifactGraph: ArtifactGraph,
|
||||
filenames: { [x: number]: ModulePath | undefined }
|
||||
) {
|
||||
super(
|
||||
'internal',
|
||||
@ -76,7 +86,8 @@ export class KCLInternalError extends KCLError {
|
||||
sourceRange,
|
||||
operations,
|
||||
artifactCommands,
|
||||
artifactGraph
|
||||
artifactGraph,
|
||||
filenames
|
||||
)
|
||||
Object.setPrototypeOf(this, KCLSyntaxError.prototype)
|
||||
}
|
||||
@ -88,7 +99,8 @@ export class KCLSyntaxError extends KCLError {
|
||||
sourceRange: SourceRange,
|
||||
operations: Operation[],
|
||||
artifactCommands: ArtifactCommand[],
|
||||
artifactGraph: ArtifactGraph
|
||||
artifactGraph: ArtifactGraph,
|
||||
filenames: { [x: number]: ModulePath | undefined }
|
||||
) {
|
||||
super(
|
||||
'syntax',
|
||||
@ -96,7 +108,8 @@ export class KCLSyntaxError extends KCLError {
|
||||
sourceRange,
|
||||
operations,
|
||||
artifactCommands,
|
||||
artifactGraph
|
||||
artifactGraph,
|
||||
filenames
|
||||
)
|
||||
Object.setPrototypeOf(this, KCLSyntaxError.prototype)
|
||||
}
|
||||
@ -108,7 +121,8 @@ export class KCLSemanticError extends KCLError {
|
||||
sourceRange: SourceRange,
|
||||
operations: Operation[],
|
||||
artifactCommands: ArtifactCommand[],
|
||||
artifactGraph: ArtifactGraph
|
||||
artifactGraph: ArtifactGraph,
|
||||
filenames: { [x: number]: ModulePath | undefined }
|
||||
) {
|
||||
super(
|
||||
'semantic',
|
||||
@ -116,7 +130,8 @@ export class KCLSemanticError extends KCLError {
|
||||
sourceRange,
|
||||
operations,
|
||||
artifactCommands,
|
||||
artifactGraph
|
||||
artifactGraph,
|
||||
filenames
|
||||
)
|
||||
Object.setPrototypeOf(this, KCLSemanticError.prototype)
|
||||
}
|
||||
@ -128,9 +143,18 @@ export class KCLTypeError extends KCLError {
|
||||
sourceRange: SourceRange,
|
||||
operations: Operation[],
|
||||
artifactCommands: ArtifactCommand[],
|
||||
artifactGraph: ArtifactGraph
|
||||
artifactGraph: ArtifactGraph,
|
||||
filenames: { [x: number]: ModulePath | undefined }
|
||||
) {
|
||||
super('type', msg, sourceRange, operations, artifactCommands, artifactGraph)
|
||||
super(
|
||||
'type',
|
||||
msg,
|
||||
sourceRange,
|
||||
operations,
|
||||
artifactCommands,
|
||||
artifactGraph,
|
||||
filenames
|
||||
)
|
||||
Object.setPrototypeOf(this, KCLTypeError.prototype)
|
||||
}
|
||||
}
|
||||
@ -141,7 +165,8 @@ export class KCLUnimplementedError extends KCLError {
|
||||
sourceRange: SourceRange,
|
||||
operations: Operation[],
|
||||
artifactCommands: ArtifactCommand[],
|
||||
artifactGraph: ArtifactGraph
|
||||
artifactGraph: ArtifactGraph,
|
||||
filenames: { [x: number]: ModulePath | undefined }
|
||||
) {
|
||||
super(
|
||||
'unimplemented',
|
||||
@ -149,7 +174,8 @@ export class KCLUnimplementedError extends KCLError {
|
||||
sourceRange,
|
||||
operations,
|
||||
artifactCommands,
|
||||
artifactGraph
|
||||
artifactGraph,
|
||||
filenames
|
||||
)
|
||||
Object.setPrototypeOf(this, KCLUnimplementedError.prototype)
|
||||
}
|
||||
@ -161,7 +187,8 @@ export class KCLUnexpectedError extends KCLError {
|
||||
sourceRange: SourceRange,
|
||||
operations: Operation[],
|
||||
artifactCommands: ArtifactCommand[],
|
||||
artifactGraph: ArtifactGraph
|
||||
artifactGraph: ArtifactGraph,
|
||||
filenames: { [x: number]: ModulePath | undefined }
|
||||
) {
|
||||
super(
|
||||
'unexpected',
|
||||
@ -169,7 +196,8 @@ export class KCLUnexpectedError extends KCLError {
|
||||
sourceRange,
|
||||
operations,
|
||||
artifactCommands,
|
||||
artifactGraph
|
||||
artifactGraph,
|
||||
filenames
|
||||
)
|
||||
Object.setPrototypeOf(this, KCLUnexpectedError.prototype)
|
||||
}
|
||||
@ -181,7 +209,8 @@ export class KCLValueAlreadyDefined extends KCLError {
|
||||
sourceRange: SourceRange,
|
||||
operations: Operation[],
|
||||
artifactCommands: ArtifactCommand[],
|
||||
artifactGraph: ArtifactGraph
|
||||
artifactGraph: ArtifactGraph,
|
||||
filenames: { [x: number]: ModulePath | undefined }
|
||||
) {
|
||||
super(
|
||||
'name',
|
||||
@ -189,7 +218,8 @@ export class KCLValueAlreadyDefined extends KCLError {
|
||||
sourceRange,
|
||||
operations,
|
||||
artifactCommands,
|
||||
artifactGraph
|
||||
artifactGraph,
|
||||
filenames
|
||||
)
|
||||
Object.setPrototypeOf(this, KCLValueAlreadyDefined.prototype)
|
||||
}
|
||||
@ -201,7 +231,8 @@ export class KCLUndefinedValueError extends KCLError {
|
||||
sourceRange: SourceRange,
|
||||
operations: Operation[],
|
||||
artifactCommands: ArtifactCommand[],
|
||||
artifactGraph: ArtifactGraph
|
||||
artifactGraph: ArtifactGraph,
|
||||
filenames: { [x: number]: ModulePath | undefined }
|
||||
) {
|
||||
super(
|
||||
'name',
|
||||
@ -209,7 +240,8 @@ export class KCLUndefinedValueError extends KCLError {
|
||||
sourceRange,
|
||||
operations,
|
||||
artifactCommands,
|
||||
artifactGraph
|
||||
artifactGraph,
|
||||
filenames
|
||||
)
|
||||
Object.setPrototypeOf(this, KCLUndefinedValueError.prototype)
|
||||
}
|
||||
@ -232,7 +264,8 @@ export function lspDiagnosticsToKclErrors(
|
||||
[posToOffset(doc, range.start)!, posToOffset(doc, range.end)!, 0],
|
||||
[],
|
||||
[],
|
||||
defaultArtifactGraph()
|
||||
defaultArtifactGraph(),
|
||||
{}
|
||||
)
|
||||
)
|
||||
.sort((a, b) => {
|
||||
@ -304,3 +337,34 @@ export function complilationErrorsToDiagnostics(
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Create an array of KCL Errors with a new formatting to
|
||||
// easily map SourceRange of an error to the filename to display in the
|
||||
// side bar UI. This is to indicate an error in an imported file, it isn't
|
||||
// the specific code mirror error interface.
|
||||
export function kclErrorsByFilename(
|
||||
errors: KCLError[]
|
||||
): Map<string, KCLError[]> {
|
||||
const fileNameToError: Map<string, KCLError[]> = new Map()
|
||||
errors.forEach((error: KCLError) => {
|
||||
const filenames = error.filenames
|
||||
const sourceRange: SourceRange = error.sourceRange
|
||||
const fileIndex = sourceRange[2]
|
||||
const modulePath: ModulePath | undefined = filenames[fileIndex]
|
||||
if (modulePath) {
|
||||
let stdOrLocalPath = modulePath.value
|
||||
if (stdOrLocalPath) {
|
||||
// Build up an array of errors per file name
|
||||
const value = fileNameToError.get(stdOrLocalPath)
|
||||
if (!value) {
|
||||
fileNameToError.set(stdOrLocalPath, [error])
|
||||
} else {
|
||||
value.push(error)
|
||||
fileNameToError.set(stdOrLocalPath, [error])
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return fileNameToError
|
||||
}
|
||||
|
||||
@ -511,7 +511,8 @@ const theExtrude = startSketchOn('XY')
|
||||
topLevelRange(129, 135),
|
||||
[],
|
||||
[],
|
||||
defaultArtifactGraph()
|
||||
defaultArtifactGraph(),
|
||||
{}
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
@ -116,7 +116,11 @@ const runGetPathToExtrudeForSegmentSelectionTest = async (
|
||||
}
|
||||
if (!extrudeInSketchPipe) {
|
||||
const init = expectedExtrudeNode.init
|
||||
if (init.type !== 'CallExpression' && init.type !== 'PipeExpression') {
|
||||
if (
|
||||
init.type !== 'CallExpression' &&
|
||||
init.type !== 'CallExpressionKw' &&
|
||||
init.type !== 'PipeExpression'
|
||||
) {
|
||||
return new Error(
|
||||
'Expected extrude expression is not a CallExpression or PipeExpression'
|
||||
)
|
||||
@ -129,25 +133,33 @@ const runGetPathToExtrudeForSegmentSelectionTest = async (
|
||||
// ast
|
||||
const ast = assertParse(code)
|
||||
|
||||
// selection
|
||||
// range
|
||||
const segmentRange = topLevelRange(
|
||||
code.indexOf(selectedSegmentSnippet),
|
||||
code.indexOf(selectedSegmentSnippet) + selectedSegmentSnippet.length
|
||||
)
|
||||
const selection: Selection = {
|
||||
codeRef: codeRefFromRange(segmentRange, ast),
|
||||
}
|
||||
|
||||
// executeAst and artifactGraph
|
||||
await kclManager.executeAst({ ast })
|
||||
const artifactGraph = engineCommandManager.artifactGraph
|
||||
|
||||
// find artifact
|
||||
const maybeArtifact = [...artifactGraph].find(([, artifact]) => {
|
||||
if (!('codeRef' in artifact && artifact.codeRef)) return false
|
||||
return isOverlap(artifact.codeRef.range, segmentRange)
|
||||
})
|
||||
|
||||
// build selection
|
||||
const selection: Selection = {
|
||||
codeRef: codeRefFromRange(segmentRange, ast),
|
||||
artifact: maybeArtifact ? maybeArtifact[1] : undefined,
|
||||
}
|
||||
|
||||
// get extrude expression
|
||||
const pathResult = getPathToExtrudeForSegmentSelection(
|
||||
ast,
|
||||
selection,
|
||||
artifactGraph,
|
||||
dependencies
|
||||
artifactGraph
|
||||
)
|
||||
if (err(pathResult)) return pathResult
|
||||
const { pathToExtrudeNode } = pathResult
|
||||
@ -234,6 +246,56 @@ extrude003 = extrude(sketch003, length = -15)`
|
||||
expectedExtrudeSnippet
|
||||
)
|
||||
})
|
||||
it('should return the correct paths for a (piped) extrude based on the other body (face)', async () => {
|
||||
const code = `sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-25, -25], %)
|
||||
|> yLine(50, %)
|
||||
|> xLine(50, %)
|
||||
|> yLine(-50, %)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
|> extrude(length = 50)
|
||||
sketch002 = startSketchOn(sketch001, 'END')
|
||||
|> startProfileAt([-15, -15], %)
|
||||
|> yLine(30, %)
|
||||
|> xLine(30, %)
|
||||
|> yLine(-30, %)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
|> extrude(length = 30)`
|
||||
const selectedSegmentSnippet = `xLine(30, %)`
|
||||
const expectedExtrudeSnippet = `extrude(length = 30)`
|
||||
await runGetPathToExtrudeForSegmentSelectionTest(
|
||||
code,
|
||||
selectedSegmentSnippet,
|
||||
expectedExtrudeSnippet
|
||||
)
|
||||
})
|
||||
it('should return the correct paths for a (non-piped) extrude based on the other body (face)', async () => {
|
||||
const code = `sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-25, -25], %)
|
||||
|> yLine(50, %)
|
||||
|> xLine(50, %)
|
||||
|> yLine(-50, %)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
extrude001 = extrude(sketch001, length = 50)
|
||||
sketch002 = startSketchOn(extrude001, 'END')
|
||||
|> startProfileAt([-15, -15], %)
|
||||
|> yLine(30, %)
|
||||
|> xLine(30, %)
|
||||
|> yLine(-30, %)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
extrude002 = extrude(sketch002, length = 30)`
|
||||
const selectedSegmentSnippet = `xLine(30, %)`
|
||||
const expectedExtrudeSnippet = `extrude002 = extrude(sketch002, length = 30)`
|
||||
await runGetPathToExtrudeForSegmentSelectionTest(
|
||||
code,
|
||||
selectedSegmentSnippet,
|
||||
expectedExtrudeSnippet
|
||||
)
|
||||
})
|
||||
it('should not return any path for missing extrusion', async () => {
|
||||
const code = `sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-30, 30], %)
|
||||
|
||||
@ -10,7 +10,6 @@ import {
|
||||
Program,
|
||||
VariableDeclaration,
|
||||
VariableDeclarator,
|
||||
sketchFromKclValue,
|
||||
} from '../wasm'
|
||||
import {
|
||||
createCallExpressionStdLib,
|
||||
@ -35,11 +34,11 @@ import {
|
||||
sketchLineHelperMap,
|
||||
sketchLineHelperMapKw,
|
||||
} from '../std/sketch'
|
||||
import { err, trap } from 'lib/trap'
|
||||
import { err } from 'lib/trap'
|
||||
import { Selection, Selections } from 'lib/selections'
|
||||
import { KclCommandValue } from 'lib/commandTypes'
|
||||
import { isArray } from 'lib/utils'
|
||||
import { Artifact, getSweepFromSuspectedPath } from 'lang/std/artifactGraph'
|
||||
import { Artifact, getSweepArtifactFromSelection } from 'lang/std/artifactGraph'
|
||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
import { findKwArg } from 'lang/util'
|
||||
import { KclManager } from 'lang/KclSingleton'
|
||||
@ -121,8 +120,7 @@ export function modifyAstWithEdgeTreatmentAndTag(
|
||||
const result = getPathToExtrudeForSegmentSelection(
|
||||
clonedAstForGetExtrude,
|
||||
selection,
|
||||
artifactGraph,
|
||||
dependencies
|
||||
artifactGraph
|
||||
)
|
||||
if (err(result)) return result
|
||||
const { pathToSegmentNode, pathToExtrudeNode } = result
|
||||
@ -279,39 +277,19 @@ function insertParametersIntoAst(
|
||||
export function getPathToExtrudeForSegmentSelection(
|
||||
ast: Program,
|
||||
selection: Selection,
|
||||
artifactGraph: ArtifactGraph,
|
||||
dependencies: {
|
||||
kclManager: KclManager
|
||||
engineCommandManager: EngineCommandManager
|
||||
editorManager: EditorManager
|
||||
codeManager: CodeManager
|
||||
}
|
||||
artifactGraph: ArtifactGraph
|
||||
): { pathToSegmentNode: PathToNode; pathToExtrudeNode: PathToNode } | Error {
|
||||
const pathToSegmentNode = getNodePathFromSourceRange(
|
||||
ast,
|
||||
selection.codeRef?.range
|
||||
)
|
||||
|
||||
const varDecNode = getNodeFromPath<VariableDeclaration>(
|
||||
ast,
|
||||
pathToSegmentNode,
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (err(varDecNode)) return varDecNode
|
||||
const sketchVar = varDecNode.node.declaration.id.name
|
||||
|
||||
const sketch = sketchFromKclValue(
|
||||
dependencies.kclManager.variables[sketchVar],
|
||||
sketchVar
|
||||
)
|
||||
if (trap(sketch)) return sketch
|
||||
|
||||
const extrusion = getSweepFromSuspectedPath(sketch.id, artifactGraph)
|
||||
if (err(extrusion)) return extrusion
|
||||
const sweepArtifact = getSweepArtifactFromSelection(selection, artifactGraph)
|
||||
if (err(sweepArtifact)) return sweepArtifact
|
||||
|
||||
const pathToExtrudeNode = getNodePathFromSourceRange(
|
||||
ast,
|
||||
extrusion.codeRef.range
|
||||
sweepArtifact.codeRef.range
|
||||
)
|
||||
if (err(pathToExtrudeNode)) return pathToExtrudeNode
|
||||
|
||||
|
||||
@ -13,36 +13,23 @@ import {
|
||||
createLiteral,
|
||||
createIdentifier,
|
||||
findUniqueName,
|
||||
createCallExpressionStdLib,
|
||||
createObjectExpression,
|
||||
createArrayExpression,
|
||||
createVariableDeclaration,
|
||||
createCallExpressionStdLibKw,
|
||||
createLabeledArg,
|
||||
} from 'lang/modifyAst'
|
||||
import { KCL_DEFAULT_CONSTANT_PREFIXES } from 'lib/constants'
|
||||
import { KclManager } from 'lang/KclSingleton'
|
||||
import { EngineCommandManager } from 'lang/std/engineConnection'
|
||||
import EditorManager from 'editor/manager'
|
||||
import CodeManager from 'lang/codeManager'
|
||||
|
||||
export function addShell({
|
||||
node,
|
||||
selection,
|
||||
artifactGraph,
|
||||
thickness,
|
||||
dependencies,
|
||||
}: {
|
||||
node: Node<Program>
|
||||
selection: Selections
|
||||
artifactGraph: ArtifactGraph
|
||||
thickness: Expr
|
||||
dependencies: {
|
||||
kclManager: KclManager
|
||||
engineCommandManager: EngineCommandManager
|
||||
editorManager: EditorManager
|
||||
codeManager: CodeManager
|
||||
}
|
||||
}): Error | { modifiedAst: Node<Program>; pathToNode: PathToNode } {
|
||||
const modifiedAst = structuredClone(node)
|
||||
|
||||
@ -55,8 +42,7 @@ export function addShell({
|
||||
const extrudeLookupResult = getPathToExtrudeForSegmentSelection(
|
||||
clonedAstForGetExtrude,
|
||||
graphSelection,
|
||||
artifactGraph,
|
||||
dependencies
|
||||
artifactGraph
|
||||
)
|
||||
if (err(extrudeLookupResult)) {
|
||||
return new Error("Couldn't find extrude")
|
||||
|
||||
@ -18,6 +18,7 @@ import {
|
||||
} from 'lang/wasm'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
import { Selection } from 'lib/selections'
|
||||
import { err } from 'lib/trap'
|
||||
import { Cap, Plane, Wall } from 'wasm-lib/kcl/bindings/Artifact'
|
||||
import { CapSubType } from 'wasm-lib/kcl/bindings/Artifact'
|
||||
@ -79,7 +80,7 @@ interface SegmentArtifactRich extends BaseArtifact {
|
||||
|
||||
interface SweepArtifactRich extends BaseArtifact {
|
||||
type: 'sweep'
|
||||
subType: 'extrusion' | 'revolve' | 'loft' | 'sweep'
|
||||
subType: 'extrusion' | 'revolve' | 'revolveAboutEdge' | 'loft' | 'sweep'
|
||||
path: PathArtifact
|
||||
surfaces: Array<WallArtifact | CapArtifact>
|
||||
edges: Array<SweepEdge>
|
||||
@ -455,6 +456,47 @@ export function getSweepFromSuspectedPath(
|
||||
)
|
||||
}
|
||||
|
||||
export function getSweepArtifactFromSelection(
|
||||
selection: Selection,
|
||||
artifactGraph: ArtifactGraph
|
||||
): SweepArtifact | Error {
|
||||
let sweepArtifact: Artifact | null = null
|
||||
if (selection.artifact?.type === 'sweepEdge') {
|
||||
const _artifact = getArtifactOfTypes(
|
||||
{ key: selection.artifact.sweepId, types: ['sweep'] },
|
||||
artifactGraph
|
||||
)
|
||||
if (err(_artifact)) return _artifact
|
||||
sweepArtifact = _artifact
|
||||
} else if (selection.artifact?.type === 'segment') {
|
||||
const _pathArtifact = getArtifactOfTypes(
|
||||
{ key: selection.artifact.pathId, types: ['path'] },
|
||||
artifactGraph
|
||||
)
|
||||
if (err(_pathArtifact)) return _pathArtifact
|
||||
if (!_pathArtifact.sweepId) return new Error('Path does not have a sweepId')
|
||||
const _artifact = getArtifactOfTypes(
|
||||
{ key: _pathArtifact.sweepId, types: ['sweep'] },
|
||||
artifactGraph
|
||||
)
|
||||
if (err(_artifact)) return _artifact
|
||||
sweepArtifact = _artifact
|
||||
} else if (
|
||||
selection.artifact?.type === 'cap' ||
|
||||
selection.artifact?.type === 'wall'
|
||||
) {
|
||||
const _artifact = getArtifactOfTypes(
|
||||
{ key: selection.artifact.sweepId, types: ['sweep'] },
|
||||
artifactGraph
|
||||
)
|
||||
if (err(_artifact)) return _artifact
|
||||
sweepArtifact = _artifact
|
||||
}
|
||||
if (!sweepArtifact) return new Error('No sweep artifact found')
|
||||
|
||||
return sweepArtifact
|
||||
}
|
||||
|
||||
export function getCodeRefsByArtifactId(
|
||||
id: string,
|
||||
artifactGraph: ArtifactGraph
|
||||
|
||||
@ -31,6 +31,8 @@ import { markOnce } from 'lib/performance'
|
||||
import { MachineManager } from 'components/MachineManagerProvider'
|
||||
import { DefaultPlaneStr } from 'lib/planes'
|
||||
import { defaultPlaneStrToKey } from 'lib/planes'
|
||||
import { buildArtifactIndex } from 'lib/artifactIndex'
|
||||
import { ArtifactIndex } from 'lib/artifactIndex'
|
||||
|
||||
// TODO(paultag): This ought to be tweakable.
|
||||
const pingIntervalMs = 5_000
|
||||
@ -1407,6 +1409,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
* see: src/lang/std/artifactGraph-README.md for a full explanation.
|
||||
*/
|
||||
artifactGraph: ArtifactGraph = new Map()
|
||||
artifactIndex: ArtifactIndex = []
|
||||
/**
|
||||
* The pendingCommands object is a map of the commands that have been sent to the engine that are still waiting on a reply
|
||||
*/
|
||||
@ -1444,11 +1447,17 @@ export class EngineCommandManager extends EventTarget {
|
||||
commandId: string
|
||||
}
|
||||
settings: SettingsViaQueryString
|
||||
width: number = 1337
|
||||
height: number = 1337
|
||||
|
||||
streamDimensions = {
|
||||
// Random defaults that are overwritten pretty much immediately
|
||||
width: 1337,
|
||||
height: 1337,
|
||||
}
|
||||
|
||||
elVideo: HTMLVideoElement | null = null
|
||||
|
||||
/**
|
||||
* Export intent traxcks the intent of the export. If it is null there is no
|
||||
* Export intent tracks the intent of the export. If it is null there is no
|
||||
* export in progress. Otherwise it is an enum value of the intent.
|
||||
* Another export cannot be started if one is already in progress.
|
||||
*/
|
||||
@ -1551,15 +1560,14 @@ export class EngineCommandManager extends EventTarget {
|
||||
return
|
||||
}
|
||||
|
||||
this.width = width
|
||||
this.height = height
|
||||
this.streamDimensions = {
|
||||
width,
|
||||
height,
|
||||
}
|
||||
|
||||
// If we already have an engine connection, just need to resize the stream.
|
||||
if (this.engineConnection) {
|
||||
this.handleResize({
|
||||
streamWidth: width,
|
||||
streamHeight: height,
|
||||
})
|
||||
this.handleResize(this.streamDimensions)
|
||||
return
|
||||
}
|
||||
|
||||
@ -1855,27 +1863,22 @@ export class EngineCommandManager extends EventTarget {
|
||||
return
|
||||
}
|
||||
|
||||
handleResize({
|
||||
streamWidth,
|
||||
streamHeight,
|
||||
}: {
|
||||
streamWidth: number
|
||||
streamHeight: number
|
||||
}) {
|
||||
handleResize({ width, height }: { width: number; height: number }) {
|
||||
if (!this.engineConnection?.isReady()) {
|
||||
return
|
||||
}
|
||||
|
||||
this.width = streamWidth
|
||||
this.height = streamHeight
|
||||
this.streamDimensions = {
|
||||
width,
|
||||
height,
|
||||
}
|
||||
|
||||
const resizeCmd: EngineCommand = {
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'reconfigure_stream',
|
||||
width: streamWidth,
|
||||
height: streamHeight,
|
||||
...this.streamDimensions,
|
||||
fps: 60,
|
||||
},
|
||||
}
|
||||
@ -2184,6 +2187,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
}
|
||||
updateArtifactGraph(execStateArtifactGraph: ExecState['artifactGraph']) {
|
||||
this.artifactGraph = execStateArtifactGraph
|
||||
this.artifactIndex = buildArtifactIndex(execStateArtifactGraph)
|
||||
// TODO check if these still need to be deferred once e2e tests are working again.
|
||||
if (this.artifactGraph.size) {
|
||||
this.deferredArtifactEmptied(null)
|
||||
|
||||
@ -60,6 +60,7 @@ import { MetaSettings } from 'wasm-lib/kcl/bindings/MetaSettings'
|
||||
import { UnitAngle, UnitLength } from 'wasm-lib/kcl/bindings/ModelingCmd'
|
||||
import { UnitLen } from 'wasm-lib/kcl/bindings/UnitLen'
|
||||
import { UnitAngle as UnitAng } from 'wasm-lib/kcl/bindings/UnitAngle'
|
||||
import { ModulePath } from 'wasm-lib/kcl/bindings/ModulePath'
|
||||
|
||||
export type { Artifact } from 'wasm-lib/kcl/bindings/Artifact'
|
||||
export type { ArtifactCommand } from 'wasm-lib/kcl/bindings/Artifact'
|
||||
@ -266,7 +267,8 @@ export const parse = (code: string | Error): ParseResult | Error => {
|
||||
firstSourceRange(parsed),
|
||||
[],
|
||||
[],
|
||||
defaultArtifactGraph()
|
||||
defaultArtifactGraph(),
|
||||
{}
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -296,6 +298,7 @@ export interface ExecState {
|
||||
artifactCommands: ArtifactCommand[]
|
||||
artifactGraph: ArtifactGraph
|
||||
errors: CompilationError[]
|
||||
filenames: { [x: number]: ModulePath | undefined }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -310,6 +313,7 @@ export function emptyExecState(): ExecState {
|
||||
artifactCommands: [],
|
||||
artifactGraph: defaultArtifactGraph(),
|
||||
errors: [],
|
||||
filenames: [],
|
||||
}
|
||||
}
|
||||
|
||||
@ -336,6 +340,7 @@ function execStateFromRust(
|
||||
artifactCommands: execOutcome.artifactCommands,
|
||||
artifactGraph,
|
||||
errors: execOutcome.errors,
|
||||
filenames: execOutcome.filenames,
|
||||
}
|
||||
}
|
||||
|
||||
@ -347,6 +352,7 @@ function mockExecStateFromRust(execOutcome: RustExecOutcome): ExecState {
|
||||
artifactCommands: execOutcome.artifactCommands,
|
||||
artifactGraph: new Map<ArtifactId, Artifact>(),
|
||||
errors: execOutcome.errors,
|
||||
filenames: execOutcome.filenames,
|
||||
}
|
||||
}
|
||||
|
||||
@ -474,7 +480,7 @@ const jsAppSettings = async () => {
|
||||
}
|
||||
|
||||
const errFromErrWithOutputs = (e: any): KCLError => {
|
||||
console.log('execute error', e)
|
||||
console.log(e)
|
||||
const parsed: KclErrorWithOutputs = JSON.parse(e.toString())
|
||||
return new KCLError(
|
||||
parsed.error.kind,
|
||||
@ -482,7 +488,8 @@ const errFromErrWithOutputs = (e: any): KCLError => {
|
||||
firstSourceRange(parsed.error),
|
||||
parsed.operations,
|
||||
parsed.artifactCommands,
|
||||
rustArtifactGraphToMap(parsed.artifactGraph)
|
||||
rustArtifactGraphToMap(parsed.artifactGraph),
|
||||
parsed.filenames
|
||||
)
|
||||
}
|
||||
|
||||
@ -548,7 +555,8 @@ export const modifyAstForSketch = async (
|
||||
firstSourceRange(parsed),
|
||||
[],
|
||||
[],
|
||||
defaultArtifactGraph()
|
||||
defaultArtifactGraph(),
|
||||
{}
|
||||
)
|
||||
|
||||
return Promise.reject(kclError)
|
||||
|
||||
29
src/lib/artifactIndex.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { ArtifactGraph, ArtifactId, SourceRange, Artifact } from 'lang/wasm'
|
||||
import { getFaceCodeRef } from 'lang/std/artifactGraph'
|
||||
|
||||
// Index artifacts in an ordered list for binary search
|
||||
export type ArtifactEntry = { artifact: Artifact; id: ArtifactId }
|
||||
/** Index artifacts by their codeRef range, ordered by start position */
|
||||
export type ArtifactIndex = Array<{
|
||||
range: SourceRange
|
||||
entry: ArtifactEntry
|
||||
}>
|
||||
|
||||
/** Creates an array of artifacts, only those with codeRefs, orders them by start range,
|
||||
* to be used later by binary search */
|
||||
export function buildArtifactIndex(
|
||||
artifactGraph: ArtifactGraph
|
||||
): ArtifactIndex {
|
||||
const index: ArtifactIndex = []
|
||||
|
||||
Array.from(artifactGraph).forEach(([id, artifact]) => {
|
||||
const codeRef = getFaceCodeRef(artifact)
|
||||
if (!codeRef?.range) return
|
||||
|
||||
const entry = { artifact, id }
|
||||
index.push({ range: codeRef.range, entry })
|
||||
})
|
||||
|
||||
// Sort by start position for binary search
|
||||
return index.sort((a, b) => a.range[0] - b.range[0])
|
||||
}
|
||||
@ -666,7 +666,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
icon: 'chat',
|
||||
args: {
|
||||
selection: {
|
||||
inputType: 'selection',
|
||||
inputType: 'selectionMixed',
|
||||
selectionTypes: [
|
||||
'solid2d',
|
||||
'segment',
|
||||
@ -678,6 +678,10 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
],
|
||||
multiple: true,
|
||||
required: true,
|
||||
selectionSource: {
|
||||
allowSceneSelection: true,
|
||||
allowCodeSelection: true,
|
||||
},
|
||||
skip: true,
|
||||
},
|
||||
prompt: {
|
||||
|
||||
@ -16,6 +16,7 @@ const INPUT_TYPES = [
|
||||
'text',
|
||||
'kcl',
|
||||
'selection',
|
||||
'selectionMixed',
|
||||
'boolean',
|
||||
] as const
|
||||
export interface KclExpression {
|
||||
@ -156,6 +157,23 @@ export type CommandArgumentConfig<
|
||||
context: CommandBarContext
|
||||
}) => Promise<boolean | string>
|
||||
}
|
||||
| {
|
||||
inputType: 'selectionMixed'
|
||||
selectionTypes: Artifact['type'][]
|
||||
multiple: boolean
|
||||
allowNoSelection?: boolean
|
||||
validation?: ({
|
||||
data,
|
||||
context,
|
||||
}: {
|
||||
data: any
|
||||
context: CommandBarContext
|
||||
}) => Promise<boolean | string>
|
||||
selectionSource?: {
|
||||
allowSceneSelection?: boolean
|
||||
allowCodeSelection?: boolean
|
||||
}
|
||||
}
|
||||
| {
|
||||
inputType: 'kcl'
|
||||
createVariableByDefault?: boolean
|
||||
@ -252,6 +270,23 @@ export type CommandArgument<
|
||||
context: CommandBarContext
|
||||
}) => Promise<boolean | string>
|
||||
}
|
||||
| {
|
||||
inputType: 'selectionMixed'
|
||||
selectionTypes: Artifact['type'][]
|
||||
multiple: boolean
|
||||
allowNoSelection?: boolean
|
||||
validation?: ({
|
||||
data,
|
||||
context,
|
||||
}: {
|
||||
data: any
|
||||
context: CommandBarContext
|
||||
}) => Promise<boolean | string>
|
||||
selectionSource?: {
|
||||
allowSceneSelection?: boolean
|
||||
allowCodeSelection?: boolean
|
||||
}
|
||||
}
|
||||
| {
|
||||
inputType: 'kcl'
|
||||
createVariableByDefault?: boolean
|
||||
|
||||
@ -187,6 +187,16 @@ export function buildCommandArgument<
|
||||
selectionTypes: arg.selectionTypes,
|
||||
validation: arg.validation,
|
||||
} satisfies CommandArgument<O, T> & { inputType: 'selection' }
|
||||
} else if (arg.inputType === 'selectionMixed') {
|
||||
return {
|
||||
inputType: arg.inputType,
|
||||
...baseCommandArgument,
|
||||
multiple: arg.multiple,
|
||||
selectionTypes: arg.selectionTypes,
|
||||
validation: arg.validation,
|
||||
allowNoSelection: arg.allowNoSelection,
|
||||
selectionSource: arg.selectionSource,
|
||||
} satisfies CommandArgument<O, T> & { inputType: 'selectionMixed' }
|
||||
} else if (arg.inputType === 'kcl') {
|
||||
return {
|
||||
inputType: arg.inputType,
|
||||
|
||||
@ -43,15 +43,33 @@ export async function submitPromptToEditToQueue({
|
||||
projectName,
|
||||
}: {
|
||||
prompt: string
|
||||
selections: Selections
|
||||
selections: Selections | null
|
||||
code: string
|
||||
projectName: string
|
||||
token?: string
|
||||
artifactGraph: ArtifactGraph
|
||||
}): Promise<Models['TextToCadIteration_type'] | Error> {
|
||||
// If no selection, use whole file
|
||||
if (selections === null) {
|
||||
const body: Models['TextToCadIterationBody_type'] = {
|
||||
original_source_code: code,
|
||||
prompt,
|
||||
source_ranges: [], // Empty ranges indicates whole file
|
||||
project_name:
|
||||
projectName !== '' && projectName !== 'browser'
|
||||
? projectName
|
||||
: undefined,
|
||||
kcl_version: kclManager.kclVersion,
|
||||
}
|
||||
return submitToApi(body, token)
|
||||
}
|
||||
|
||||
// Handle manual code selections and artifact selections differently
|
||||
const ranges: Models['TextToCadIterationBody_type']['source_ranges'] =
|
||||
selections.graphSelections.flatMap((selection) => {
|
||||
const artifact = selection.artifact
|
||||
|
||||
// For artifact selections, add context
|
||||
const prompts: Models['TextToCadIterationBody_type']['source_ranges'] = []
|
||||
|
||||
if (artifact?.type === 'cap') {
|
||||
@ -153,8 +171,17 @@ See later source ranges for more context. about the sweep`,
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!artifact) {
|
||||
// manually selected code is more likely to not have an artifact
|
||||
// an example might be highlighting the variable name only in a variable declaration
|
||||
prompts.push({
|
||||
prompt: '',
|
||||
range: convertAppRangeToApiRange(selection.codeRef.range, code),
|
||||
})
|
||||
}
|
||||
return prompts
|
||||
})
|
||||
|
||||
const body: Models['TextToCadIterationBody_type'] = {
|
||||
original_source_code: code,
|
||||
prompt,
|
||||
@ -163,6 +190,15 @@ See later source ranges for more context. about the sweep`,
|
||||
projectName !== '' && projectName !== 'browser' ? projectName : undefined,
|
||||
kcl_version: kclManager.kclVersion,
|
||||
}
|
||||
|
||||
return submitToApi(body, token)
|
||||
}
|
||||
|
||||
// Helper function to handle API submission
|
||||
async function submitToApi(
|
||||
body: Models['TextToCadIterationBody_type'],
|
||||
token?: string
|
||||
): Promise<Models['TextToCadIteration_type'] | Error> {
|
||||
const url = VITE_KC_API_BASE_URL + '/ml/text-to-cad/iteration'
|
||||
const data: Models['TextToCadIteration_type'] | Error =
|
||||
await crossPlatformFetch(
|
||||
|
||||
@ -23,30 +23,6 @@ export const telemetryLoader: LoaderFunction = async ({
|
||||
return null
|
||||
}
|
||||
|
||||
// Redirect users to the appropriate onboarding page if they haven't completed it
|
||||
export const onboardingRedirectLoader: ActionFunction = async (args) => {
|
||||
const settings = getSettings()
|
||||
const onboardingStatus: OnboardingStatus =
|
||||
settings.app.onboardingStatus.current || ''
|
||||
const notEnRouteToOnboarding = !args.request.url.includes(
|
||||
PATHS.ONBOARDING.INDEX
|
||||
)
|
||||
// '' is the initial state, 'completed' and 'dismissed' are the final states
|
||||
const hasValidOnboardingStatus =
|
||||
onboardingStatus.length === 0 ||
|
||||
!(onboardingStatus === 'completed' || onboardingStatus === 'dismissed')
|
||||
const shouldRedirectToOnboarding =
|
||||
notEnRouteToOnboarding && hasValidOnboardingStatus
|
||||
|
||||
if (shouldRedirectToOnboarding) {
|
||||
return redirect(
|
||||
makeUrlPathRelative(PATHS.ONBOARDING.INDEX) + onboardingStatus.slice(1)
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export const fileLoader: LoaderFunction = async (
|
||||
routerData
|
||||
): Promise<FileLoaderData | Response> => {
|
||||
|
||||
1298
src/lib/selections.test.ts
Normal file
@ -11,6 +11,7 @@ import {
|
||||
Expr,
|
||||
defaultSourceRange,
|
||||
topLevelRange,
|
||||
ArtifactGraph,
|
||||
} from 'lang/wasm'
|
||||
import { ModelingMachineEvent } from 'machines/modelingMachine'
|
||||
import { isNonNullable, uuidv4 } from 'lib/utils'
|
||||
@ -31,19 +32,13 @@ import { PathToNodeMap } from 'lang/std/sketchcombos'
|
||||
import { err } from 'lib/trap'
|
||||
import {
|
||||
Artifact,
|
||||
getArtifactOfTypes,
|
||||
getArtifactsOfTypes,
|
||||
getCapCodeRef,
|
||||
getSweepEdgeCodeRef,
|
||||
getSolid2dCodeRef,
|
||||
getWallCodeRef,
|
||||
CodeRef,
|
||||
getCodeRefsByArtifactId,
|
||||
ArtifactId,
|
||||
getFaceCodeRef,
|
||||
} from 'lang/std/artifactGraph'
|
||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
import { DefaultPlaneStr } from './planes'
|
||||
import { ArtifactEntry, ArtifactIndex } from './artifactIndex'
|
||||
|
||||
export const X_AXIS_UUID = 'ad792545-7fd3-482a-a602-a93924e3055b'
|
||||
export const Y_AXIS_UUID = '680fd157-266f-4b8a-984f-cdf46b8bdf01'
|
||||
@ -54,38 +49,7 @@ export type DefaultPlaneSelection = {
|
||||
id: string
|
||||
}
|
||||
|
||||
/** @deprecated Use {@link Artifact} instead. */
|
||||
type Selection__old =
|
||||
| {
|
||||
type:
|
||||
| 'default'
|
||||
| 'line-end'
|
||||
| 'line-mid'
|
||||
| 'extrude-wall'
|
||||
| 'solid2d'
|
||||
| 'start-cap'
|
||||
| 'end-cap'
|
||||
| 'point'
|
||||
| 'edge'
|
||||
| 'adjacent-edge'
|
||||
| 'line'
|
||||
| 'arc'
|
||||
| 'all'
|
||||
range: SourceRange
|
||||
}
|
||||
| {
|
||||
type: 'opposite-edgeCut' | 'adjacent-edgeCut' | 'base-edgeCut'
|
||||
range: SourceRange
|
||||
// TODO this is a temporary measure that well be made redundant with: https://github.com/KittyCAD/modeling-app/pull/3836
|
||||
secondaryRange: SourceRange
|
||||
}
|
||||
export type NonCodeSelection = Axis | DefaultPlaneSelection
|
||||
|
||||
/** @deprecated Use {@link Selection} instead. */
|
||||
export type Selections__old = {
|
||||
otherSelections: NonCodeSelection[]
|
||||
codeBasedSelections: Selection__old[]
|
||||
}
|
||||
export interface Selection {
|
||||
artifact?: Artifact
|
||||
codeRef: CodeRef
|
||||
@ -95,76 +59,6 @@ export type Selections = {
|
||||
graphSelections: Array<Selection>
|
||||
}
|
||||
|
||||
/** @deprecated If you're writing a new function, it should use {@link Selection} and not {@link Selection__old}
|
||||
* this function should only be used for backwards compatibility with old functions.
|
||||
*/
|
||||
function convertSelectionToOld(selection: Selection): Selection__old | null {
|
||||
// return {} as Selection__old
|
||||
// TODO implementation
|
||||
const _artifact = selection.artifact
|
||||
if (_artifact?.type === 'solid2d') {
|
||||
const codeRef = getSolid2dCodeRef(
|
||||
_artifact,
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
if (err(codeRef)) return null
|
||||
return { range: codeRef.range, type: 'solid2d' }
|
||||
}
|
||||
if (_artifact?.type === 'cap') {
|
||||
const codeRef = getCapCodeRef(_artifact, engineCommandManager.artifactGraph)
|
||||
if (err(codeRef)) return null
|
||||
return {
|
||||
range: codeRef.range,
|
||||
type: _artifact?.subType === 'end' ? 'end-cap' : 'start-cap',
|
||||
}
|
||||
}
|
||||
if (_artifact?.type === 'wall') {
|
||||
const codeRef = getWallCodeRef(
|
||||
_artifact,
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
if (err(codeRef)) return null
|
||||
return { range: codeRef.range, type: 'extrude-wall' }
|
||||
}
|
||||
if (_artifact?.type === 'segment' || _artifact?.type === 'path') {
|
||||
return { range: _artifact.codeRef.range, type: 'default' }
|
||||
}
|
||||
if (_artifact?.type === 'sweepEdge') {
|
||||
const codeRef = getSweepEdgeCodeRef(
|
||||
_artifact,
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
if (err(codeRef)) return null
|
||||
if (_artifact?.subType === 'adjacent') {
|
||||
return { range: codeRef.range, type: 'adjacent-edge' }
|
||||
}
|
||||
return { range: codeRef.range, type: 'edge' }
|
||||
}
|
||||
if (_artifact?.type === 'edgeCut') {
|
||||
const codeRef = _artifact.codeRef
|
||||
return { range: codeRef.range, type: 'default' }
|
||||
}
|
||||
if (selection?.codeRef?.range) {
|
||||
return { range: selection.codeRef.range, type: 'default' }
|
||||
}
|
||||
return null
|
||||
}
|
||||
/** @deprecated If you're writing a new function, it should use {@link Selection} and not {@link Selection__old}
|
||||
* this function should only be used for backwards compatibility with old functions.
|
||||
*/
|
||||
export function convertSelectionsToOld(selection: Selections): Selections__old {
|
||||
const selections: Selection__old[] = []
|
||||
for (const artifact of selection.graphSelections) {
|
||||
const converted = convertSelectionToOld(artifact)
|
||||
if (converted) selections.push(converted)
|
||||
}
|
||||
const selectionsOld: Selections__old = {
|
||||
otherSelections: selection.otherSelections,
|
||||
codeBasedSelections: selections,
|
||||
}
|
||||
return selectionsOld
|
||||
}
|
||||
|
||||
export async function getEventForSelectWithPoint({
|
||||
data,
|
||||
}: Extract<
|
||||
@ -310,7 +204,6 @@ export function handleSelectionBatch({
|
||||
selections.graphSelections.forEach(({ artifact }) => {
|
||||
artifact?.id &&
|
||||
selectionToEngine.push({
|
||||
type: 'default',
|
||||
id: artifact?.id,
|
||||
range:
|
||||
getCodeRefsByArtifactId(
|
||||
@ -350,7 +243,6 @@ export function handleSelectionBatch({
|
||||
}
|
||||
|
||||
type SelectionToEngine = {
|
||||
type: Selection__old['type']
|
||||
id?: string
|
||||
range: SourceRange
|
||||
}
|
||||
@ -360,11 +252,13 @@ export function processCodeMirrorRanges({
|
||||
selectionRanges,
|
||||
isShiftDown,
|
||||
ast,
|
||||
artifactGraph,
|
||||
}: {
|
||||
codeMirrorRanges: readonly SelectionRange[]
|
||||
selectionRanges: Selections
|
||||
isShiftDown: boolean
|
||||
ast: Program
|
||||
artifactGraph: ArtifactGraph
|
||||
}): null | {
|
||||
modelingEvent: ModelingMachineEvent
|
||||
engineEvents: Models['WebSocketRequest_type'][]
|
||||
@ -392,8 +286,11 @@ export function processCodeMirrorRanges({
|
||||
},
|
||||
}
|
||||
})
|
||||
const idBasedSelections: SelectionToEngine[] =
|
||||
codeToIdSelections(codeBasedSelections)
|
||||
const idBasedSelections: SelectionToEngine[] = codeToIdSelections(
|
||||
codeBasedSelections,
|
||||
artifactGraph,
|
||||
engineCommandManager.artifactIndex
|
||||
)
|
||||
const selections: Selection[] = []
|
||||
for (const { id, range } of idBasedSelections) {
|
||||
if (!id) {
|
||||
@ -406,11 +303,8 @@ export function processCodeMirrorRanges({
|
||||
})
|
||||
continue
|
||||
}
|
||||
const artifact = engineCommandManager.artifactGraph.get(id)
|
||||
const codeRefs = getCodeRefsByArtifactId(
|
||||
id,
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
const artifact = artifactGraph.get(id)
|
||||
const codeRefs = getCodeRefsByArtifactId(id, artifactGraph)
|
||||
if (artifact && codeRefs) {
|
||||
selections.push({ artifact, codeRef: codeRefs[0] })
|
||||
} else if (codeRefs) {
|
||||
@ -587,7 +481,9 @@ export function getSelectionTypeDisplayText(
|
||||
|
||||
export function canSubmitSelectionArg(
|
||||
selectionsByType: 'none' | Map<ResolvedSelectionType, number>,
|
||||
argument: CommandArgument<unknown> & { inputType: 'selection' }
|
||||
argument: CommandArgument<unknown> & {
|
||||
inputType: 'selection' | 'selectionMixed'
|
||||
}
|
||||
) {
|
||||
return (
|
||||
selectionsByType !== 'none' &&
|
||||
@ -601,249 +497,166 @@ export function canSubmitSelectionArg(
|
||||
)
|
||||
}
|
||||
|
||||
export function codeToIdSelections(
|
||||
selections: Selection[]
|
||||
): SelectionToEngine[] {
|
||||
const selectionsOld = convertSelectionsToOld({
|
||||
graphSelections: selections,
|
||||
otherSelections: [],
|
||||
}).codeBasedSelections
|
||||
return selectionsOld
|
||||
.flatMap((selection): null | SelectionToEngine[] => {
|
||||
const { type } = selection
|
||||
// TODO #868: loops over all artifacts will become inefficient at a large scale
|
||||
const overlappingEntries = Array.from(engineCommandManager.artifactGraph)
|
||||
.map(([id, artifact]) => {
|
||||
const codeRef = getFaceCodeRef(artifact)
|
||||
if (!codeRef) return null
|
||||
return isOverlap(codeRef.range, selection.range)
|
||||
? {
|
||||
artifact,
|
||||
selection,
|
||||
id,
|
||||
}
|
||||
: null
|
||||
})
|
||||
.filter(isNonNullable)
|
||||
/**
|
||||
* Find the index of the last range where range[0] < targetStart
|
||||
* This is used as a starting point for linear search of overlapping ranges
|
||||
* @param index The sorted array of ranges to search through
|
||||
* @param targetStart The start position to compare against
|
||||
* @returns The index of the last range where range[0] < targetStart
|
||||
*/
|
||||
export function findLastRangeStartingBefore(
|
||||
index: ArtifactIndex,
|
||||
targetStart: number
|
||||
): number {
|
||||
let left = 0
|
||||
let right = index.length - 1
|
||||
let lastValidIndex = 0
|
||||
|
||||
/** TODO refactor
|
||||
* selections in our app is a sourceRange plus some metadata
|
||||
* The metadata is just a union type string of different types of artifacts or 3d features 'extrude-wall' 'segment' etc
|
||||
* Because the source range is not enough to figure out what the user selected, so here we're using filtering through all the artifacts
|
||||
* to find something that matches both the source range and the metadata.
|
||||
*
|
||||
* What we should migrate to is just storing what the user selected by what it matched in the artifactGraph it will simply the below a lot.
|
||||
*
|
||||
* In the case of a user moving the cursor them, we will still need to figure out what artifact from the graph matches best, but we will just need sane defaults
|
||||
* and most of the time we can expect the user to be clicking in the 3d scene instead.
|
||||
*/
|
||||
let bestCandidate:
|
||||
| {
|
||||
id: ArtifactId
|
||||
artifact: unknown
|
||||
selection: Selection__old
|
||||
}
|
||||
| undefined
|
||||
overlappingEntries.forEach((entry) => {
|
||||
// TODO probably need to remove much of the `type === 'xyz'` below
|
||||
if (type === 'default' && entry.artifact.type === 'segment') {
|
||||
bestCandidate = entry
|
||||
return
|
||||
}
|
||||
if (entry.artifact.type === 'path') {
|
||||
const artifact = engineCommandManager.artifactGraph.get(
|
||||
entry.artifact.solid2dId || ''
|
||||
)
|
||||
if (artifact?.type !== 'solid2d') {
|
||||
bestCandidate = {
|
||||
artifact: entry.artifact,
|
||||
selection,
|
||||
id: entry.id,
|
||||
}
|
||||
}
|
||||
if (!entry.artifact.solid2dId) {
|
||||
console.error(
|
||||
'Expected PathArtifact to have solid2dId, but none found'
|
||||
)
|
||||
return
|
||||
}
|
||||
bestCandidate = {
|
||||
artifact: artifact,
|
||||
selection,
|
||||
id: entry.artifact.solid2dId,
|
||||
}
|
||||
}
|
||||
if (entry.artifact.type === 'plane') {
|
||||
bestCandidate = {
|
||||
artifact: entry.artifact,
|
||||
selection,
|
||||
id: entry.id,
|
||||
}
|
||||
}
|
||||
if (entry.artifact.type === 'cap') {
|
||||
bestCandidate = {
|
||||
artifact: entry.artifact,
|
||||
selection,
|
||||
id: entry.id,
|
||||
}
|
||||
}
|
||||
if (entry.artifact.type === 'wall') {
|
||||
bestCandidate = {
|
||||
artifact: entry.artifact,
|
||||
selection,
|
||||
id: entry.id,
|
||||
}
|
||||
}
|
||||
if (type === 'extrude-wall' && entry.artifact.type === 'segment') {
|
||||
if (!entry.artifact.surfaceId) return
|
||||
const wall = engineCommandManager.artifactGraph.get(
|
||||
entry.artifact.surfaceId
|
||||
)
|
||||
if (wall?.type !== 'wall') return
|
||||
bestCandidate = {
|
||||
artifact: wall,
|
||||
selection,
|
||||
id: entry.artifact.surfaceId,
|
||||
}
|
||||
return
|
||||
}
|
||||
if (type === 'edge' && entry.artifact.type === 'segment') {
|
||||
const edges = getArtifactsOfTypes(
|
||||
{ keys: entry.artifact.edgeIds, types: ['sweepEdge'] },
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
const edge = [...edges].find(([_, edge]) => edge.type === 'sweepEdge')
|
||||
if (!edge) return
|
||||
bestCandidate = {
|
||||
artifact: edge[1],
|
||||
selection,
|
||||
id: edge[0],
|
||||
}
|
||||
}
|
||||
if (type === 'adjacent-edge' && entry.artifact.type === 'segment') {
|
||||
const edges = getArtifactsOfTypes(
|
||||
{ keys: entry.artifact.edgeIds, types: ['sweepEdge'] },
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
const edge = [...edges].find(
|
||||
([_, edge]) =>
|
||||
edge.type === 'sweepEdge' && edge.subType === 'adjacent'
|
||||
)
|
||||
if (!edge) return
|
||||
bestCandidate = {
|
||||
artifact: edge[1],
|
||||
selection,
|
||||
id: edge[0],
|
||||
}
|
||||
}
|
||||
if (
|
||||
(type === 'end-cap' || type === 'start-cap') &&
|
||||
entry.artifact.type === 'path'
|
||||
) {
|
||||
if (!entry.artifact.sweepId) return
|
||||
const extrusion = getArtifactOfTypes(
|
||||
{
|
||||
key: entry.artifact.sweepId,
|
||||
types: ['sweep'],
|
||||
},
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
if (err(extrusion)) return
|
||||
const caps = getArtifactsOfTypes(
|
||||
{ keys: extrusion.surfaceIds, types: ['cap'] },
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
const cap = [...caps].find(
|
||||
([_, cap]) => cap.subType === (type === 'end-cap' ? 'end' : 'start')
|
||||
)
|
||||
if (!cap) return
|
||||
bestCandidate = {
|
||||
artifact: entry.artifact,
|
||||
selection,
|
||||
id: cap[0],
|
||||
}
|
||||
return
|
||||
}
|
||||
if (entry.artifact.type === 'edgeCut') {
|
||||
const consumedEdge = getArtifactOfTypes(
|
||||
{
|
||||
key: entry.artifact.consumedEdgeId,
|
||||
types: ['segment', 'sweepEdge'],
|
||||
},
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
if (err(consumedEdge)) return
|
||||
if (
|
||||
consumedEdge.type === 'segment' &&
|
||||
type === 'base-edgeCut' &&
|
||||
isOverlap(
|
||||
consumedEdge.codeRef.range,
|
||||
selection.secondaryRange || [0, 0]
|
||||
)
|
||||
) {
|
||||
bestCandidate = {
|
||||
artifact: entry.artifact,
|
||||
selection,
|
||||
id: entry.id,
|
||||
}
|
||||
} else if (
|
||||
consumedEdge.type === 'sweepEdge' &&
|
||||
((type === 'adjacent-edgeCut' &&
|
||||
consumedEdge.subType === 'adjacent') ||
|
||||
(type === 'opposite-edgeCut' &&
|
||||
consumedEdge.subType === 'opposite'))
|
||||
) {
|
||||
const seg = getArtifactOfTypes(
|
||||
{ key: consumedEdge.segId, types: ['segment'] },
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
if (err(seg)) return
|
||||
if (
|
||||
isOverlap(seg.codeRef.range, selection.secondaryRange || [0, 0])
|
||||
) {
|
||||
bestCandidate = {
|
||||
artifact: entry.artifact,
|
||||
selection,
|
||||
id: entry.id,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
while (left <= right) {
|
||||
const mid = left + Math.floor((right - left) / 2)
|
||||
const midRange = index[mid].range
|
||||
|
||||
if (entry.artifact.type === 'sweep') {
|
||||
bestCandidate = {
|
||||
artifact: entry.artifact,
|
||||
selection,
|
||||
id: entry.id,
|
||||
}
|
||||
}
|
||||
})
|
||||
if (midRange[0] < targetStart) {
|
||||
// This range starts before our selection, look in right half for later ones
|
||||
lastValidIndex = mid
|
||||
left = mid + 1
|
||||
} else {
|
||||
// This range starts at or after our selection, look in left half
|
||||
right = mid - 1
|
||||
}
|
||||
}
|
||||
|
||||
if (bestCandidate) {
|
||||
return [
|
||||
{
|
||||
type,
|
||||
id: bestCandidate.id,
|
||||
range: bestCandidate.selection.range,
|
||||
},
|
||||
]
|
||||
return lastValidIndex
|
||||
}
|
||||
|
||||
function findOverlappingArtifactsFromIndex(
|
||||
selection: Selection,
|
||||
index: ArtifactIndex
|
||||
): ArtifactEntry[] {
|
||||
if (!selection.codeRef?.range) {
|
||||
console.warn('Selection missing code reference range')
|
||||
return []
|
||||
}
|
||||
|
||||
const selectionRange = selection.codeRef.range
|
||||
const results: ArtifactEntry[] = []
|
||||
|
||||
// Binary search to find the last range where range[0] < selectionRange[0]
|
||||
// This search does not take into consideration the end range, so it's possible
|
||||
// the index it finds dose not have any overlap (depending on the end range)
|
||||
// but it's main purpose is to act as a starting point for the linear part of the search
|
||||
// so a tiny loss in efficiency is acceptable to keep the code simple
|
||||
const startIndex = findLastRangeStartingBefore(index, selectionRange[0])
|
||||
|
||||
// Check all potential overlaps from the found position
|
||||
for (let i = startIndex; i < index.length; i++) {
|
||||
const { range, entry } = index[i]
|
||||
// Stop if we've gone past possible overlaps
|
||||
if (range[0] > selectionRange[1]) break
|
||||
|
||||
if (isOverlap(range, selectionRange)) {
|
||||
results.push(entry)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
function getBestCandidate(
|
||||
entries: ArtifactEntry[],
|
||||
artifactGraph: ArtifactGraph
|
||||
): ArtifactEntry | undefined {
|
||||
if (!entries.length) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
// Segments take precedence
|
||||
if (entry.artifact.type === 'segment') {
|
||||
return entry
|
||||
}
|
||||
|
||||
// Handle paths and their solid2d references
|
||||
if (entry.artifact.type === 'path') {
|
||||
const solid2dId = entry.artifact.solid2dId
|
||||
if (!solid2dId) {
|
||||
return entry
|
||||
}
|
||||
return [selection]
|
||||
const solid2d = artifactGraph.get(solid2dId)
|
||||
if (solid2d?.type === 'solid2d') {
|
||||
return { id: solid2dId, artifact: solid2d }
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Other valid artifact types
|
||||
if (['plane', 'cap', 'wall', 'sweep'].includes(entry.artifact.type)) {
|
||||
return entry
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function createSelectionToEngine(
|
||||
selection: Selection,
|
||||
candidateId?: ArtifactId
|
||||
): SelectionToEngine {
|
||||
return {
|
||||
...(candidateId && { id: candidateId }),
|
||||
range: selection.codeRef.range,
|
||||
}
|
||||
}
|
||||
|
||||
export function codeToIdSelections(
|
||||
selections: Selection[],
|
||||
artifactGraph: ArtifactGraph,
|
||||
artifactIndex: ArtifactIndex
|
||||
): SelectionToEngine[] {
|
||||
if (!selections?.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (!artifactGraph) {
|
||||
console.warn('Artifact graph is missing or empty')
|
||||
return selections.map((selection) => createSelectionToEngine(selection))
|
||||
}
|
||||
|
||||
return selections
|
||||
.flatMap((selection): SelectionToEngine[] => {
|
||||
if (!selection) {
|
||||
console.warn('Null or undefined selection encountered')
|
||||
return []
|
||||
}
|
||||
|
||||
// Direct artifact case
|
||||
if (selection.artifact?.id) {
|
||||
return [createSelectionToEngine(selection, selection.artifact.id)]
|
||||
}
|
||||
|
||||
// Find matching artifacts by code range overlap
|
||||
const overlappingEntries = findOverlappingArtifactsFromIndex(
|
||||
selection,
|
||||
artifactIndex
|
||||
)
|
||||
const bestCandidate = getBestCandidate(overlappingEntries, artifactGraph)
|
||||
|
||||
return [createSelectionToEngine(selection, bestCandidate?.id)]
|
||||
})
|
||||
.filter(isNonNullable)
|
||||
}
|
||||
|
||||
export async function sendSelectEventToEngine(
|
||||
e: MouseEvent | React.MouseEvent<HTMLDivElement, MouseEvent>,
|
||||
el: HTMLVideoElement
|
||||
e: React.MouseEvent<HTMLDivElement, MouseEvent>
|
||||
) {
|
||||
const { x, y } = getNormalisedCoordinates({
|
||||
clientX: e.clientX,
|
||||
clientY: e.clientY,
|
||||
el,
|
||||
streamWidth: engineCommandManager.width,
|
||||
streamHeight: engineCommandManager.height,
|
||||
})
|
||||
// No video stream to normalise against, return immediately
|
||||
if (!engineCommandManager.elVideo)
|
||||
return Promise.reject('video element not ready')
|
||||
|
||||
const { x, y } = getNormalisedCoordinates(
|
||||
e,
|
||||
engineCommandManager.elVideo,
|
||||
engineCommandManager.streamDimensions
|
||||
)
|
||||
const res = await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
|
||||
@ -161,25 +161,20 @@ export function toSync<F extends AsyncFn<F>>(
|
||||
}
|
||||
}
|
||||
|
||||
export function getNormalisedCoordinates({
|
||||
clientX,
|
||||
clientY,
|
||||
streamWidth,
|
||||
streamHeight,
|
||||
el,
|
||||
}: {
|
||||
clientX: number
|
||||
clientY: number
|
||||
streamWidth: number
|
||||
streamHeight: number
|
||||
el: HTMLElement
|
||||
}) {
|
||||
const { left, top, width, height } = el?.getBoundingClientRect()
|
||||
const browserX = clientX - left
|
||||
const browserY = clientY - top
|
||||
export function getNormalisedCoordinates(
|
||||
e: PointerEvent | React.MouseEvent<HTMLDivElement, MouseEvent>,
|
||||
elVideo: HTMLVideoElement,
|
||||
streamDimensions: {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
) {
|
||||
const { left, top, width, height } = elVideo?.getBoundingClientRect()
|
||||
const browserX = e.clientX - left
|
||||
const browserY = e.clientY - top
|
||||
return {
|
||||
x: Math.round((browserX / width) * streamWidth),
|
||||
y: Math.round((browserY / height) * streamHeight),
|
||||
x: Math.round((browserX / width) * streamDimensions.width),
|
||||
y: Math.round((browserY / height) * streamDimensions.height),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,7 +5,6 @@ import {
|
||||
CommandArgumentWithName,
|
||||
KclCommandValue,
|
||||
} from 'lib/commandTypes'
|
||||
import { Selections__old } from 'lib/selections'
|
||||
import { getCommandArgumentKclValuesOnly } from 'lib/commandUtils'
|
||||
import { MachineManager } from 'components/MachineManagerProvider'
|
||||
import toast from 'react-hot-toast'
|
||||
@ -16,7 +15,6 @@ export type CommandBarContext = {
|
||||
commands: Command[]
|
||||
selectedCommand?: Command
|
||||
currentArgument?: CommandArgument<unknown> & { name: string }
|
||||
selectionRanges: Selections__old
|
||||
argumentsToSubmit: { [x: string]: unknown }
|
||||
machineManager: MachineManager
|
||||
}
|
||||
@ -297,7 +295,8 @@ export const commandBarMachine = setup({
|
||||
if (
|
||||
context.currentArgument &&
|
||||
context.selectedCommand &&
|
||||
argConfig?.inputType === 'selection' &&
|
||||
(argConfig?.inputType === 'selection' ||
|
||||
argConfig?.inputType === 'selectionMixed') &&
|
||||
argConfig?.validation
|
||||
) {
|
||||
argConfig
|
||||
|
||||
@ -1995,12 +1995,6 @@ export const modelingMachine = setup({
|
||||
// Extract inputs
|
||||
const ast = kclManager.ast
|
||||
const { selection, thickness } = input
|
||||
const dependencies = {
|
||||
kclManager,
|
||||
engineCommandManager,
|
||||
editorManager,
|
||||
codeManager,
|
||||
}
|
||||
|
||||
// Insert the thickness variable if it exists
|
||||
if (
|
||||
@ -2026,7 +2020,6 @@ export const modelingMachine = setup({
|
||||
'variableName' in thickness
|
||||
? thickness.variableIdentifierAst
|
||||
: thickness.valueAst,
|
||||
dependencies,
|
||||
})
|
||||
if (err(shellResult)) {
|
||||
return err(shellResult)
|
||||
|
||||
30
src/wasm-lib/Cargo.lock
generated
@ -122,9 +122,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.95"
|
||||
version = "1.0.96"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
|
||||
checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
]
|
||||
@ -730,7 +730,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "derive-docs"
|
||||
version = "0.1.38"
|
||||
version = "0.1.39"
|
||||
dependencies = [
|
||||
"Inflector",
|
||||
"anyhow",
|
||||
@ -1724,7 +1724,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-lib"
|
||||
version = "0.2.38"
|
||||
version = "0.2.39"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"approx 0.5.1",
|
||||
@ -1791,7 +1791,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-test-server"
|
||||
version = "0.1.38"
|
||||
version = "0.1.39"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"hyper 0.14.32",
|
||||
@ -1858,9 +1858,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kittycad-modeling-cmds"
|
||||
version = "0.2.97"
|
||||
version = "0.2.99"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c37ad10b8a2afdcd1852d027f123cf4e38864ea93e0fda5c7ee1e8a49af49fb"
|
||||
checksum = "828a0c74476533e6258ea7dd70cfc7d63a5df4b37753d30ef198e0689eaac4eb"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@ -3245,9 +3245,9 @@ checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.217"
|
||||
version = "1.0.218"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
|
||||
checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
@ -3263,9 +3263,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.217"
|
||||
version = "1.0.218"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
|
||||
checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -3285,9 +3285,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.138"
|
||||
version = "1.0.139"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949"
|
||||
checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6"
|
||||
dependencies = [
|
||||
"indexmap 2.7.1",
|
||||
"itoa",
|
||||
@ -4208,9 +4208,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.13.1"
|
||||
version = "1.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0"
|
||||
checksum = "93d59ca99a559661b96bf898d8fce28ed87935fd2bea9f05983c1464dd6c71b1"
|
||||
dependencies = [
|
||||
"getrandom 0.3.1",
|
||||
"js-sys",
|
||||
|
||||
@ -80,7 +80,7 @@ members = [
|
||||
[workspace.dependencies]
|
||||
http = "1"
|
||||
kittycad = { version = "0.3.28", default-features = false, features = ["js", "requests"] }
|
||||
kittycad-modeling-cmds = { version = "0.2.97", features = [
|
||||
kittycad-modeling-cmds = { version = "0.2.99", features = [
|
||||
"ts-rs",
|
||||
"websocket",
|
||||
] }
|
||||
@ -103,3 +103,7 @@ path = "tests/modify/main.rs"
|
||||
#[patch.crates-io]
|
||||
#kittycad-modeling-cmds = { path = "../../../modeling-api/modeling-cmds" }
|
||||
#kittycad-modeling-session = { path = "../../../modeling-api/modeling-session" }
|
||||
|
||||
# Local development only. Placeholder to speed up development cycle
|
||||
#[package.metadata.wasm-pack.profile.release]
|
||||
#wasm-opt = false
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "derive-docs"
|
||||
description = "A tool for generating documentation from Rust derive macros"
|
||||
version = "0.1.38"
|
||||
version = "0.1.39"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/KittyCAD/modeling-app"
|
||||
|
||||