Compare commits

...

20 Commits

Author SHA1 Message Date
cbfe3db203 Add simplified walkie talkie test 2025-02-26 13:41:23 -05:00
615b7feabb Don't toss logs on successful snapshot tests (#5522) 2025-02-26 12:09:38 -05:00
5743b9ced0 Correct hovering highlights on HiDPI screens + correct 2 flakey tests (#5510)
* Fix hover highlights on HiDPI screens

* Fix flakey tests with new toolbar.exitSketch

* tsc && lint && fmt

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Disable pw electron thing again

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-26 11:12:08 -05:00
8896d06028 Release KCL 39 (#5518) 2025-02-26 09:07:57 -06:00
max
1f217ef50b Fix Second-Body Extrude Selection (#5456)
* getSweepArtifactFromSelection

* update getPathToExtrudeForSegmentSelection

* update shell

* add tests and update selection

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* add support for wall and cap

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* fmt

* add CallExpressionKw

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-26 08:28:41 -05:00
5ef5c6280c Fix: revert the red color for runtime error back to the hue shift color (#5509)
* fix: don't use red for runtime error, use hue shift like the original error icon

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* fix: decrease font size for better layout

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-25 22:15:01 -06:00
aac95e1e2e Fix to add revolve about edge to the artifact graph (#5511) 2025-02-25 23:04:10 -05:00
18f4a1303c Multiple prompt-to-edit selection, plus direct editor selections (#5478)
* Add multiple selections and editor selections for promptToEdit

* remove unused

* re-enable prompt to edit tests

* add test for manual code selection

* at test for multi-selection

* clean up

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* typo

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-26 03:06:51 +00:00
ded97eda61 Fix kcl-samples URL and other minor things (#5508)
* Fix kcl-samples URL

* Delete debug files
2025-02-25 20:54:05 +00:00
f6b06520ee Bugfix: wait for settings loading before onboarding redirect check (#5470)
* Bugfix: wait for settings loading before onboarding redirect check

If you refresh the app while viewing a file, the settingsActor could not
have loaded the user settings before checking the onboardingStatus
setting. This uses a subscription on the settingsActor to await the
"init" state, after the user settings have loaded.

* Adjust approach to not use routeLoaders

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-25 14:35:19 -05:00
dcfcdc98ce Feature: Show runtime errors within files that are being imported (#5500)
* chore: dumping progress

* chore: saving progress

* fix: Got a working example of filenames piped from Rust to TS

* fix: cleaning up debugging code

* fix: TS type for filenames

* fix: rust linter errors

* fix: cargo fmt

* fix: testing code, updating KCLError class for filenames

* fix: auto fixes

* feat: display badge in project folder if there is an error in another file

* chore: skeleton ideas for badge notifications from errors in imported files

* fix: more skeleton code to test some potential implementations

* fix: addressing PR comments

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* fix: fixing the rust struct?

* fix: cargo fmt

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* feat: skeleton workflow for showing runtime errors

* chore: showBadge, adding more props

* fix: new application state to reset errors from previous execution if parse fails first

* fix: cleanup

* fix: better UI

* fix: adding comment for future

* fix: revert for production

* fix: removing unused comment

* chore: swapping JS object to typed Map

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-25 13:18:59 -06:00
9ab3325580 Revert "Get rid of failing cache on build-apps' setup-node" and fix wasm copy on Windows (#5505)
* Revert "Get rid of failing cache on build-apps' setup-node (#5490)"

This reverts commit 2523242bb1.

* Fix JSON backlash escape hell
2025-02-25 18:56:00 +00:00
cb5ad3ab27 Make nightly builds tag-based to allow manual triggering (#5493)
* Get rid of failing cache on build-apps' setup-node

* Make nightly runs tag-based to allow manual triggering
Fixes #5492

* Add permissions contents write
2025-02-25 12:53:40 -05:00
1e539cc134 Chore: Return file paths to map SourceRange index to filename (#5471)
* chore: dumping progress

* chore: saving progress

* fix: Got a working example of filenames piped from Rust to TS

* fix: cleaning up debugging code

* fix: TS type for filenames

* fix: rust linter errors

* fix: cargo fmt

* fix: testing code, updating KCLError class for filenames

* fix: auto fixes

* fix: addressing PR comments

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* fix: fixing the rust struct?

* fix: cargo fmt

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-25 11:51:54 -06:00
4297dc43ae Disable pw object.assign context (#5498) 2025-02-25 16:49:58 +00:00
935b4ee7f5 KCL: All keyword arguments should use camelCase (#5481)
Closes #5355
2025-02-25 10:29:59 -06:00
2523242bb1 Get rid of failing cache on build-apps' setup-node (#5490) 2025-02-25 09:53:18 -05:00
309e4fadf0 Backspace as Delete only on macOS (#5453)
* Backspace as Delete only on macOS

* Call isDesktop first

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-25 09:41:25 -05:00
842054de09 refactor codeToIdSelections (#5432)
* add test for original range to artifact conversion

* try naieve refactor

* types clean up

* typo

* break function into smaller functions

* optimizations

* better comments

* camera test tweak

* fmt

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* overflow fix

* update binary search to ignore end ranges

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* break binary search into sub function

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-25 12:51:25 +00:00
af146284b6 Share instances of the std prelude (#5448)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-02-25 16:10:06 +13:00
222 changed files with 8086 additions and 12179 deletions

View File

@ -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 })

View File

@ -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
View 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

View File

@ -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 |

View File

@ -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

View File

@ -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

View File

@ -1,15 +0,0 @@
---
title: "std::prelude::HALF_TURN"
excerpt: ""
layout: manual
---
```js
std::prelude::HALF_TURN: number(deg) = 180deg
```

View File

@ -1,15 +0,0 @@
---
title: "std::prelude::QUARTER_TURN"
excerpt: ""
layout: manual
---
```js
std::prelude::QUARTER_TURN: number(deg) = 90deg
```

View File

@ -1,15 +0,0 @@
---
title: "std::prelude::THREE_QUARTER_TURN"
excerpt: ""
layout: manual
---
```js
std::prelude::THREE_QUARTER_TURN: number(deg) = 270deg
```

View File

@ -1,15 +0,0 @@
---
title: "std::prelude::ZERO"
excerpt: ""
layout: manual
---
```js
std::prelude::ZERO: number = 0
```

View File

@ -0,0 +1,15 @@
---
title: "std::HALF_TURN"
excerpt: ""
layout: manual
---
```js
std::HALF_TURN: number(deg) = 180deg
```

View File

@ -0,0 +1,15 @@
---
title: "std::QUARTER_TURN"
excerpt: ""
layout: manual
---
```js
std::QUARTER_TURN: number(deg) = 90deg
```

View File

@ -0,0 +1,15 @@
---
title: "std::THREE_QUARTER_TURN"
excerpt: ""
layout: manual
---
```js
std::THREE_QUARTER_TURN: number(deg) = 270deg
```

View File

@ -0,0 +1,15 @@
---
title: "std::ZERO"
excerpt: ""
layout: manual
---
```js
std::ZERO: number = 0
```

View File

@ -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

View File

@ -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 |

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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 |

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 |

View File

@ -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

View File

@ -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",

View File

@ -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()
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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')

View File

@ -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()
})
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View File

@ -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])

View File

@ -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
View File

@ -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
View File

@ -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()

View File

@ -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
View File

@ -0,0 +1 @@
copy src\wasm-lib\pkg\wasm_lib_bg.wasm public

View File

@ -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

View File

@ -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 />,

View File

@ -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,
})

View File

@ -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 || ''
)

View File

@ -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

View File

@ -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} />

View File

@ -124,7 +124,8 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
<span className="sr-only">:&nbsp;</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(

View 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>
)
}

View File

@ -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(

View File

@ -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}
/>
)
)}

View File

@ -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' })
})

View File

@ -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',

View File

@ -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">&nbsp;has&nbsp;</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">
&nbsp;notification{Number(showBadge.value) > 1 ? 's' : ''}
</span>
{typeof showBadge.value === 'number' && (
<span className="sr-only">
&nbsp;notification{Number(showBadge.value) > 1 ? 's' : ''}
</span>
)}
</p>
)}
</div>

View File

@ -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])

View File

@ -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

View File

@ -374,6 +374,7 @@ export default class EditorManager {
selectionRanges: this._selectionRanges,
isShiftDown: this._isShiftDown,
ast: kclManager.ast,
artifactGraph: engineCommandManager.artifactGraph,
})
if (!eventInfo) {

View File

@ -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 = () => {

View File

@ -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) {

View File

@ -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)

View File

@ -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
}

View File

@ -511,7 +511,8 @@ const theExtrude = startSketchOn('XY')
topLevelRange(129, 135),
[],
[],
defaultArtifactGraph()
defaultArtifactGraph(),
{}
)
)
})

View File

@ -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], %)

View File

@ -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

View File

@ -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")

View File

@ -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

View File

@ -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)

View File

@ -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
View 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])
}

View File

@ -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: {

View File

@ -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

View File

@ -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,

View File

@ -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(

View File

@ -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

File diff suppressed because it is too large Load Diff

View 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: {

View File

@ -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),
}
}

View File

@ -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

View File

@ -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)

View File

@ -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",

View File

@ -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

View File

@ -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"

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