Compare commits
23 Commits
pierremtb/
...
nightly-v2
Author | SHA1 | Date | |
---|---|---|---|
775007ef0d | |||
f490e7d6fe | |||
89bc93e4cd | |||
12edb6375d | |||
8d9bba02d6 | |||
56e100fad7 | |||
38f7a4089e | |||
91f5465e2d | |||
58a9c60d0b | |||
725c4d95f8 | |||
615b7feabb | |||
5743b9ced0 | |||
8896d06028 | |||
1f217ef50b | |||
5ef5c6280c | |||
aac95e1e2e | |||
18f4a1303c | |||
ded97eda61 | |||
f6b06520ee | |||
dcfcdc98ce | |||
9ab3325580 | |||
cb5ad3ab27 | |||
1e539cc134 |
@ -1,3 +1,4 @@
|
||||
src/wasm-lib/*
|
||||
*.typegen.ts
|
||||
packages/codemirror-lsp-client/dist/*
|
||||
e2e/playwright/snapshots/prompt-to-edit/*
|
||||
|
36
.github/workflows/build-apps.yml
vendored
@ -7,14 +7,11 @@ on:
|
||||
- main
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
schedule:
|
||||
- cron: '0 4 * * *'
|
||||
# Daily at 04:00 AM UTC
|
||||
# Will checkout the last commit from the default branch (main as of 2023-10-04)
|
||||
- 'nightly-v[0-9]+.[0-9]+.[0-9]+'
|
||||
|
||||
env:
|
||||
IS_RELEASE: ${{ github.ref_type == 'tag' }}
|
||||
IS_NIGHTLY: ${{ github.event_name == 'schedule' }}
|
||||
IS_RELEASE: ${{ github.ref_type == 'tag' && startsWith(github.ref_name, 'v') }}
|
||||
IS_NIGHTLY: ${{ github.ref_type == 'tag' && startsWith(github.ref_name, 'nightly-v') }}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
@ -32,6 +29,7 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
|
||||
- run: yarn install
|
||||
|
||||
@ -52,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' }}
|
||||
@ -123,9 +124,11 @@ jobs:
|
||||
cp prepared-files/assets/icon.ico assets/icon.ico
|
||||
cp prepared-files/assets/icon.png assets/icon.png
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- name: Sync node version and setup cache
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn' # Set this to npm, yarn or pnpm.
|
||||
|
||||
- name: yarn install
|
||||
# Windows is picky sometimes and fails on fetch. Step takes about ~30s
|
||||
@ -270,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) }}
|
||||
@ -327,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 \
|
||||
@ -411,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 })
|
||||
|
8
.github/workflows/e2e-tests.yml
vendored
@ -142,7 +142,7 @@ jobs:
|
||||
# TODO: break this in its own job, for now it's not slowing down the overall execution as ubuntu is the quickest,
|
||||
# but we could do better. This forces a large 1/1 shard of all 20 snapshot tests that runs in about 3 minutes.
|
||||
run: |
|
||||
PLATFORM=web yarn playwright test --config=playwright.config.ts --retries="3" --update-snapshots --grep=@snapshot --shard=1/1
|
||||
PLATFORM=web yarn playwright test --config=playwright.config.ts --retries="3" --update-snapshots --grep=@snapshot --trace=on --shard=1/1
|
||||
env:
|
||||
CI: true
|
||||
NODE_ENV: development
|
||||
@ -153,7 +153,7 @@ jobs:
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() && (success() || failure()) }}
|
||||
with:
|
||||
name: playwright-report-${{ matrix.os }}-snapshot-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
name: playwright-report-snapshots-${{ matrix.os }}-snapshot-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
path: playwright-report/
|
||||
include-hidden-files: true
|
||||
retention-days: 30
|
||||
@ -167,7 +167,7 @@ jobs:
|
||||
shell: bash
|
||||
id: git-check
|
||||
run: |
|
||||
git add e2e/playwright/snapshot-tests.spec.ts-snapshots
|
||||
git add e2e/playwright/snapshot-tests.spec.ts-snapshots e2e/playwright/snapshots
|
||||
if git status | grep -q "Changes to be committed"
|
||||
then echo "modified=true" >> $GITHUB_OUTPUT
|
||||
else echo "modified=false" >> $GITHUB_OUTPUT
|
||||
@ -176,7 +176,7 @@ jobs:
|
||||
if: steps.git-check.outputs.modified == 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
git add e2e/playwright/snapshot-tests.spec.ts-snapshots
|
||||
git add e2e/playwright/snapshot-tests.spec.ts-snapshots e2e/playwright/snapshots
|
||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git remote set-url origin https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
|
||||
|
39
.github/workflows/tag-nightly.yml
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
name: tag-nightly
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 4 * * *'
|
||||
# Daily at 04:00 AM UTC
|
||||
# Will checkout the last commit from the default branch (main as of 2023-10-04)
|
||||
|
||||
jobs:
|
||||
tag-nightly:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/create-github-app-token@v1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.MODELING_APP_GH_APP_ID }}
|
||||
private-key: ${{ secrets.MODELING_APP_GH_APP_PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- run: yarn install
|
||||
|
||||
- name: Push tag
|
||||
run: |
|
||||
VERSION_NO_V=$(date +'%-y.%-m.%-d')
|
||||
TAG="nightly-v$VERSION_NO_V"
|
||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git tag $TAG
|
||||
git push origin tag $TAG
|
3
.gitignore
vendored
@ -24,7 +24,7 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
.idea
|
||||
# .vscode
|
||||
.vscode
|
||||
.helix
|
||||
src/wasm-lib/.idea
|
||||
src/wasm-lib/.vscode
|
||||
@ -71,3 +71,4 @@ venv
|
||||
|
||||
# electron
|
||||
out/
|
||||
*.snap.new
|
||||
|
@ -10,6 +10,7 @@ target
|
||||
src/wasm-lib/pkg
|
||||
src/wasm-lib/kcl/bindings
|
||||
e2e/playwright/export-snapshots
|
||||
e2e/playwright/snapshots/prompt-to-edit
|
||||
|
||||
|
||||
# XState generated files
|
||||
|
5
.vscode/settings.json
vendored
@ -1,5 +0,0 @@
|
||||
{
|
||||
"rust-analyzer.linkedProjects": [
|
||||
"src/wasm-lib/Cargo.toml"
|
||||
]
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
---
|
||||
title: "QUARTER_TURN"
|
||||
excerpt: ""
|
||||
layout: manual
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
```js
|
||||
QUARTER_TURN: number(deg) = 90deg
|
||||
```
|
||||
|
||||
|
@ -1,15 +0,0 @@
|
||||
---
|
||||
title: "THREE_QUARTER_TURN"
|
||||
excerpt: ""
|
||||
layout: manual
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
```js
|
||||
THREE_QUARTER_TURN: number(deg) = 270deg
|
||||
```
|
||||
|
||||
|
@ -29,7 +29,7 @@ angledLine(data: AngledLineData, sketch: Sketch, tag?: TagDeclarator) -> Sketch
|
||||
### Examples
|
||||
|
||||
```js
|
||||
exampleSketch = startSketchOn('XZ')
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> yLineTo(15, %)
|
||||
|> angledLine({ angle = 30, length = 15 }, %)
|
||||
|
@ -29,7 +29,7 @@ angledLineOfXLength(data: AngledLineData, sketch: Sketch, tag?: TagDeclarator) -
|
||||
### Examples
|
||||
|
||||
```js
|
||||
sketch001 = startSketchOn('XZ')
|
||||
sketch001 = startSketchOn(XZ)
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> angledLineOfXLength({ angle = 45, length = 10 }, %, $edge1)
|
||||
|> angledLineOfXLength({ angle = -15, length = 20 }, %, $edge2)
|
||||
|
@ -29,7 +29,7 @@ angledLineOfYLength(data: AngledLineData, sketch: Sketch, tag?: TagDeclarator) -
|
||||
### Examples
|
||||
|
||||
```js
|
||||
exampleSketch = startSketchOn('XZ')
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> line(end = [10, 0])
|
||||
|> angledLineOfYLength({ angle = 45, length = 10 }, %)
|
||||
|
@ -29,7 +29,7 @@ angledLineThatIntersects(data: AngledLineThatIntersectsData, sketch: Sketch, tag
|
||||
### Examples
|
||||
|
||||
```js
|
||||
exampleSketch = startSketchOn('XZ')
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> line(endAbsolute = [5, 10])
|
||||
|> line(endAbsolute = [-10, 10], tag = $lineToIntersect)
|
||||
|
@ -29,7 +29,7 @@ angledLineToX(data: AngledLineToData, sketch: Sketch, tag?: TagDeclarator) -> Sk
|
||||
### Examples
|
||||
|
||||
```js
|
||||
exampleSketch = startSketchOn('XZ')
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> angledLineToX({ angle = 30, to = 10 }, %)
|
||||
|> line(end = [0, 10])
|
||||
|
@ -29,7 +29,7 @@ angledLineToY(data: AngledLineToData, sketch: Sketch, tag?: TagDeclarator) -> Sk
|
||||
### Examples
|
||||
|
||||
```js
|
||||
exampleSketch = startSketchOn('XZ')
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> angledLineToY({ angle = 60, to = 20 }, %)
|
||||
|> line(end = [-20, 0])
|
||||
|
@ -31,7 +31,7 @@ arc(data: ArcData, sketch: Sketch, tag?: TagDeclarator) -> Sketch
|
||||
### Examples
|
||||
|
||||
```js
|
||||
exampleSketch = startSketchOn('XZ')
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> line(end = [10, 0])
|
||||
|> arc({
|
||||
|
@ -29,7 +29,7 @@ arcTo(data: ArcToData, sketch: Sketch, tag?: TagDeclarator) -> Sketch
|
||||
### Examples
|
||||
|
||||
```js
|
||||
exampleSketch = startSketchOn('XZ')
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> arcTo({ end = [10, 0], interior = [5, 5] }, %)
|
||||
|> close()
|
||||
|
@ -29,7 +29,7 @@ bezierCurve(data: BezierData, sketch: Sketch, tag?: TagDeclarator) -> Sketch
|
||||
### Examples
|
||||
|
||||
```js
|
||||
exampleSketch = startSketchOn('XZ')
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> line(end = [0, 10])
|
||||
|> bezierCurve({
|
||||
|
@ -1,15 +0,0 @@
|
||||
---
|
||||
title: "HALF_TURN"
|
||||
excerpt: ""
|
||||
layout: manual
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
```js
|
||||
HALF_TURN: number(deg) = 180deg
|
||||
```
|
||||
|
||||
|
@ -1,15 +0,0 @@
|
||||
---
|
||||
title: "QUARTER_TURN"
|
||||
excerpt: ""
|
||||
layout: manual
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
```js
|
||||
QUARTER_TURN: number(deg) = 90deg
|
||||
```
|
||||
|
||||
|
@ -1,15 +0,0 @@
|
||||
---
|
||||
title: "THREE_QUARTER_TURN"
|
||||
excerpt: ""
|
||||
layout: manual
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
```js
|
||||
THREE_QUARTER_TURN: number(deg) = 270deg
|
||||
```
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
title: "ZERO"
|
||||
title: "std::XY"
|
||||
excerpt: ""
|
||||
layout: manual
|
||||
---
|
||||
@ -9,7 +9,7 @@ layout: manual
|
||||
|
||||
|
||||
```js
|
||||
ZERO: number = 0
|
||||
std::XY
|
||||
```
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
title: "ZERO"
|
||||
title: "std::XZ"
|
||||
excerpt: ""
|
||||
layout: manual
|
||||
---
|
||||
@ -9,7 +9,7 @@ layout: manual
|
||||
|
||||
|
||||
```js
|
||||
ZERO: number = 0
|
||||
std::XZ
|
||||
```
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
title: "HALF_TURN"
|
||||
title: "std::YZ"
|
||||
excerpt: ""
|
||||
layout: manual
|
||||
---
|
||||
@ -9,7 +9,7 @@ layout: manual
|
||||
|
||||
|
||||
```js
|
||||
HALF_TURN: number(deg) = 180deg
|
||||
std::YZ
|
||||
```
|
||||
|
||||
|
@ -28,7 +28,7 @@ hole(holeSketch: SketchSet, sketch: Sketch) -> Sketch
|
||||
### Examples
|
||||
|
||||
```js
|
||||
exampleSketch = startSketchOn('XY')
|
||||
exampleSketch = startSketchOn(XY)
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> line(end = [0, 5])
|
||||
|> line(end = [5, 0])
|
||||
@ -44,7 +44,7 @@ example = extrude(exampleSketch, length = 1)
|
||||
|
||||
```js
|
||||
fn squareHoleSketch() {
|
||||
squareSketch = startSketchOn('-XZ')
|
||||
squareSketch = startSketchOn(-XZ)
|
||||
|> startProfileAt([-1, -1], %)
|
||||
|> line(end = [2, 0])
|
||||
|> line(end = [0, 2])
|
||||
@ -53,7 +53,7 @@ fn squareHoleSketch() {
|
||||
return squareSketch
|
||||
}
|
||||
|
||||
exampleSketch = startSketchOn('-XZ')
|
||||
exampleSketch = startSketchOn(-XZ)
|
||||
|> circle({ center = [0, 0], radius = 3 }, %)
|
||||
|> hole(squareHoleSketch(), %)
|
||||
example = extrude(exampleSketch, length = 1)
|
||||
|
@ -6,13 +6,22 @@ layout: manual
|
||||
|
||||
## Table of Contents
|
||||
|
||||
* [Types](kcl/types)
|
||||
* [Modules](kcl/modules)
|
||||
* [Known Issues](kcl/KNOWN-ISSUES)
|
||||
### Language
|
||||
|
||||
* [`Types`](kcl/types)
|
||||
* [`Modules`](kcl/modules)
|
||||
* [`Settings`](kcl/settings)
|
||||
* [`Known Issues`](kcl/known-issues)
|
||||
|
||||
### Standard library
|
||||
|
||||
* **`std`**
|
||||
* [`HALF_TURN`](kcl/const_std-HALF_TURN)
|
||||
* [`QUARTER_TURN`](kcl/const_std-QUARTER_TURN)
|
||||
* [`THREE_QUARTER_TURN`](kcl/const_std-THREE_QUARTER_TURN)
|
||||
* [`XY`](kcl/const_std-XY)
|
||||
* [`XZ`](kcl/const_std-XZ)
|
||||
* [`YZ`](kcl/const_std-YZ)
|
||||
* [`ZERO`](kcl/const_std-ZERO)
|
||||
* [`abs`](kcl/abs)
|
||||
* [`acos`](kcl/acos)
|
||||
@ -90,7 +99,9 @@ layout: manual
|
||||
* [`reduce`](kcl/reduce)
|
||||
* [`rem`](kcl/rem)
|
||||
* [`revolve`](kcl/revolve)
|
||||
* [`rotate`](kcl/rotate)
|
||||
* [`round`](kcl/round)
|
||||
* [`scale`](kcl/scale)
|
||||
* [`segAng`](kcl/segAng)
|
||||
* [`segEnd`](kcl/segEnd)
|
||||
* [`segEndX`](kcl/segEndX)
|
||||
@ -110,6 +121,7 @@ layout: manual
|
||||
* [`tangentialArcToRelative`](kcl/tangentialArcToRelative)
|
||||
* [`toDegrees`](kcl/toDegrees)
|
||||
* [`toRadians`](kcl/toRadians)
|
||||
* [`translate`](kcl/translate)
|
||||
* [`xLine`](kcl/xLine)
|
||||
* [`xLineTo`](kcl/xLineTo)
|
||||
* [`yLine`](kcl/yLine)
|
||||
|
@ -30,7 +30,7 @@ line(sketch: Sketch, endAbsolute?: [number], end?: [number], tag?: TagDeclarator
|
||||
### Examples
|
||||
|
||||
```js
|
||||
triangle = startSketchOn("XZ")
|
||||
triangle = startSketchOn(XZ)
|
||||
|> startProfileAt([0, 0], %)
|
||||
// The 'end' argument means it ends at exactly [10, 0].
|
||||
// This is an absolute measurement, it is NOT relative to
|
||||
@ -41,7 +41,7 @@ triangle = startSketchOn("XZ")
|
||||
|> close()
|
||||
|> extrude(length = 5)
|
||||
|
||||
box = startSketchOn("XZ")
|
||||
box = startSketchOn(XZ)
|
||||
|> startProfileAt([10, 10], %)
|
||||
// The 'to' argument means move the pen this much.
|
||||
// So, [10, 0] is a relative distance away from the current point.
|
||||
|
@ -20,7 +20,7 @@ export fn increment(x) {
|
||||
Other files in the project can now import functions that have been exported.
|
||||
This makes them available to use in another file.
|
||||
|
||||
```
|
||||
```norun
|
||||
// main.kcl
|
||||
import increment from "util.kcl"
|
||||
|
||||
@ -48,13 +48,13 @@ export fn decrement(x) {
|
||||
|
||||
When importing, you can import multiple functions at once.
|
||||
|
||||
```
|
||||
```norun
|
||||
import increment, decrement from "util.kcl"
|
||||
```
|
||||
|
||||
Imported symbols can be renamed for convenience or to avoid name collisions.
|
||||
|
||||
```
|
||||
```norun
|
||||
import increment as inc, decrement as dec from "util.kcl"
|
||||
```
|
||||
|
||||
@ -63,13 +63,13 @@ import increment as inc, decrement as dec from "util.kcl"
|
||||
`import` can also be used to import files from other CAD systems. The format of the statement is the
|
||||
same as for KCL files. You can only import the whole file, not items from it. E.g.,
|
||||
|
||||
```
|
||||
```norun
|
||||
import "tests/inputs/cube.obj"
|
||||
|
||||
// Use `cube` just like a KCL object.
|
||||
```
|
||||
|
||||
```
|
||||
```norun
|
||||
import "tests/inputs/cube-2.sldprt" as cube
|
||||
|
||||
// Use `cube` just like a KCL object.
|
||||
@ -78,7 +78,7 @@ import "tests/inputs/cube-2.sldprt" as cube
|
||||
You can make the file format explicit using a format attribute (useful if using a different
|
||||
extension), e.g.,
|
||||
|
||||
```
|
||||
```norun
|
||||
@(format = obj)
|
||||
import "tests/inputs/cube"
|
||||
```
|
||||
@ -87,7 +87,7 @@ For formats lacking unit data (such as STL, OBJ, or PLY files), the default
|
||||
unit of measurement is millimeters. Alternatively you may specify the unit
|
||||
by using an attirbute. Likewise, you can also specify a coordinate system. E.g.,
|
||||
|
||||
```
|
||||
```norun
|
||||
@(unitLength = ft, coords = opengl)
|
||||
import "tests/inputs/cube.obj"
|
||||
```
|
||||
|
@ -27,7 +27,7 @@ profileStart(sketch: Sketch) -> [number]
|
||||
### Examples
|
||||
|
||||
```js
|
||||
sketch001 = startSketchOn('XY')
|
||||
sketch001 = startSketchOn(XY)
|
||||
|> startProfileAt([5, 2], %)
|
||||
|> angledLine({ angle = 120, length = 50 }, %, $seg01)
|
||||
|> angledLine({
|
||||
|
@ -27,7 +27,7 @@ profileStartX(sketch: Sketch) -> number
|
||||
### Examples
|
||||
|
||||
```js
|
||||
sketch001 = startSketchOn('XY')
|
||||
sketch001 = startSketchOn(XY)
|
||||
|> startProfileAt([5, 2], %)
|
||||
|> angledLine([-26.6, 50], %)
|
||||
|> angledLine([90, 50], %)
|
||||
|
@ -27,7 +27,7 @@ profileStartY(sketch: Sketch) -> number
|
||||
### Examples
|
||||
|
||||
```js
|
||||
sketch001 = startSketchOn('XY')
|
||||
sketch001 = startSketchOn(XY)
|
||||
|> startProfileAt([5, 2], %)
|
||||
|> angledLine({ angle = -60, length = 14 }, %)
|
||||
|> angledLineToY({ angle = 30, to = profileStartY(%) }, %)
|
||||
|
101
docs/kcl/rotate.md
Normal file
59
docs/kcl/scale.md
Normal file
30
docs/kcl/settings.md
Normal file
@ -0,0 +1,30 @@
|
||||
---
|
||||
title: "KCL settings"
|
||||
excerpt: "Documentation of settings for the KCL language and Zoo Modeling App."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
# Per-file settings
|
||||
|
||||
Settings which affect a single file are configured using the settings attribute.
|
||||
This must be at the top of the KCL file (comments before the attribute are permitted).
|
||||
E.g.,
|
||||
|
||||
```
|
||||
// The settings attribute.
|
||||
@settings(defaultLengthUnit = in)
|
||||
|
||||
// The rest of your KCL code goes below...
|
||||
|
||||
x = 42 // Represents 42 inches.
|
||||
```
|
||||
|
||||
The settings attribute may contain multiple properties separated by commas.
|
||||
Valid properties are:
|
||||
|
||||
- `defaultLengthUnit`: the default length unit to use for numbers declared in this file.
|
||||
- Accepted values: `mm`, `cm`, `m`, `in` (inches), `ft` (feet), `yd` (yards).
|
||||
- `defaultAngleUnit`: the default angle unit to use for numbers declared in this file.
|
||||
- Accepted values: `deg` (degrees), `rad` (radians).
|
||||
|
||||
These settings override any project-wide settings (configured in project.toml or via the UI).
|
27505
docs/kcl/std.json
@ -29,7 +29,7 @@ tangentialArc(data: TangentialArcData, sketch: Sketch, tag?: TagDeclarator) -> S
|
||||
### Examples
|
||||
|
||||
```js
|
||||
exampleSketch = startSketchOn('XZ')
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> angledLine({ angle = 60, length = 10 }, %)
|
||||
|> tangentialArc({ radius = 10, offset = -120 }, %)
|
||||
|
@ -29,7 +29,7 @@ tangentialArcTo(to: [number], sketch: Sketch, tag?: TagDeclarator) -> Sketch
|
||||
### Examples
|
||||
|
||||
```js
|
||||
exampleSketch = startSketchOn('XZ')
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> angledLine({ angle = 60, length = 10 }, %)
|
||||
|> tangentialArcTo([15, 15], %)
|
||||
|
@ -29,7 +29,7 @@ tangentialArcToRelative(delta: [number], sketch: Sketch, tag?: TagDeclarator) ->
|
||||
### Examples
|
||||
|
||||
```js
|
||||
exampleSketch = startSketchOn('XZ')
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> angledLine({ angle = 45, length = 10 }, %)
|
||||
|> tangentialArcToRelative([0, -10], %)
|
||||
|
57
docs/kcl/translate.md
Normal file
@ -74,18 +74,15 @@ fn myFn(x) {
|
||||
|
||||
As you can see above `myFn` just returns whatever it is given.
|
||||
|
||||
KCL's early drafts used positional arguments, but we now use keyword arguments. If you declare a
|
||||
function like this:
|
||||
KCL's early drafts used positional arguments, but we now use keyword arguments:
|
||||
|
||||
```
|
||||
// If you declare a function like this
|
||||
fn add(left, right) {
|
||||
return left + right
|
||||
}
|
||||
```
|
||||
|
||||
You can call it like this:
|
||||
|
||||
```
|
||||
// You can call it like this:
|
||||
total = add(left = 1, right = 2)
|
||||
```
|
||||
|
||||
@ -111,14 +108,14 @@ three = add(1, delta = 2)
|
||||
|
||||
It can be hard to read repeated function calls, because of all the nested brackets.
|
||||
|
||||
```
|
||||
```norun
|
||||
i = 1
|
||||
x = h(g(f(i)))
|
||||
```
|
||||
|
||||
You can make this easier to read by breaking it into many declarations, but that is a bit annoying.
|
||||
|
||||
```
|
||||
```norun
|
||||
i = 1
|
||||
x0 = f(i)
|
||||
x1 = g(x0)
|
||||
@ -133,12 +130,12 @@ the `%` in the right-hand side.
|
||||
So, this means `x |> f(%) |> g(%)` is shorthand for `g(f(x))`. The code example above, with its
|
||||
somewhat-clunky `x0` and `x1` constants could be rewritten as
|
||||
|
||||
```
|
||||
```norun
|
||||
i = 1
|
||||
x = i
|
||||
|> f(%)
|
||||
|> g(%)
|
||||
|> h(%)
|
||||
|> f(%)
|
||||
|> g(%)
|
||||
|> h(%)
|
||||
```
|
||||
|
||||
This helps keep your code neat and avoid unnecessary declarations.
|
||||
@ -147,12 +144,12 @@ This helps keep your code neat and avoid unnecessary declarations.
|
||||
|
||||
Say you have a long pipeline of sketch functions, like this:
|
||||
|
||||
```
|
||||
startSketch()
|
||||
|> line(%, end = [3, 4])
|
||||
|> line(%, end = [10, 10])
|
||||
|> line(%, end = [-13, -14])
|
||||
|> close(%)
|
||||
```norun
|
||||
startSketchOn('XZ')
|
||||
|> line(%, end = [3, 4])
|
||||
|> line(%, end = [10, 10])
|
||||
|> line(%, end = [-13, -14])
|
||||
|> close(%)
|
||||
```
|
||||
|
||||
In this example, each function call outputs a sketch, and it gets put into the next function call via
|
||||
@ -162,12 +159,12 @@ If a function call uses an unlabeled first parameter, it will default to `%` if
|
||||
means that `|> line(%, end = [3, 4])` and `|> line(end = [3, 4])` are equivalent! So the above
|
||||
could be rewritten as
|
||||
|
||||
```
|
||||
startSketch()
|
||||
|> line(end = [3, 4])
|
||||
|> line(end = [10, 10])
|
||||
|> line(end = [-13, -14])
|
||||
|> close()
|
||||
```norun
|
||||
startSketchOn('XZ')
|
||||
|> line(end = [3, 4])
|
||||
|> line(end = [10, 10])
|
||||
|> line(end = [-13, -14])
|
||||
|> close()
|
||||
```
|
||||
|
||||
Note that we are still in the process of migrating KCL's standard library to use keyword arguments. So some
|
||||
@ -184,7 +181,7 @@ Tags are used to give a name (tag) to a specific path.
|
||||
The syntax for declaring a tag is `$myTag` you would use it in the following
|
||||
way:
|
||||
|
||||
```
|
||||
```norun
|
||||
startSketchOn('XZ')
|
||||
|> startProfileAt(origin, %)
|
||||
|> angledLine({angle = 0, length = 191.26}, %, $rectangleSegmentA001)
|
||||
@ -216,7 +213,7 @@ use the tag `rectangleSegmentA001` in any function or expression in the file.
|
||||
|
||||
However if the code was written like this:
|
||||
|
||||
```
|
||||
```norun
|
||||
fn rect(origin) {
|
||||
return startSketchOn('XZ')
|
||||
|> startProfileAt(origin, %)
|
||||
@ -244,7 +241,7 @@ However you likely want to use those tags somewhere outside the `rect` function.
|
||||
Tags are accessible through the sketch group they are declared in.
|
||||
For example the following code works.
|
||||
|
||||
```
|
||||
```norun
|
||||
fn rect(origin) {
|
||||
return startSketchOn('XZ')
|
||||
|> startProfileAt(origin, %)
|
||||
|
@ -34,6 +34,18 @@ A custom plane.
|
||||
|
||||
|
||||
|
||||
----
|
||||
A custom plane which has not been sent to the engine. It must be sent before it is used.
|
||||
|
||||
**enum:** `Uninit`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
----
|
||||
|
||||
|
||||
|
@ -29,7 +29,7 @@ xLine(length: number, sketch: Sketch, tag?: TagDeclarator) -> Sketch
|
||||
### Examples
|
||||
|
||||
```js
|
||||
exampleSketch = startSketchOn('XZ')
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> xLine(15, %)
|
||||
|> angledLine({ angle = 80, length = 15 }, %)
|
||||
|
@ -29,7 +29,7 @@ xLineTo(to: number, sketch: Sketch, tag?: TagDeclarator) -> Sketch
|
||||
### Examples
|
||||
|
||||
```js
|
||||
exampleSketch = startSketchOn('XZ')
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> xLineTo(15, %)
|
||||
|> angledLine({ angle = 80, length = 15 }, %)
|
||||
|
@ -29,7 +29,7 @@ yLine(length: number, sketch: Sketch, tag?: TagDeclarator) -> Sketch
|
||||
### Examples
|
||||
|
||||
```js
|
||||
exampleSketch = startSketchOn('XZ')
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> yLine(15, %)
|
||||
|> angledLine({ angle = 30, length = 15 }, %)
|
||||
|
@ -29,7 +29,7 @@ yLineTo(to: number, sketch: Sketch, tag?: TagDeclarator) -> Sketch
|
||||
### Examples
|
||||
|
||||
```js
|
||||
exampleSketch = startSketchOn("XZ")
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> angledLine({ angle = 50, length = 45 }, %)
|
||||
|> yLineTo(0, %)
|
||||
|
@ -1,5 +1,7 @@
|
||||
import type { Page, Locator } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
import type { Page, Locator, Route, Request } from '@playwright/test'
|
||||
import { expect, TestInfo } from '@playwright/test'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
|
||||
type CmdBarSerialised =
|
||||
| {
|
||||
@ -187,4 +189,71 @@ export class CmdBarFixture {
|
||||
selectOption = (options: Parameters<typeof this.page.getByRole>[1]) => {
|
||||
return this.page.getByRole('option', options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures a snapshot of the request sent to the text-to-cad API endpoint
|
||||
* and saves it to a file named after the current test.
|
||||
*
|
||||
* The snapshot file will be saved in the specified directory with a filename
|
||||
* derived from the test's full path (including describe blocks).
|
||||
*
|
||||
* @param testInfoInOrderToGetTestTitle The TestInfo object from the test context
|
||||
* @param customOutputDir Optional custom directory for the output file
|
||||
*/
|
||||
async captureTextToCadRequestSnapshot(
|
||||
testInfoInOrderToGetTestTitle: TestInfo,
|
||||
customOutputDir = 'e2e/playwright/snapshots/prompt-to-edit'
|
||||
) {
|
||||
// First sanitize each title component individually
|
||||
const sanitizedTitleComponents = [
|
||||
...testInfoInOrderToGetTestTitle.titlePath.slice(0, -1), // Get all parent titles
|
||||
testInfoInOrderToGetTestTitle.title, // Add the test title
|
||||
].map(
|
||||
(component) =>
|
||||
component
|
||||
.replace(/[^a-z0-9]/gi, '-') // Replace non-alphanumeric chars with hyphens
|
||||
.toLowerCase()
|
||||
.replace(/-+/g, '-') // Replace multiple consecutive hyphens with a single one
|
||||
.replace(/^-|-$/g, '') // Remove leading/trailing hyphens
|
||||
)
|
||||
|
||||
// Join the sanitized components with -- as a clear separator
|
||||
const sanitizedTestName = sanitizedTitleComponents.join('--')
|
||||
|
||||
// Create the output path
|
||||
const outputPath = path.join(
|
||||
customOutputDir,
|
||||
`${sanitizedTestName}.snap.json`
|
||||
)
|
||||
|
||||
// Create a handler function that saves request bodies to a file
|
||||
const requestHandler = (route: Route, request: Request) => {
|
||||
try {
|
||||
const requestBody = request.postDataJSON()
|
||||
|
||||
// Ensure directory exists
|
||||
const dir = path.dirname(outputPath)
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true })
|
||||
}
|
||||
|
||||
// Write the request body to the file
|
||||
fs.writeFileSync(outputPath, JSON.stringify(requestBody, null, 2))
|
||||
|
||||
console.log(`Saved text-to-cad API request to: ${outputPath}`)
|
||||
} catch (error) {
|
||||
console.error('Error processing text-to-cad request:', error)
|
||||
}
|
||||
|
||||
// Use void to explicitly mark the promise as ignored
|
||||
void route.continue()
|
||||
}
|
||||
|
||||
// Start monitoring requests
|
||||
await this.page.route('**/ml/text-to-cad/iteration', requestHandler)
|
||||
|
||||
console.log(
|
||||
`Monitoring text-to-cad API requests. Output will be saved to: ${outputPath}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
98
e2e/playwright/prompt-to-edit-snapshot-tests.spec.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import { test, expect } from './zoo-test'
|
||||
/* eslint-disable jest/no-conditional-expect */
|
||||
|
||||
/**
|
||||
* Snapshot Tests for Text-to-CAD API Requests
|
||||
*
|
||||
* These tests are primarily designed to capture the requests sent to the Text-to-CAD API
|
||||
* rather than to verify application behavior. Unlike regular tests, these tests:
|
||||
*
|
||||
* 1. Don't assert much about the application's response or state changes
|
||||
* 2. Focus on setting up specific scenarios and triggering API requests
|
||||
* 3. Use the captureTextToCadRequestSnapshot() method to save request payloads to snapshot files
|
||||
*
|
||||
* The main purpose is to maintain a collection of real-world API request examples that can be:
|
||||
* - Used for regression testing the (AI) API
|
||||
* - Referenced when making changes to the Text-to-CAD integration, particularly the meta-prompts
|
||||
* the frontend adds to the user's prompt
|
||||
*
|
||||
* These tests intentionally don't wait for or verify responses, as we're primarily
|
||||
* interested in capturing the outgoing requests for documentation and analysis.
|
||||
*
|
||||
*/
|
||||
|
||||
const file = `sketch001 = startSketchOn('XZ')
|
||||
profile001 = startProfileAt([57.81, 250.51], sketch001)
|
||||
|> line(end = [121.13, 56.63], tag = $seg02)
|
||||
|> line(end = [83.37, -34.61], tag = $seg01)
|
||||
|> line(end = [19.66, -116.4])
|
||||
|> line(end = [-221.8, -41.69])
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
extrude001 = extrude(profile001, length = 200)
|
||||
sketch002 = startSketchOn('XZ')
|
||||
|> startProfileAt([-73.64, -42.89], %)
|
||||
|> xLine(173.71, %)
|
||||
|> line(end = [-22.12, -94.4])
|
||||
|> xLine(-156.98, %)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
extrude002 = extrude(sketch002, length = 50)
|
||||
sketch003 = startSketchOn('XY')
|
||||
|> startProfileAt([52.92, 157.81], %)
|
||||
|> angledLine([0, 176.4], %, $rectangleSegmentA001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001) - 90,
|
||||
53.4
|
||||
], %, $rectangleSegmentB001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001),
|
||||
-segLen(rectangleSegmentA001)
|
||||
], %, $rectangleSegmentC001)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
extrude003 = extrude(sketch003, length = 20)
|
||||
`
|
||||
|
||||
test(
|
||||
`change colour`,
|
||||
{ tag: '@snapshot' },
|
||||
async ({ context, homePage, cmdBar, editor, page, scene }) => {
|
||||
await context.addInitScript((file) => {
|
||||
localStorage.setItem('persistCode', file)
|
||||
}, file)
|
||||
await homePage.goToModelingScene()
|
||||
await scene.waitForExecutionDone()
|
||||
|
||||
const body1CapCoords = { x: 571, y: 351 }
|
||||
const [clickBody1Cap] = scene.makeMouseHelpers(
|
||||
body1CapCoords.x,
|
||||
body1CapCoords.y
|
||||
)
|
||||
const yellow: [number, number, number] = [179, 179, 131]
|
||||
const submittingToast = page.getByText('Submitting to Text-to-CAD API...')
|
||||
|
||||
await test.step('wait for scene to load select body and check selection came through', async () => {
|
||||
await scene.expectPixelColor([134, 134, 134], body1CapCoords, 15)
|
||||
await clickBody1Cap()
|
||||
await scene.expectPixelColor(yellow, body1CapCoords, 20)
|
||||
await editor.expectState({
|
||||
highlightedCode: '',
|
||||
activeLines: ['|>startProfileAt([-73.64,-42.89],%)'],
|
||||
diagnostics: [],
|
||||
})
|
||||
})
|
||||
|
||||
await test.step('fire off edit prompt', async () => {
|
||||
await cmdBar.captureTextToCadRequestSnapshot(test.info())
|
||||
await cmdBar.openCmdBar('promptToEdit')
|
||||
// being specific about the color with a hex means asserting pixel color is more stable
|
||||
await page
|
||||
.getByTestId('cmd-bar-arg-value')
|
||||
.fill('make this neon green please, use #39FF14')
|
||||
await page.waitForTimeout(100)
|
||||
await cmdBar.progressCmdBar()
|
||||
await expect(submittingToast).toBeVisible()
|
||||
})
|
||||
}
|
||||
)
|
@ -36,7 +36,7 @@ extrude003 = extrude(sketch003, length = 20)
|
||||
`
|
||||
|
||||
test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
|
||||
test.fixme('Check the happy path, for basic changing color', () => {
|
||||
test.describe('Check the happy path, for basic changing color', () => {
|
||||
const cases = [
|
||||
{
|
||||
desc: 'User accepts change',
|
||||
@ -70,7 +70,7 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
|
||||
body1CapCoords.y
|
||||
)
|
||||
const yellow: [number, number, number] = [179, 179, 131]
|
||||
const green: [number, number, number] = [108, 152, 75]
|
||||
const green: [number, number, number] = [128, 194, 88]
|
||||
const notGreen: [number, number, number] = [132, 132, 132]
|
||||
const body2NotGreen: [number, number, number] = [88, 88, 88]
|
||||
const submittingToast = page.getByText(
|
||||
@ -109,7 +109,7 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
|
||||
})
|
||||
|
||||
await test.step('verify initial change', async () => {
|
||||
await scene.expectPixelColor(green, greenCheckCoords, 15)
|
||||
await scene.expectPixelColor(green, greenCheckCoords, 20)
|
||||
await scene.expectPixelColor(body2NotGreen, body2WallCoords, 15)
|
||||
await editor.expectEditor.toContain('appearance(')
|
||||
})
|
||||
@ -142,7 +142,7 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
|
||||
}
|
||||
})
|
||||
|
||||
test(`bad edit prompt`, async ({
|
||||
test('bad edit prompt', async ({
|
||||
context,
|
||||
homePage,
|
||||
cmdBar,
|
||||
@ -195,4 +195,150 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
|
||||
await expect(failToast).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test(`manual code selection rename`, async ({
|
||||
context,
|
||||
homePage,
|
||||
cmdBar,
|
||||
editor,
|
||||
page,
|
||||
scene,
|
||||
}) => {
|
||||
const body1CapCoords = { x: 571, y: 351 }
|
||||
|
||||
await context.addInitScript((file) => {
|
||||
localStorage.setItem('persistCode', file)
|
||||
}, file)
|
||||
await homePage.goToModelingScene()
|
||||
await scene.waitForExecutionDone()
|
||||
|
||||
const submittingToast = page.getByText('Submitting to Text-to-CAD API...')
|
||||
const successToast = page.getByText('Prompt to edit successful')
|
||||
const acceptBtn = page.getByRole('button', { name: 'checkmark Accept' })
|
||||
|
||||
await test.step('wait for scene to load and select code in editor', async () => {
|
||||
// Find and select the text "sketch002" in the editor
|
||||
await editor.selectText('sketch002')
|
||||
|
||||
// Verify the selection was made
|
||||
await editor.expectState({
|
||||
highlightedCode: '',
|
||||
activeLines: ["sketch002 = startSketchOn('XZ')"],
|
||||
diagnostics: [],
|
||||
})
|
||||
})
|
||||
|
||||
await test.step('fire off edit prompt', async () => {
|
||||
await scene.expectPixelColor([134, 134, 134], body1CapCoords, 15)
|
||||
await cmdBar.openCmdBar('promptToEdit')
|
||||
await page
|
||||
.getByTestId('cmd-bar-arg-value')
|
||||
.fill('Please rename to mySketch')
|
||||
await page.waitForTimeout(100)
|
||||
await cmdBar.progressCmdBar()
|
||||
await expect(submittingToast).toBeVisible()
|
||||
await expect(submittingToast).not.toBeVisible({
|
||||
timeout: 2 * 60_000,
|
||||
})
|
||||
await expect(successToast).toBeVisible()
|
||||
})
|
||||
|
||||
await test.step('verify rename change and accept it', async () => {
|
||||
await editor.expectEditor.toContain('mySketch = startSketchOn')
|
||||
await editor.expectEditor.not.toContain('sketch002 = startSketchOn')
|
||||
await editor.expectEditor.toContain(
|
||||
'extrude002 = extrude(mySketch, length = 50)'
|
||||
)
|
||||
|
||||
await acceptBtn.click()
|
||||
await expect(successToast).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test('multiple body selections', async ({
|
||||
context,
|
||||
homePage,
|
||||
cmdBar,
|
||||
editor,
|
||||
page,
|
||||
scene,
|
||||
}) => {
|
||||
const body1CapCoords = { x: 571, y: 351 }
|
||||
const body2WallCoords = { x: 620, y: 152 }
|
||||
const [clickBody1Cap] = scene.makeMouseHelpers(
|
||||
body1CapCoords.x,
|
||||
body1CapCoords.y
|
||||
)
|
||||
const [clickBody2Cap] = scene.makeMouseHelpers(
|
||||
body2WallCoords.x,
|
||||
body2WallCoords.y
|
||||
)
|
||||
const grey: [number, number, number] = [132, 132, 132]
|
||||
|
||||
await context.addInitScript((file) => {
|
||||
localStorage.setItem('persistCode', file)
|
||||
}, file)
|
||||
await homePage.goToModelingScene()
|
||||
await scene.waitForExecutionDone()
|
||||
|
||||
const submittingToast = page.getByText('Submitting to Text-to-CAD API...')
|
||||
const successToast = page.getByText('Prompt to edit successful')
|
||||
const acceptBtn = page.getByRole('button', { name: 'checkmark Accept' })
|
||||
|
||||
await test.step('select multiple bodies and fire prompt', async () => {
|
||||
// Initial color check
|
||||
await scene.expectPixelColor(grey, body1CapCoords, 15)
|
||||
|
||||
// Open command bar first (without selection)
|
||||
await cmdBar.openCmdBar('promptToEdit')
|
||||
|
||||
// Select first body
|
||||
await page.waitForTimeout(100)
|
||||
await clickBody1Cap()
|
||||
|
||||
// Hold shift and select second body
|
||||
await editor.expectState({
|
||||
highlightedCode: '',
|
||||
activeLines: ['|>startProfileAt([-73.64,-42.89],%)'],
|
||||
diagnostics: [],
|
||||
})
|
||||
await page.keyboard.down('Shift')
|
||||
await page.waitForTimeout(100)
|
||||
await clickBody2Cap()
|
||||
await editor.expectState({
|
||||
highlightedCode:
|
||||
'line(end=[121.13,56.63],tag=$seg02)extrude(profile001,length=200)',
|
||||
activeLines: [
|
||||
'|>line(end=[121.13,56.63],tag=$seg02)',
|
||||
'|>startProfileAt([-73.64,-42.89],%)',
|
||||
],
|
||||
diagnostics: [],
|
||||
})
|
||||
await page.keyboard.up('Shift')
|
||||
await page.waitForTimeout(100)
|
||||
await cmdBar.progressCmdBar()
|
||||
|
||||
// Enter prompt and submit
|
||||
await page
|
||||
.getByTestId('cmd-bar-arg-value')
|
||||
.fill('make these neon green please, use #39FF14')
|
||||
await page.waitForTimeout(100)
|
||||
await cmdBar.progressCmdBar()
|
||||
|
||||
// Wait for API response
|
||||
await expect(submittingToast).toBeVisible()
|
||||
await expect(submittingToast).not.toBeVisible({
|
||||
timeout: 2 * 60_000,
|
||||
})
|
||||
await expect(successToast).toBeVisible()
|
||||
})
|
||||
|
||||
await test.step('verify code changed', async () => {
|
||||
await editor.expectEditor.toContain('appearance(')
|
||||
|
||||
// Accept changes
|
||||
await acceptBtn.click()
|
||||
await expect(successToast).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
@ -0,0 +1,33 @@
|
||||
{
|
||||
"original_source_code": "sketch001 = startSketchOn('XZ')\nprofile001 = startProfileAt([57.81, 250.51], sketch001)\n |> line(end = [121.13, 56.63], tag = $seg02)\n |> line(end = [83.37, -34.61], tag = $seg01)\n |> line(end = [19.66, -116.4])\n |> line(end = [-221.8, -41.69])\n |> line(endAbsolute = [profileStartX(%), profileStartY(%)])\n |> close()\nextrude001 = extrude(profile001, length = 200)\nsketch002 = startSketchOn('XZ')\n |> startProfileAt([-73.64, -42.89], %)\n |> xLine(173.71, %)\n |> line(end = [-22.12, -94.4])\n |> xLine(-156.98, %)\n |> line(endAbsolute = [profileStartX(%), profileStartY(%)])\n |> close()\nextrude002 = extrude(sketch002, length = 50)\nsketch003 = startSketchOn('XY')\n |> startProfileAt([52.92, 157.81], %)\n |> angledLine([0, 176.4], %, $rectangleSegmentA001)\n |> angledLine([\n segAng(rectangleSegmentA001) - 90,\n 53.4\n ], %, $rectangleSegmentB001)\n |> angledLine([\n segAng(rectangleSegmentA001),\n -segLen(rectangleSegmentA001)\n ], %, $rectangleSegmentC001)\n |> line(endAbsolute = [profileStartX(%), profileStartY(%)])\n |> close()\nextrude003 = extrude(sketch003, length = 20)\n",
|
||||
"prompt": "make this neon green please, use #39FF14",
|
||||
"source_ranges": [
|
||||
{
|
||||
"prompt": "The users main selection is the end cap of a general-sweep (that is an extrusion, revolve, sweep or loft).\nThe source range most likely refers to \"startProfileAt\" simply because this is the start of the profile that was swept.\nIf you need to operate on this cap, for example for sketching on the face, you can use the special string END i.e. `startSketchOn(someSweepVariable, END)`\nWhen they made this selection they main have intended this surface directly or meant something more general like the sweep body.\nSee later source ranges for more context.",
|
||||
"range": {
|
||||
"start": {
|
||||
"line": 11,
|
||||
"column": 5
|
||||
},
|
||||
"end": {
|
||||
"line": 11,
|
||||
"column": 40
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"prompt": "This is the sweep's source range from the user's main selection of the end cap.",
|
||||
"range": {
|
||||
"start": {
|
||||
"line": 17,
|
||||
"column": 13
|
||||
},
|
||||
"end": {
|
||||
"line": 17,
|
||||
"column": 44
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"kcl_version": "0.2.40"
|
||||
}
|
1
exp
@ -1 +0,0 @@
|
||||
sketch001=startSketchOn('XZ')|>startProfileAt([75.8,317.2],%)//[$startCapTag,$EndCapTag]|>angledLine([0,268.43],%,$rectangleSegmentA001)|>angledLine([segAng(rectangleSegmentA001)-90,217.26],%,$seg01)|>angledLine([segAng(rectangleSegmentA001),-segLen(rectangleSegmentA001)],%,$yo)|>line(endAbsolute=[profileStartX(%),profileStartY(%)],tag=$seg02)|>close()extrude001=extrude(sketch001,length=100)|>chamfer(length=30,tags=[getOppositeEdge(seg01)],tag=$seg03)|>chamfer(length=30,tags=[seg01],tag=$seg04)|>chamfer(length=30,tags=[getNextAdjacentEdge(seg02)],tag=$seg05)|>chamfer(length=30,tags=[getNextAdjacentEdge(yo)],tag=$seg06)sketch004=startSketchOn(extrude001,seg05)profile003=startProfileAt([82.57,322.96],sketch004)|>angledLine([0,11.16],%,$rectangleSegmentA004)|>angledLine([segAng(rectangleSegmentA004)-90,103.07],%)|>angledLine([segAng(rectangleSegmentA004),-segLen(rectangleSegmentA004)],%)|>line(endAbsolute=[profileStartX(%),profileStartY(%)])|>close()sketch003=startSketchOn(extrude001,seg04)profile002=startProfileAt([-209.64,255.28],sketch003)|>angledLine([0,11.56],%,$rectangleSegmentA003)|>angledLine([segAng(rectangleSegmentA003)-90,106.84],%)|>angledLine([segAng(rectangleSegmentA003),-segLen(rectangleSegmentA003)],%)|>line(endAbsolute=[profileStartX(%),profileStartY(%)])|>close()sketch002=startSketchOn(extrude001,seg03)profile001=startProfileAt([205.96,254.59],sketch002)|>angledLine([0,11.39],%,$rectangleSegmentA002)|>angledLine([segAng(rectangleSegmentA002)-90,105.26],%)|>angledLine([segAng(rectangleSegmentA002),-segLen(rectangleSegmentA002)],%)|>line(endAbsolute=[profileStartX(%),profileStartY(%)])|>close()
|
1
got
@ -1 +0,0 @@
|
||||
sketch001=startSketchOn('XZ')|>startProfileAt([75.8,317.2],%)//[$startCapTag,$EndCapTag]|>angledLine([0,268.43],%,$rectangleSegmentA001)|>angledLine([segAng(rectangleSegmentA001)-90,217.26],%,$seg01)|>angledLine([segAng(rectangleSegmentA001),-segLen(rectangleSegmentA001)],%,$yo)|>line(endAbsolute=[profileStartX(%),profileStartY(%)],tag=$seg02)|>close()extrude001=extrude(sketch001,length=100)|>chamfer(length=30,tags=[getOppositeEdge(seg01)],tag=$seg03)|>chamfer(length=30,tags=[seg01],tag=$seg04)|>chamfer(length=30,tags=[getNextAdjacentEdge(seg02)],tag=$seg05)|>chamfer(length=30,tags=[getNextAdjacentEdge(yo)],tag=$seg06)sketch005=startSketchOn(extrude001,seg06)profile004=startProfileAt([-23.43,19.69],sketch005)|>angledLine([0,9.1],%,$rectangleSegmentA005)|>angledLine([segAng(rectangleSegmentA005)-90,84.07],%)|>angledLine([segAng(rectangleSegmentA005),-segLen(rectangleSegmentA005)],%)|>line(endAbsolute=[profileStartX(%),profileStartY(%)])|>close()sketch004=startSketchOn(extrude001,seg05)profile003=startProfileAt([82.57,322.96],sketch004)|>angledLine([0,11.16],%,$rectangleSegmentA004)|>angledLine([segAng(rectangleSegmentA004)-90,103.07],%)|>angledLine([segAng(rectangleSegmentA004),-segLen(rectangleSegmentA004)],%)|>line(endAbsolute=[profileStartX(%),profileStartY(%)])|>close()sketch003=startSketchOn(extrude001,seg04)profile002=startProfileAt([-209.64,255.28],sketch003)|>angledLine([0,11.56],%,$rectangleSegmentA003)|>angledLine([segAng(rectangleSegmentA003)-90,106.84],%)|>angledLine([segAng(rectangleSegmentA003),-segLen(rectangleSegmentA003)],%)|>line(endAbsolute=[profileStartX(%),profileStartY(%)])|>close()sketch002=startSketchOn(extrude001,seg03)profile001=startProfileAt([205.96,254.59],sketch002)|>angledLine([0,11.39],%,$rectangleSegmentA002)|>angledLine([segAng(rectangleSegmentA002)-90,105.26],%)|>angledLine([segAng(rectangleSegmentA002),-segLen(rectangleSegmentA002)],%)|>line(endAbsolute=[profileStartX(%),profileStartY(%)])|>close()
|
@ -91,11 +91,11 @@
|
||||
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages",
|
||||
"fetch:wasm": "./scripts/get-latest-wasm-bundle.sh",
|
||||
"fetch:wasm:windows": "./scripts/get-latest-wasm-bundle.ps1",
|
||||
"fetch:samples": "echo \"Fetching latest KCL samples...\" && curl -o public/kcl-samples-manifest-fallback.json https://raw.githubusercontent.com/KittyCAD/next/manifest.json",
|
||||
"fetch:samples": "echo \"Fetching latest KCL samples...\" && curl -o public/kcl-samples-manifest-fallback.json https://raw.githubusercontent.com/KittyCAD/kcl-samples/next/manifest.json",
|
||||
"build:wasm-dev": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && yarn isomorphic-copy-wasm && yarn fmt",
|
||||
"build:wasm:nocopy": "yarn wasm-prep && cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings",
|
||||
"build:wasm": "yarn build:wasm:nocopy && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt",
|
||||
"build:wasm:windows": "yarn install:wasm-pack:cargo && yarn build:wasm:nocopy && copy src\\wasm-lib\\pkg\\wasm_lib_bg.wasm public && yarn fmt",
|
||||
"build:wasm:windows": "yarn install:wasm-pack:cargo && yarn build:wasm:nocopy && ./scripts/copy-wasm.ps1 && yarn fmt",
|
||||
"remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
|
||||
"wasm-prep": "rimraf src/wasm-lib/pkg && mkdirp src/wasm-lib/pkg && rimraf src/wasm-lib/kcl/bindings",
|
||||
"lint-fix": "eslint --fix --ext .ts --ext .tsx src e2e packages/codemirror-lsp-client/src",
|
||||
|
1
scripts/copy-wasm.ps1
Normal file
@ -0,0 +1 @@
|
||||
copy src\wasm-lib\pkg\wasm_lib_bg.wasm public
|
@ -1,10 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
export VERSION=$(date +'%-y.%-m.%-d')
|
||||
export COMMIT=$(git rev-parse --short HEAD)
|
||||
|
||||
# package.json
|
||||
yarn files:set-version
|
||||
PACKAGE=$(jq '.productName="Zoo Modeling App (Nightly)" | .name="zoo-modeling-app-nightly"' package.json --indent 2)
|
||||
echo "$PACKAGE" > package.json
|
||||
|
||||
@ -14,7 +12,7 @@ yq -i '.appId = "dev.zoo.modeling-app-nightly"' electron-builder.yml
|
||||
yq -i '.nsis.include = "./scripts/installer-nightly.nsh"' electron-builder.yml
|
||||
|
||||
# Release notes
|
||||
echo "Nightly build $VERSION (commit $COMMIT)" > release-notes.md
|
||||
echo "Nightly build (commit $COMMIT)" > release-notes.md
|
||||
|
||||
# icons
|
||||
cp assets/icon-nightly.png assets/icon.png
|
||||
|
@ -24,12 +24,7 @@ import ModelingMachineProvider from 'components/ModelingMachineProvider'
|
||||
import FileMachineProvider from 'components/FileMachineProvider'
|
||||
import { MachineManagerProvider } from 'components/MachineManagerProvider'
|
||||
import { PATHS } from 'lib/paths'
|
||||
import {
|
||||
fileLoader,
|
||||
homeLoader,
|
||||
onboardingRedirectLoader,
|
||||
telemetryLoader,
|
||||
} from 'lib/routeLoaders'
|
||||
import { fileLoader, homeLoader, telemetryLoader } from 'lib/routeLoaders'
|
||||
import LspProvider from 'components/LspProvider'
|
||||
import { KclContextProvider } from 'lang/KclProvider'
|
||||
import { ASK_TO_OPEN_QUERY_PARAM, BROWSER_PROJECT_NAME } from 'lib/constants'
|
||||
@ -113,11 +108,6 @@ const router = createRouter([
|
||||
{
|
||||
id: PATHS.FILE + 'SETTINGS',
|
||||
children: [
|
||||
{
|
||||
loader: onboardingRedirectLoader,
|
||||
index: true,
|
||||
element: <></>,
|
||||
},
|
||||
{
|
||||
path: makeUrlPathRelative(PATHS.SETTINGS),
|
||||
element: <Settings />,
|
||||
|
@ -17,7 +17,9 @@ export const CommandBar = () => {
|
||||
const {
|
||||
context: { selectedCommand, currentArgument, commands },
|
||||
} = commandBarState
|
||||
const isSelectionArgument = currentArgument?.inputType === 'selection'
|
||||
const isSelectionArgument =
|
||||
currentArgument?.inputType === 'selection' ||
|
||||
currentArgument?.inputType === 'selectionMixed'
|
||||
const WrapperComponent = isSelectionArgument ? Popover : Dialog
|
||||
|
||||
// Close the command bar when navigating
|
||||
|
@ -1,6 +1,7 @@
|
||||
import CommandArgOptionInput from './CommandArgOptionInput'
|
||||
import CommandBarBasicInput from './CommandBarBasicInput'
|
||||
import CommandBarSelectionInput from './CommandBarSelectionInput'
|
||||
import CommandBarSelectionMixedInput from './CommandBarSelectionMixedInput'
|
||||
import { CommandArgument } from 'lib/commandTypes'
|
||||
import CommandBarHeader from './CommandBarHeader'
|
||||
import CommandBarKclInput from './CommandBarKclInput'
|
||||
@ -84,6 +85,14 @@ function ArgumentInput({
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
)
|
||||
case 'selectionMixed':
|
||||
return (
|
||||
<CommandBarSelectionMixedInput
|
||||
arg={arg}
|
||||
stepBack={stepBack}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
)
|
||||
case 'kcl':
|
||||
return (
|
||||
<CommandBarKclInput arg={arg} stepBack={stepBack} onSubmit={onSubmit} />
|
||||
|
@ -124,7 +124,8 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
|
||||
<span className="sr-only">: </span>
|
||||
<span data-testid="header-arg-value">
|
||||
{argValue ? (
|
||||
arg.inputType === 'selection' ? (
|
||||
arg.inputType === 'selection' ||
|
||||
arg.inputType === 'selectionMixed' ? (
|
||||
getSelectionTypeDisplayText(argValue as Selections)
|
||||
) : arg.inputType === 'kcl' ? (
|
||||
roundOff(
|
||||
|
135
src/components/CommandBar/CommandBarSelectionMixedInput.tsx
Normal file
@ -0,0 +1,135 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { CommandArgument } from 'lib/commandTypes'
|
||||
import {
|
||||
Selections,
|
||||
canSubmitSelectionArg,
|
||||
getSelectionCountByType,
|
||||
getSelectionTypeDisplayText,
|
||||
} from 'lib/selections'
|
||||
import { useSelector } from '@xstate/react'
|
||||
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||
|
||||
const selectionSelector = (snapshot: any) => snapshot?.context.selectionRanges
|
||||
|
||||
export default function CommandBarSelectionMixedInput({
|
||||
arg,
|
||||
stepBack,
|
||||
onSubmit,
|
||||
}: {
|
||||
arg: CommandArgument<unknown> & { inputType: 'selectionMixed'; name: string }
|
||||
stepBack: () => void
|
||||
onSubmit: (data: unknown) => void
|
||||
}) {
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const commandBarState = useCommandBarState()
|
||||
const [hasSubmitted, setHasSubmitted] = useState(false)
|
||||
const [hasAutoSkipped, setHasAutoSkipped] = useState(false)
|
||||
const selection: Selections = useSelector(arg.machineActor, selectionSelector)
|
||||
|
||||
const selectionsByType = useMemo(() => {
|
||||
return getSelectionCountByType(selection)
|
||||
}, [selection])
|
||||
|
||||
const canSubmitSelection = useMemo<boolean>(() => {
|
||||
if (!selection) return false
|
||||
const isNonZeroRange = selection.graphSelections.some((sel) => {
|
||||
const range = sel.codeRef.range
|
||||
return range[1] - range[0] !== 0 // Non-zero range is always valid
|
||||
})
|
||||
if (isNonZeroRange) return true
|
||||
return canSubmitSelectionArg(selectionsByType, arg)
|
||||
}, [selectionsByType, selection])
|
||||
|
||||
useEffect(() => {
|
||||
inputRef.current?.focus()
|
||||
}, [selection, inputRef])
|
||||
|
||||
// Only auto-skip on initial mount if we have a valid selection
|
||||
// different from the component CommandBarSelectionInput in the the dependency array
|
||||
// is empty
|
||||
useEffect(() => {
|
||||
if (!hasAutoSkipped && canSubmitSelection && arg.skip) {
|
||||
const argValue = commandBarState.context.argumentsToSubmit[arg.name]
|
||||
if (argValue === undefined) {
|
||||
handleSubmit()
|
||||
setHasAutoSkipped(true)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
function handleChange() {
|
||||
inputRef.current?.focus()
|
||||
}
|
||||
|
||||
function handleSubmit(e?: React.FormEvent<HTMLFormElement>) {
|
||||
e?.preventDefault()
|
||||
|
||||
if (!canSubmitSelection) {
|
||||
setHasSubmitted(true)
|
||||
return
|
||||
}
|
||||
|
||||
onSubmit(selection)
|
||||
}
|
||||
|
||||
const isMixedSelection = arg.inputType === 'selectionMixed'
|
||||
const allowNoSelection = isMixedSelection && arg.allowNoSelection
|
||||
const showSceneSelection =
|
||||
isMixedSelection && arg.selectionSource?.allowSceneSelection
|
||||
|
||||
return (
|
||||
<form id="arg-form" onSubmit={handleSubmit}>
|
||||
<label
|
||||
className={
|
||||
'relative flex flex-col mx-4 my-4 ' +
|
||||
(!hasSubmitted || canSubmitSelection || 'text-destroy-50')
|
||||
}
|
||||
>
|
||||
{canSubmitSelection
|
||||
? 'Select objects in the scene'
|
||||
: 'Select code or objects in the scene'}
|
||||
|
||||
{showSceneSelection && (
|
||||
<div className="scene-selection mt-2">
|
||||
<p className="text-sm text-chalkboard-60">
|
||||
Select objects in the scene
|
||||
</p>
|
||||
{/* Scene selection UI will be handled by the parent component */}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{allowNoSelection && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSubmit(null)}
|
||||
className="mt-2 px-4 py-2 rounded border border-chalkboard-30 text-chalkboard-90 dark:text-chalkboard-10 hover:bg-chalkboard-10 dark:hover:bg-chalkboard-90 transition-colors"
|
||||
>
|
||||
Continue without selection
|
||||
</button>
|
||||
)}
|
||||
|
||||
<span data-testid="cmd-bar-arg-name" className="sr-only">
|
||||
{arg.name}
|
||||
</span>
|
||||
<input
|
||||
id="selection"
|
||||
name="selection"
|
||||
ref={inputRef}
|
||||
required
|
||||
data-testid="cmd-bar-arg-value"
|
||||
placeholder="Select an entity with your mouse"
|
||||
className="absolute inset-0 w-full h-full opacity-0 cursor-default"
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Backspace') {
|
||||
stepBack()
|
||||
} else if (event.key === 'Escape') {
|
||||
commandBarActor.send({ type: 'Close' })
|
||||
}
|
||||
}}
|
||||
onChange={handleChange}
|
||||
value={JSON.stringify(selection || {})}
|
||||
/>
|
||||
</label>
|
||||
</form>
|
||||
)
|
||||
}
|
@ -130,6 +130,8 @@ export const FileMachineProvider = ({
|
||||
navigateToFile: ({ context, event }) => {
|
||||
if (event.type !== 'xstate.done.actor.create-and-open-file') return
|
||||
if (event.output && 'name' in event.output) {
|
||||
// TODO: Technically this is not the same as the FileTree Onclick even if they are in the same page
|
||||
// What is "Open file?"
|
||||
commandBarActor.send({ type: 'Close' })
|
||||
navigate(
|
||||
`..${PATHS.FILE}/${encodeURIComponent(
|
||||
|
@ -23,6 +23,8 @@ import { FileEntry } from 'lib/project'
|
||||
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
||||
import { normalizeLineEndings } from 'lib/codeEditor'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { useKclContext } from 'lang/KclProvider'
|
||||
import { kclErrorsByFilename, KCLError } from 'lang/errors'
|
||||
|
||||
function getIndentationCSS(level: number) {
|
||||
return `calc(1rem * ${level + 1})`
|
||||
@ -158,6 +160,7 @@ const FileTreeItem = ({
|
||||
level = 0,
|
||||
treeSelection,
|
||||
setTreeSelection,
|
||||
runtimeErrors,
|
||||
}: {
|
||||
parentDir: FileEntry | undefined
|
||||
project?: IndexLoaderData['project']
|
||||
@ -177,6 +180,7 @@ const FileTreeItem = ({
|
||||
level?: number
|
||||
treeSelection: FileEntry | undefined
|
||||
setTreeSelection: Dispatch<React.SetStateAction<FileEntry | undefined>>
|
||||
runtimeErrors: Map<string, KCLError[]>
|
||||
}) => {
|
||||
const { send: fileSend, context: fileContext } = useFileContext()
|
||||
const { onFileOpen, onFileClose } = useLspContext()
|
||||
@ -186,6 +190,8 @@ const FileTreeItem = ({
|
||||
const isFileOrDirHighlighted = treeSelection?.path === fileOrDir?.path
|
||||
const itemRef = useRef(null)
|
||||
|
||||
const hasRuntimeError = runtimeErrors.has(fileOrDir.path)
|
||||
|
||||
// Since every file or directory gets its own FileTreeItem, we can do this.
|
||||
// Because subtrees only render when they are opened, that means this
|
||||
// only listens when they open. Because this acts like a useEffect, when
|
||||
@ -292,7 +298,7 @@ const FileTreeItem = ({
|
||||
>
|
||||
{!isRenaming ? (
|
||||
<button
|
||||
className="flex gap-1 items-center py-0.5 rounded-none border-none p-0 m-0 text-sm w-full hover:!bg-transparent text-left !text-inherit"
|
||||
className="relative flex gap-1 items-center py-0.5 rounded-none border-none p-0 m-0 text-sm w-full hover:!bg-transparent text-left !text-inherit"
|
||||
style={{ paddingInlineStart: getIndentationCSS(level) }}
|
||||
onClick={(e) => {
|
||||
e.currentTarget.focus()
|
||||
@ -300,11 +306,21 @@ const FileTreeItem = ({
|
||||
}}
|
||||
onKeyUp={handleKeyUp}
|
||||
>
|
||||
{hasRuntimeError && (
|
||||
<p
|
||||
className={
|
||||
'absolute m-0 p-0 bottom-3 left-6 w-3 h-3 flex items-center justify-center text-[9px] font-semibold text-white bg-primary hue-rotate-90 rounded-full border border-chalkboard-10 dark:border-chalkboard-80 z-50 hover:cursor-pointer hover:scale-[2] transition-transform duration-200'
|
||||
}
|
||||
title={`Click to view notifications`}
|
||||
>
|
||||
<span>x</span>
|
||||
</p>
|
||||
)}
|
||||
<CustomIcon
|
||||
name={fileOrDir.name?.endsWith(FILE_EXT) ? 'kcl' : 'file'}
|
||||
className="inline-block w-3 text-current"
|
||||
/>
|
||||
{fileOrDir.name}
|
||||
<span className="pl-1">{fileOrDir.name}</span>
|
||||
</button>
|
||||
) : (
|
||||
<RenameForm
|
||||
@ -414,6 +430,7 @@ const FileTreeItem = ({
|
||||
key={level + '-' + child.path}
|
||||
treeSelection={treeSelection}
|
||||
setTreeSelection={setTreeSelection}
|
||||
runtimeErrors={runtimeErrors}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
@ -660,6 +677,8 @@ export const FileTreeInner = ({
|
||||
const loaderData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
||||
const { send: fileSend, context: fileContext } = useFileContext()
|
||||
const { send: modelingSend } = useModelingContext()
|
||||
const { errors } = useKclContext()
|
||||
const runtimeErrors = kclErrorsByFilename(errors)
|
||||
|
||||
const [lastDirectoryClicked, setLastDirectoryClicked] = useState<
|
||||
FileEntry | undefined
|
||||
@ -769,6 +788,7 @@ export const FileTreeInner = ({
|
||||
key={fileOrDir.path}
|
||||
treeSelection={treeSelection}
|
||||
setTreeSelection={setTreeSelection}
|
||||
runtimeErrors={runtimeErrors}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
@ -18,6 +18,7 @@ import { editorManager } from 'lib/singletons'
|
||||
import { ContextFrom } from 'xstate'
|
||||
import { settingsMachine } from 'machines/settingsMachine'
|
||||
import { FeatureTreePane } from './FeatureTreePane'
|
||||
import { kclErrorsByFilename } from 'lang/errors'
|
||||
|
||||
export type SidebarType =
|
||||
| 'code'
|
||||
@ -30,8 +31,10 @@ export type SidebarType =
|
||||
| 'variables'
|
||||
|
||||
export interface BadgeInfo {
|
||||
value: (props: PaneCallbackProps) => boolean | number
|
||||
value: (props: PaneCallbackProps) => boolean | number | string
|
||||
onClick?: MouseEventHandler<any>
|
||||
className?: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@ -152,6 +155,25 @@ export const sidebarPanes: SidebarPane[] = [
|
||||
},
|
||||
keybinding: 'Shift + F',
|
||||
hide: ({ platform }) => platform === 'web',
|
||||
showBadge: {
|
||||
value: (context) => {
|
||||
// Only compute runtime errors! Compilation errors are not tracked here.
|
||||
const errors = kclErrorsByFilename(context.kclContext.errors)
|
||||
return errors.size > 0 ? 'x' : ''
|
||||
},
|
||||
onClick: (e) => {
|
||||
e.preventDefault()
|
||||
// TODO: When we have generic file open
|
||||
// If badge is pressed
|
||||
// Open the first error in the array of errors
|
||||
// Then scroll to error
|
||||
// Do you automatically open the project files
|
||||
// editorManager.scrollToFirstErrorDiagnosticIfExists()
|
||||
},
|
||||
className:
|
||||
'absolute m-0 p-0 bottom-4 left-4 w-3 h-3 flex items-center justify-center text-[9px] font-semibold text-white bg-primary hue-rotate-90 rounded-full border border-chalkboard-10 dark:border-chalkboard-80 z-50 hover:cursor-pointer hover:scale-[2] transition-transform duration-200',
|
||||
title: 'Project files have runtime errors',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'variables',
|
||||
|
@ -27,8 +27,10 @@ interface ModelingSidebarProps {
|
||||
}
|
||||
|
||||
interface BadgeInfoComputed {
|
||||
value: number | boolean
|
||||
value: number | boolean | string
|
||||
onClick?: MouseEventHandler<any>
|
||||
className?: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
function getPlatformString(): 'web' | 'desktop' {
|
||||
@ -116,6 +118,8 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
||||
acc[pane.id] = {
|
||||
value: pane.showBadge.value(paneCallbackProps),
|
||||
onClick: pane.showBadge.onClick,
|
||||
className: pane.showBadge.className,
|
||||
title: pane.showBadge.title,
|
||||
}
|
||||
}
|
||||
return acc
|
||||
@ -125,6 +129,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
||||
// Clear any hidden panes from the `openPanes` array
|
||||
useEffect(() => {
|
||||
const panesToReset: SidebarType[] = []
|
||||
|
||||
sidebarPanes.forEach((pane) => {
|
||||
if (
|
||||
pane.hide === true ||
|
||||
@ -339,22 +344,31 @@ function ModelingPaneButton({
|
||||
<p
|
||||
id={`${paneConfig.id}-badge`}
|
||||
className={
|
||||
'absolute m-0 p-0 bottom-4 left-4 w-3 h-3 flex items-center justify-center text-[10px] font-semibold text-white bg-primary hue-rotate-90 rounded-full border border-chalkboard-10 dark:border-chalkboard-80 z-50 hover:cursor-pointer hover:scale-[2] transition-transform duration-200'
|
||||
showBadge.className
|
||||
? showBadge.className
|
||||
: 'absolute m-0 p-0 bottom-4 left-4 w-3 h-3 flex items-center justify-center text-[10px] font-semibold text-white bg-primary hue-rotate-90 rounded-full border border-chalkboard-10 dark:border-chalkboard-80 z-50 hover:cursor-pointer hover:scale-[2] transition-transform duration-200'
|
||||
}
|
||||
onClick={showBadge.onClick}
|
||||
title={`Click to view ${showBadge.value} notification${
|
||||
Number(showBadge.value) > 1 ? 's' : ''
|
||||
}`}
|
||||
title={
|
||||
showBadge.title
|
||||
? showBadge.title
|
||||
: `Click to view ${showBadge.value} notification${
|
||||
Number(showBadge.value) > 1 ? 's' : ''
|
||||
}`
|
||||
}
|
||||
>
|
||||
<span className="sr-only"> has </span>
|
||||
{typeof showBadge.value === 'number' ? (
|
||||
{typeof showBadge.value === 'number' ||
|
||||
typeof showBadge.value === 'string' ? (
|
||||
<span>{showBadge.value}</span>
|
||||
) : (
|
||||
<span className="sr-only">a</span>
|
||||
)}
|
||||
<span className="sr-only">
|
||||
notification{Number(showBadge.value) > 1 ? 's' : ''}
|
||||
</span>
|
||||
{typeof showBadge.value === 'number' && (
|
||||
<span className="sr-only">
|
||||
notification{Number(showBadge.value) > 1 ? 's' : ''}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
@ -4,11 +4,12 @@ import {
|
||||
useLocation,
|
||||
useNavigate,
|
||||
useRouteLoaderData,
|
||||
redirect,
|
||||
} from 'react-router-dom'
|
||||
import { PATHS } from 'lib/paths'
|
||||
import { markOnce } from 'lib/performance'
|
||||
import { useAuthNavigation } from 'hooks/useAuthNavigation'
|
||||
import { useAuthState } from 'machines/appMachine'
|
||||
import { useAuthState, useSettings } from 'machines/appMachine'
|
||||
import { IndexLoaderData } from 'lib/types'
|
||||
import { getAppSettingsFilePath } from 'lib/desktop'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
@ -16,6 +17,9 @@ import { trap } from 'lib/trap'
|
||||
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
||||
import { loadAndValidateSettings } from 'lib/settings/settingsUtils'
|
||||
import { settingsActor } from 'machines/appMachine'
|
||||
import makeUrlPathRelative from 'lib/makeUrlPathRelative'
|
||||
import { OnboardingStatus } from 'wasm-lib/kcl/bindings/OnboardingStatus'
|
||||
import { SnapshotFrom } from 'xstate'
|
||||
|
||||
export const RouteProviderContext = createContext({})
|
||||
|
||||
@ -29,6 +33,7 @@ export function RouteProvider({ children }: { children: ReactNode }) {
|
||||
const navigation = useNavigation()
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const settings = useSettings()
|
||||
|
||||
const authState = useAuthState()
|
||||
useEffect(() => {
|
||||
@ -43,6 +48,32 @@ export function RouteProvider({ children }: { children: ReactNode }) {
|
||||
markOnce('code/willLoadHome')
|
||||
} else if (isFile) {
|
||||
markOnce('code/willLoadFile')
|
||||
|
||||
/**
|
||||
* TODO: Move to XState. This block has been moved from routerLoaders
|
||||
* and is borrowing the `isFile` logic from the rest of this
|
||||
* telemetry-focused `useEffect`. Once `appMachine` knows about
|
||||
* the current route and navigation, this can be moved into settingsMachine
|
||||
* to fire as soon as the user settings have been read.
|
||||
*/
|
||||
const onboardingStatus: OnboardingStatus =
|
||||
settings.app.onboardingStatus.current || ''
|
||||
// '' is the initial state, 'completed' and 'dismissed' are the final states
|
||||
const needsToOnboard =
|
||||
onboardingStatus.length === 0 ||
|
||||
!(onboardingStatus === 'completed' || onboardingStatus === 'dismissed')
|
||||
const shouldRedirectToOnboarding = isFile && needsToOnboard
|
||||
|
||||
if (
|
||||
shouldRedirectToOnboarding &&
|
||||
settingsActor.getSnapshot().matches('idle')
|
||||
) {
|
||||
navigate(
|
||||
(first ? location.pathname : navigation.location?.pathname) +
|
||||
PATHS.ONBOARDING.INDEX +
|
||||
onboardingStatus.slice(1)
|
||||
)
|
||||
}
|
||||
}
|
||||
setFirstState(false)
|
||||
}, [navigation])
|
||||
|
@ -293,6 +293,13 @@ export class KclManager {
|
||||
return null
|
||||
}
|
||||
|
||||
// GOTCHA:
|
||||
// When we safeParse this is tied to execution because they clicked a new file to load
|
||||
// Clear all previous errors and logs because they are old since they executed a new file
|
||||
// If we decouple safeParse from execution we need to move this application logic.
|
||||
this._kclErrorsCallBack([])
|
||||
this._logsCallBack([])
|
||||
|
||||
this.addDiagnostics(complilationErrorsToDiagnostics(result.errors))
|
||||
this.addDiagnostics(complilationErrorsToDiagnostics(result.warnings))
|
||||
if (result.errors.length > 0) {
|
||||
|
@ -13,6 +13,7 @@ describe('test kclErrToDiagnostic', () => {
|
||||
operations: [],
|
||||
artifactCommands: [],
|
||||
artifactGraph: defaultArtifactGraph(),
|
||||
filenames: {},
|
||||
},
|
||||
{
|
||||
name: '',
|
||||
@ -23,6 +24,7 @@ describe('test kclErrToDiagnostic', () => {
|
||||
operations: [],
|
||||
artifactCommands: [],
|
||||
artifactGraph: defaultArtifactGraph(),
|
||||
filenames: {},
|
||||
},
|
||||
]
|
||||
const diagnostics = kclErrorsToDiagnostics(errors)
|
||||
|
@ -1,4 +1,7 @@
|
||||
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
|
||||
import {
|
||||
KclError,
|
||||
KclError as RustKclError,
|
||||
} from '../wasm-lib/kcl/bindings/KclError'
|
||||
import { CompilationError } from 'wasm-lib/kcl/bindings/CompilationError'
|
||||
import { Diagnostic as CodeMirrorDiagnostic } from '@codemirror/lint'
|
||||
import { posToOffset } from '@kittycad/codemirror-lsp-client'
|
||||
@ -13,6 +16,7 @@ import {
|
||||
SourceRange,
|
||||
} from 'lang/wasm'
|
||||
import { Operation } from 'wasm-lib/kcl/bindings/Operation'
|
||||
import { ModulePath } from 'wasm-lib/kcl/bindings/ModulePath'
|
||||
|
||||
type ExtractKind<T> = T extends { kind: infer K } ? K : never
|
||||
export class KCLError extends Error {
|
||||
@ -22,6 +26,7 @@ export class KCLError extends Error {
|
||||
operations: Operation[]
|
||||
artifactCommands: ArtifactCommand[]
|
||||
artifactGraph: ArtifactGraph
|
||||
filenames: { [x: number]: ModulePath | undefined }
|
||||
|
||||
constructor(
|
||||
kind: ExtractKind<RustKclError> | 'name',
|
||||
@ -29,7 +34,8 @@ export class KCLError extends Error {
|
||||
sourceRange: SourceRange,
|
||||
operations: Operation[],
|
||||
artifactCommands: ArtifactCommand[],
|
||||
artifactGraph: ArtifactGraph
|
||||
artifactGraph: ArtifactGraph,
|
||||
filenames: { [x: number]: ModulePath | undefined }
|
||||
) {
|
||||
super()
|
||||
this.kind = kind
|
||||
@ -38,6 +44,7 @@ export class KCLError extends Error {
|
||||
this.operations = operations
|
||||
this.artifactCommands = artifactCommands
|
||||
this.artifactGraph = artifactGraph
|
||||
this.filenames = filenames
|
||||
Object.setPrototypeOf(this, KCLError.prototype)
|
||||
}
|
||||
}
|
||||
@ -48,7 +55,8 @@ export class KCLLexicalError extends KCLError {
|
||||
sourceRange: SourceRange,
|
||||
operations: Operation[],
|
||||
artifactCommands: ArtifactCommand[],
|
||||
artifactGraph: ArtifactGraph
|
||||
artifactGraph: ArtifactGraph,
|
||||
filenames: { [x: number]: ModulePath | undefined }
|
||||
) {
|
||||
super(
|
||||
'lexical',
|
||||
@ -56,7 +64,8 @@ export class KCLLexicalError extends KCLError {
|
||||
sourceRange,
|
||||
operations,
|
||||
artifactCommands,
|
||||
artifactGraph
|
||||
artifactGraph,
|
||||
filenames
|
||||
)
|
||||
Object.setPrototypeOf(this, KCLSyntaxError.prototype)
|
||||
}
|
||||
@ -68,7 +77,8 @@ export class KCLInternalError extends KCLError {
|
||||
sourceRange: SourceRange,
|
||||
operations: Operation[],
|
||||
artifactCommands: ArtifactCommand[],
|
||||
artifactGraph: ArtifactGraph
|
||||
artifactGraph: ArtifactGraph,
|
||||
filenames: { [x: number]: ModulePath | undefined }
|
||||
) {
|
||||
super(
|
||||
'internal',
|
||||
@ -76,7 +86,8 @@ export class KCLInternalError extends KCLError {
|
||||
sourceRange,
|
||||
operations,
|
||||
artifactCommands,
|
||||
artifactGraph
|
||||
artifactGraph,
|
||||
filenames
|
||||
)
|
||||
Object.setPrototypeOf(this, KCLSyntaxError.prototype)
|
||||
}
|
||||
@ -88,7 +99,8 @@ export class KCLSyntaxError extends KCLError {
|
||||
sourceRange: SourceRange,
|
||||
operations: Operation[],
|
||||
artifactCommands: ArtifactCommand[],
|
||||
artifactGraph: ArtifactGraph
|
||||
artifactGraph: ArtifactGraph,
|
||||
filenames: { [x: number]: ModulePath | undefined }
|
||||
) {
|
||||
super(
|
||||
'syntax',
|
||||
@ -96,7 +108,8 @@ export class KCLSyntaxError extends KCLError {
|
||||
sourceRange,
|
||||
operations,
|
||||
artifactCommands,
|
||||
artifactGraph
|
||||
artifactGraph,
|
||||
filenames
|
||||
)
|
||||
Object.setPrototypeOf(this, KCLSyntaxError.prototype)
|
||||
}
|
||||
@ -108,7 +121,8 @@ export class KCLSemanticError extends KCLError {
|
||||
sourceRange: SourceRange,
|
||||
operations: Operation[],
|
||||
artifactCommands: ArtifactCommand[],
|
||||
artifactGraph: ArtifactGraph
|
||||
artifactGraph: ArtifactGraph,
|
||||
filenames: { [x: number]: ModulePath | undefined }
|
||||
) {
|
||||
super(
|
||||
'semantic',
|
||||
@ -116,7 +130,8 @@ export class KCLSemanticError extends KCLError {
|
||||
sourceRange,
|
||||
operations,
|
||||
artifactCommands,
|
||||
artifactGraph
|
||||
artifactGraph,
|
||||
filenames
|
||||
)
|
||||
Object.setPrototypeOf(this, KCLSemanticError.prototype)
|
||||
}
|
||||
@ -128,9 +143,18 @@ export class KCLTypeError extends KCLError {
|
||||
sourceRange: SourceRange,
|
||||
operations: Operation[],
|
||||
artifactCommands: ArtifactCommand[],
|
||||
artifactGraph: ArtifactGraph
|
||||
artifactGraph: ArtifactGraph,
|
||||
filenames: { [x: number]: ModulePath | undefined }
|
||||
) {
|
||||
super('type', msg, sourceRange, operations, artifactCommands, artifactGraph)
|
||||
super(
|
||||
'type',
|
||||
msg,
|
||||
sourceRange,
|
||||
operations,
|
||||
artifactCommands,
|
||||
artifactGraph,
|
||||
filenames
|
||||
)
|
||||
Object.setPrototypeOf(this, KCLTypeError.prototype)
|
||||
}
|
||||
}
|
||||
@ -141,7 +165,8 @@ export class KCLUnimplementedError extends KCLError {
|
||||
sourceRange: SourceRange,
|
||||
operations: Operation[],
|
||||
artifactCommands: ArtifactCommand[],
|
||||
artifactGraph: ArtifactGraph
|
||||
artifactGraph: ArtifactGraph,
|
||||
filenames: { [x: number]: ModulePath | undefined }
|
||||
) {
|
||||
super(
|
||||
'unimplemented',
|
||||
@ -149,7 +174,8 @@ export class KCLUnimplementedError extends KCLError {
|
||||
sourceRange,
|
||||
operations,
|
||||
artifactCommands,
|
||||
artifactGraph
|
||||
artifactGraph,
|
||||
filenames
|
||||
)
|
||||
Object.setPrototypeOf(this, KCLUnimplementedError.prototype)
|
||||
}
|
||||
@ -161,7 +187,8 @@ export class KCLUnexpectedError extends KCLError {
|
||||
sourceRange: SourceRange,
|
||||
operations: Operation[],
|
||||
artifactCommands: ArtifactCommand[],
|
||||
artifactGraph: ArtifactGraph
|
||||
artifactGraph: ArtifactGraph,
|
||||
filenames: { [x: number]: ModulePath | undefined }
|
||||
) {
|
||||
super(
|
||||
'unexpected',
|
||||
@ -169,7 +196,8 @@ export class KCLUnexpectedError extends KCLError {
|
||||
sourceRange,
|
||||
operations,
|
||||
artifactCommands,
|
||||
artifactGraph
|
||||
artifactGraph,
|
||||
filenames
|
||||
)
|
||||
Object.setPrototypeOf(this, KCLUnexpectedError.prototype)
|
||||
}
|
||||
@ -181,7 +209,8 @@ export class KCLValueAlreadyDefined extends KCLError {
|
||||
sourceRange: SourceRange,
|
||||
operations: Operation[],
|
||||
artifactCommands: ArtifactCommand[],
|
||||
artifactGraph: ArtifactGraph
|
||||
artifactGraph: ArtifactGraph,
|
||||
filenames: { [x: number]: ModulePath | undefined }
|
||||
) {
|
||||
super(
|
||||
'name',
|
||||
@ -189,7 +218,8 @@ export class KCLValueAlreadyDefined extends KCLError {
|
||||
sourceRange,
|
||||
operations,
|
||||
artifactCommands,
|
||||
artifactGraph
|
||||
artifactGraph,
|
||||
filenames
|
||||
)
|
||||
Object.setPrototypeOf(this, KCLValueAlreadyDefined.prototype)
|
||||
}
|
||||
@ -201,7 +231,8 @@ export class KCLUndefinedValueError extends KCLError {
|
||||
sourceRange: SourceRange,
|
||||
operations: Operation[],
|
||||
artifactCommands: ArtifactCommand[],
|
||||
artifactGraph: ArtifactGraph
|
||||
artifactGraph: ArtifactGraph,
|
||||
filenames: { [x: number]: ModulePath | undefined }
|
||||
) {
|
||||
super(
|
||||
'name',
|
||||
@ -209,7 +240,8 @@ export class KCLUndefinedValueError extends KCLError {
|
||||
sourceRange,
|
||||
operations,
|
||||
artifactCommands,
|
||||
artifactGraph
|
||||
artifactGraph,
|
||||
filenames
|
||||
)
|
||||
Object.setPrototypeOf(this, KCLUndefinedValueError.prototype)
|
||||
}
|
||||
@ -232,7 +264,8 @@ export function lspDiagnosticsToKclErrors(
|
||||
[posToOffset(doc, range.start)!, posToOffset(doc, range.end)!, 0],
|
||||
[],
|
||||
[],
|
||||
defaultArtifactGraph()
|
||||
defaultArtifactGraph(),
|
||||
{}
|
||||
)
|
||||
)
|
||||
.sort((a, b) => {
|
||||
@ -304,3 +337,34 @@ export function complilationErrorsToDiagnostics(
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Create an array of KCL Errors with a new formatting to
|
||||
// easily map SourceRange of an error to the filename to display in the
|
||||
// side bar UI. This is to indicate an error in an imported file, it isn't
|
||||
// the specific code mirror error interface.
|
||||
export function kclErrorsByFilename(
|
||||
errors: KCLError[]
|
||||
): Map<string, KCLError[]> {
|
||||
const fileNameToError: Map<string, KCLError[]> = new Map()
|
||||
errors.forEach((error: KCLError) => {
|
||||
const filenames = error.filenames
|
||||
const sourceRange: SourceRange = error.sourceRange
|
||||
const fileIndex = sourceRange[2]
|
||||
const modulePath: ModulePath | undefined = filenames[fileIndex]
|
||||
if (modulePath && modulePath.type === 'Local') {
|
||||
let localPath = modulePath.value
|
||||
if (localPath) {
|
||||
// Build up an array of errors per file name
|
||||
const value = fileNameToError.get(localPath)
|
||||
if (!value) {
|
||||
fileNameToError.set(localPath, [error])
|
||||
} else {
|
||||
value.push(error)
|
||||
fileNameToError.set(localPath, [error])
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return fileNameToError
|
||||
}
|
||||
|
@ -511,7 +511,8 @@ const theExtrude = startSketchOn('XY')
|
||||
topLevelRange(129, 135),
|
||||
[],
|
||||
[],
|
||||
defaultArtifactGraph()
|
||||
defaultArtifactGraph(),
|
||||
{}
|
||||
)
|
||||
)
|
||||
})
|
||||
|
@ -116,7 +116,11 @@ const runGetPathToExtrudeForSegmentSelectionTest = async (
|
||||
}
|
||||
if (!extrudeInSketchPipe) {
|
||||
const init = expectedExtrudeNode.init
|
||||
if (init.type !== 'CallExpression' && init.type !== 'PipeExpression') {
|
||||
if (
|
||||
init.type !== 'CallExpression' &&
|
||||
init.type !== 'CallExpressionKw' &&
|
||||
init.type !== 'PipeExpression'
|
||||
) {
|
||||
return new Error(
|
||||
'Expected extrude expression is not a CallExpression or PipeExpression'
|
||||
)
|
||||
@ -129,25 +133,33 @@ const runGetPathToExtrudeForSegmentSelectionTest = async (
|
||||
// ast
|
||||
const ast = assertParse(code)
|
||||
|
||||
// selection
|
||||
// range
|
||||
const segmentRange = topLevelRange(
|
||||
code.indexOf(selectedSegmentSnippet),
|
||||
code.indexOf(selectedSegmentSnippet) + selectedSegmentSnippet.length
|
||||
)
|
||||
const selection: Selection = {
|
||||
codeRef: codeRefFromRange(segmentRange, ast),
|
||||
}
|
||||
|
||||
// executeAst and artifactGraph
|
||||
await kclManager.executeAst({ ast })
|
||||
const artifactGraph = engineCommandManager.artifactGraph
|
||||
|
||||
// find artifact
|
||||
const maybeArtifact = [...artifactGraph].find(([, artifact]) => {
|
||||
if (!('codeRef' in artifact && artifact.codeRef)) return false
|
||||
return isOverlap(artifact.codeRef.range, segmentRange)
|
||||
})
|
||||
|
||||
// build selection
|
||||
const selection: Selection = {
|
||||
codeRef: codeRefFromRange(segmentRange, ast),
|
||||
artifact: maybeArtifact ? maybeArtifact[1] : undefined,
|
||||
}
|
||||
|
||||
// get extrude expression
|
||||
const pathResult = getPathToExtrudeForSegmentSelection(
|
||||
ast,
|
||||
selection,
|
||||
artifactGraph,
|
||||
dependencies
|
||||
artifactGraph
|
||||
)
|
||||
if (err(pathResult)) return pathResult
|
||||
const { pathToExtrudeNode } = pathResult
|
||||
@ -234,6 +246,56 @@ extrude003 = extrude(sketch003, length = -15)`
|
||||
expectedExtrudeSnippet
|
||||
)
|
||||
})
|
||||
it('should return the correct paths for a (piped) extrude based on the other body (face)', async () => {
|
||||
const code = `sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-25, -25], %)
|
||||
|> yLine(50, %)
|
||||
|> xLine(50, %)
|
||||
|> yLine(-50, %)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
|> extrude(length = 50)
|
||||
sketch002 = startSketchOn(sketch001, 'END')
|
||||
|> startProfileAt([-15, -15], %)
|
||||
|> yLine(30, %)
|
||||
|> xLine(30, %)
|
||||
|> yLine(-30, %)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
|> extrude(length = 30)`
|
||||
const selectedSegmentSnippet = `xLine(30, %)`
|
||||
const expectedExtrudeSnippet = `extrude(length = 30)`
|
||||
await runGetPathToExtrudeForSegmentSelectionTest(
|
||||
code,
|
||||
selectedSegmentSnippet,
|
||||
expectedExtrudeSnippet
|
||||
)
|
||||
})
|
||||
it('should return the correct paths for a (non-piped) extrude based on the other body (face)', async () => {
|
||||
const code = `sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-25, -25], %)
|
||||
|> yLine(50, %)
|
||||
|> xLine(50, %)
|
||||
|> yLine(-50, %)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
extrude001 = extrude(sketch001, length = 50)
|
||||
sketch002 = startSketchOn(extrude001, 'END')
|
||||
|> startProfileAt([-15, -15], %)
|
||||
|> yLine(30, %)
|
||||
|> xLine(30, %)
|
||||
|> yLine(-30, %)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
extrude002 = extrude(sketch002, length = 30)`
|
||||
const selectedSegmentSnippet = `xLine(30, %)`
|
||||
const expectedExtrudeSnippet = `extrude002 = extrude(sketch002, length = 30)`
|
||||
await runGetPathToExtrudeForSegmentSelectionTest(
|
||||
code,
|
||||
selectedSegmentSnippet,
|
||||
expectedExtrudeSnippet
|
||||
)
|
||||
})
|
||||
it('should not return any path for missing extrusion', async () => {
|
||||
const code = `sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-30, 30], %)
|
||||
|
@ -10,7 +10,6 @@ import {
|
||||
Program,
|
||||
VariableDeclaration,
|
||||
VariableDeclarator,
|
||||
sketchFromKclValue,
|
||||
} from '../wasm'
|
||||
import {
|
||||
createCallExpressionStdLib,
|
||||
@ -35,11 +34,11 @@ import {
|
||||
sketchLineHelperMap,
|
||||
sketchLineHelperMapKw,
|
||||
} from '../std/sketch'
|
||||
import { err, trap } from 'lib/trap'
|
||||
import { err } from 'lib/trap'
|
||||
import { Selection, Selections } from 'lib/selections'
|
||||
import { KclCommandValue } from 'lib/commandTypes'
|
||||
import { isArray } from 'lib/utils'
|
||||
import { Artifact, getSweepFromSuspectedPath } from 'lang/std/artifactGraph'
|
||||
import { Artifact, getSweepArtifactFromSelection } from 'lang/std/artifactGraph'
|
||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
import { findKwArg } from 'lang/util'
|
||||
import { KclManager } from 'lang/KclSingleton'
|
||||
@ -121,8 +120,7 @@ export function modifyAstWithEdgeTreatmentAndTag(
|
||||
const result = getPathToExtrudeForSegmentSelection(
|
||||
clonedAstForGetExtrude,
|
||||
selection,
|
||||
artifactGraph,
|
||||
dependencies
|
||||
artifactGraph
|
||||
)
|
||||
if (err(result)) return result
|
||||
const { pathToSegmentNode, pathToExtrudeNode } = result
|
||||
@ -279,39 +277,19 @@ function insertParametersIntoAst(
|
||||
export function getPathToExtrudeForSegmentSelection(
|
||||
ast: Program,
|
||||
selection: Selection,
|
||||
artifactGraph: ArtifactGraph,
|
||||
dependencies: {
|
||||
kclManager: KclManager
|
||||
engineCommandManager: EngineCommandManager
|
||||
editorManager: EditorManager
|
||||
codeManager: CodeManager
|
||||
}
|
||||
artifactGraph: ArtifactGraph
|
||||
): { pathToSegmentNode: PathToNode; pathToExtrudeNode: PathToNode } | Error {
|
||||
const pathToSegmentNode = getNodePathFromSourceRange(
|
||||
ast,
|
||||
selection.codeRef?.range
|
||||
)
|
||||
|
||||
const varDecNode = getNodeFromPath<VariableDeclaration>(
|
||||
ast,
|
||||
pathToSegmentNode,
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (err(varDecNode)) return varDecNode
|
||||
const sketchVar = varDecNode.node.declaration.id.name
|
||||
|
||||
const sketch = sketchFromKclValue(
|
||||
dependencies.kclManager.variables[sketchVar],
|
||||
sketchVar
|
||||
)
|
||||
if (trap(sketch)) return sketch
|
||||
|
||||
const extrusion = getSweepFromSuspectedPath(sketch.id, artifactGraph)
|
||||
if (err(extrusion)) return extrusion
|
||||
const sweepArtifact = getSweepArtifactFromSelection(selection, artifactGraph)
|
||||
if (err(sweepArtifact)) return sweepArtifact
|
||||
|
||||
const pathToExtrudeNode = getNodePathFromSourceRange(
|
||||
ast,
|
||||
extrusion.codeRef.range
|
||||
sweepArtifact.codeRef.range
|
||||
)
|
||||
if (err(pathToExtrudeNode)) return pathToExtrudeNode
|
||||
|
||||
|
@ -13,36 +13,23 @@ import {
|
||||
createLiteral,
|
||||
createIdentifier,
|
||||
findUniqueName,
|
||||
createCallExpressionStdLib,
|
||||
createObjectExpression,
|
||||
createArrayExpression,
|
||||
createVariableDeclaration,
|
||||
createCallExpressionStdLibKw,
|
||||
createLabeledArg,
|
||||
} from 'lang/modifyAst'
|
||||
import { KCL_DEFAULT_CONSTANT_PREFIXES } from 'lib/constants'
|
||||
import { KclManager } from 'lang/KclSingleton'
|
||||
import { EngineCommandManager } from 'lang/std/engineConnection'
|
||||
import EditorManager from 'editor/manager'
|
||||
import CodeManager from 'lang/codeManager'
|
||||
|
||||
export function addShell({
|
||||
node,
|
||||
selection,
|
||||
artifactGraph,
|
||||
thickness,
|
||||
dependencies,
|
||||
}: {
|
||||
node: Node<Program>
|
||||
selection: Selections
|
||||
artifactGraph: ArtifactGraph
|
||||
thickness: Expr
|
||||
dependencies: {
|
||||
kclManager: KclManager
|
||||
engineCommandManager: EngineCommandManager
|
||||
editorManager: EditorManager
|
||||
codeManager: CodeManager
|
||||
}
|
||||
}): Error | { modifiedAst: Node<Program>; pathToNode: PathToNode } {
|
||||
const modifiedAst = structuredClone(node)
|
||||
|
||||
@ -55,8 +42,7 @@ export function addShell({
|
||||
const extrudeLookupResult = getPathToExtrudeForSegmentSelection(
|
||||
clonedAstForGetExtrude,
|
||||
graphSelection,
|
||||
artifactGraph,
|
||||
dependencies
|
||||
artifactGraph
|
||||
)
|
||||
if (err(extrudeLookupResult)) {
|
||||
return new Error("Couldn't find extrude")
|
||||
|
@ -18,6 +18,7 @@ import {
|
||||
} from 'lang/wasm'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
import { Selection } from 'lib/selections'
|
||||
import { err } from 'lib/trap'
|
||||
import { Cap, Plane, Wall } from 'wasm-lib/kcl/bindings/Artifact'
|
||||
import { CapSubType } from 'wasm-lib/kcl/bindings/Artifact'
|
||||
@ -79,7 +80,7 @@ interface SegmentArtifactRich extends BaseArtifact {
|
||||
|
||||
interface SweepArtifactRich extends BaseArtifact {
|
||||
type: 'sweep'
|
||||
subType: 'extrusion' | 'revolve' | 'loft' | 'sweep'
|
||||
subType: 'extrusion' | 'revolve' | 'revolveAboutEdge' | 'loft' | 'sweep'
|
||||
path: PathArtifact
|
||||
surfaces: Array<WallArtifact | CapArtifact>
|
||||
edges: Array<SweepEdge>
|
||||
@ -455,6 +456,47 @@ export function getSweepFromSuspectedPath(
|
||||
)
|
||||
}
|
||||
|
||||
export function getSweepArtifactFromSelection(
|
||||
selection: Selection,
|
||||
artifactGraph: ArtifactGraph
|
||||
): SweepArtifact | Error {
|
||||
let sweepArtifact: Artifact | null = null
|
||||
if (selection.artifact?.type === 'sweepEdge') {
|
||||
const _artifact = getArtifactOfTypes(
|
||||
{ key: selection.artifact.sweepId, types: ['sweep'] },
|
||||
artifactGraph
|
||||
)
|
||||
if (err(_artifact)) return _artifact
|
||||
sweepArtifact = _artifact
|
||||
} else if (selection.artifact?.type === 'segment') {
|
||||
const _pathArtifact = getArtifactOfTypes(
|
||||
{ key: selection.artifact.pathId, types: ['path'] },
|
||||
artifactGraph
|
||||
)
|
||||
if (err(_pathArtifact)) return _pathArtifact
|
||||
if (!_pathArtifact.sweepId) return new Error('Path does not have a sweepId')
|
||||
const _artifact = getArtifactOfTypes(
|
||||
{ key: _pathArtifact.sweepId, types: ['sweep'] },
|
||||
artifactGraph
|
||||
)
|
||||
if (err(_artifact)) return _artifact
|
||||
sweepArtifact = _artifact
|
||||
} else if (
|
||||
selection.artifact?.type === 'cap' ||
|
||||
selection.artifact?.type === 'wall'
|
||||
) {
|
||||
const _artifact = getArtifactOfTypes(
|
||||
{ key: selection.artifact.sweepId, types: ['sweep'] },
|
||||
artifactGraph
|
||||
)
|
||||
if (err(_artifact)) return _artifact
|
||||
sweepArtifact = _artifact
|
||||
}
|
||||
if (!sweepArtifact) return new Error('No sweep artifact found')
|
||||
|
||||
return sweepArtifact
|
||||
}
|
||||
|
||||
export function getCodeRefsByArtifactId(
|
||||
id: string,
|
||||
artifactGraph: ArtifactGraph
|
||||
|
@ -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)
|
||||
|
@ -666,7 +666,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
icon: 'chat',
|
||||
args: {
|
||||
selection: {
|
||||
inputType: 'selection',
|
||||
inputType: 'selectionMixed',
|
||||
selectionTypes: [
|
||||
'solid2d',
|
||||
'segment',
|
||||
@ -678,6 +678,10 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
],
|
||||
multiple: true,
|
||||
required: true,
|
||||
selectionSource: {
|
||||
allowSceneSelection: true,
|
||||
allowCodeSelection: true,
|
||||
},
|
||||
skip: true,
|
||||
},
|
||||
prompt: {
|
||||
|
@ -16,6 +16,7 @@ const INPUT_TYPES = [
|
||||
'text',
|
||||
'kcl',
|
||||
'selection',
|
||||
'selectionMixed',
|
||||
'boolean',
|
||||
] as const
|
||||
export interface KclExpression {
|
||||
@ -156,6 +157,23 @@ export type CommandArgumentConfig<
|
||||
context: CommandBarContext
|
||||
}) => Promise<boolean | string>
|
||||
}
|
||||
| {
|
||||
inputType: 'selectionMixed'
|
||||
selectionTypes: Artifact['type'][]
|
||||
multiple: boolean
|
||||
allowNoSelection?: boolean
|
||||
validation?: ({
|
||||
data,
|
||||
context,
|
||||
}: {
|
||||
data: any
|
||||
context: CommandBarContext
|
||||
}) => Promise<boolean | string>
|
||||
selectionSource?: {
|
||||
allowSceneSelection?: boolean
|
||||
allowCodeSelection?: boolean
|
||||
}
|
||||
}
|
||||
| {
|
||||
inputType: 'kcl'
|
||||
createVariableByDefault?: boolean
|
||||
@ -252,6 +270,23 @@ export type CommandArgument<
|
||||
context: CommandBarContext
|
||||
}) => Promise<boolean | string>
|
||||
}
|
||||
| {
|
||||
inputType: 'selectionMixed'
|
||||
selectionTypes: Artifact['type'][]
|
||||
multiple: boolean
|
||||
allowNoSelection?: boolean
|
||||
validation?: ({
|
||||
data,
|
||||
context,
|
||||
}: {
|
||||
data: any
|
||||
context: CommandBarContext
|
||||
}) => Promise<boolean | string>
|
||||
selectionSource?: {
|
||||
allowSceneSelection?: boolean
|
||||
allowCodeSelection?: boolean
|
||||
}
|
||||
}
|
||||
| {
|
||||
inputType: 'kcl'
|
||||
createVariableByDefault?: boolean
|
||||
|
@ -187,6 +187,16 @@ export function buildCommandArgument<
|
||||
selectionTypes: arg.selectionTypes,
|
||||
validation: arg.validation,
|
||||
} satisfies CommandArgument<O, T> & { inputType: 'selection' }
|
||||
} else if (arg.inputType === 'selectionMixed') {
|
||||
return {
|
||||
inputType: arg.inputType,
|
||||
...baseCommandArgument,
|
||||
multiple: arg.multiple,
|
||||
selectionTypes: arg.selectionTypes,
|
||||
validation: arg.validation,
|
||||
allowNoSelection: arg.allowNoSelection,
|
||||
selectionSource: arg.selectionSource,
|
||||
} satisfies CommandArgument<O, T> & { inputType: 'selectionMixed' }
|
||||
} else if (arg.inputType === 'kcl') {
|
||||
return {
|
||||
inputType: arg.inputType,
|
||||
|
@ -43,15 +43,33 @@ export async function submitPromptToEditToQueue({
|
||||
projectName,
|
||||
}: {
|
||||
prompt: string
|
||||
selections: Selections
|
||||
selections: Selections | null
|
||||
code: string
|
||||
projectName: string
|
||||
token?: string
|
||||
artifactGraph: ArtifactGraph
|
||||
}): Promise<Models['TextToCadIteration_type'] | Error> {
|
||||
// If no selection, use whole file
|
||||
if (selections === null) {
|
||||
const body: Models['TextToCadIterationBody_type'] = {
|
||||
original_source_code: code,
|
||||
prompt,
|
||||
source_ranges: [], // Empty ranges indicates whole file
|
||||
project_name:
|
||||
projectName !== '' && projectName !== 'browser'
|
||||
? projectName
|
||||
: undefined,
|
||||
kcl_version: kclManager.kclVersion,
|
||||
}
|
||||
return submitToApi(body, token)
|
||||
}
|
||||
|
||||
// Handle manual code selections and artifact selections differently
|
||||
const ranges: Models['TextToCadIterationBody_type']['source_ranges'] =
|
||||
selections.graphSelections.flatMap((selection) => {
|
||||
const artifact = selection.artifact
|
||||
|
||||
// For artifact selections, add context
|
||||
const prompts: Models['TextToCadIterationBody_type']['source_ranges'] = []
|
||||
|
||||
if (artifact?.type === 'cap') {
|
||||
@ -153,8 +171,17 @@ See later source ranges for more context. about the sweep`,
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!artifact) {
|
||||
// manually selected code is more likely to not have an artifact
|
||||
// an example might be highlighting the variable name only in a variable declaration
|
||||
prompts.push({
|
||||
prompt: '',
|
||||
range: convertAppRangeToApiRange(selection.codeRef.range, code),
|
||||
})
|
||||
}
|
||||
return prompts
|
||||
})
|
||||
|
||||
const body: Models['TextToCadIterationBody_type'] = {
|
||||
original_source_code: code,
|
||||
prompt,
|
||||
@ -163,6 +190,15 @@ See later source ranges for more context. about the sweep`,
|
||||
projectName !== '' && projectName !== 'browser' ? projectName : undefined,
|
||||
kcl_version: kclManager.kclVersion,
|
||||
}
|
||||
|
||||
return submitToApi(body, token)
|
||||
}
|
||||
|
||||
// Helper function to handle API submission
|
||||
async function submitToApi(
|
||||
body: Models['TextToCadIterationBody_type'],
|
||||
token?: string
|
||||
): Promise<Models['TextToCadIteration_type'] | Error> {
|
||||
const url = VITE_KC_API_BASE_URL + '/ml/text-to-cad/iteration'
|
||||
const data: Models['TextToCadIteration_type'] | Error =
|
||||
await crossPlatformFetch(
|
||||
|
@ -23,30 +23,6 @@ export const telemetryLoader: LoaderFunction = async ({
|
||||
return null
|
||||
}
|
||||
|
||||
// Redirect users to the appropriate onboarding page if they haven't completed it
|
||||
export const onboardingRedirectLoader: ActionFunction = async (args) => {
|
||||
const settings = getSettings()
|
||||
const onboardingStatus: OnboardingStatus =
|
||||
settings.app.onboardingStatus.current || ''
|
||||
const notEnRouteToOnboarding = !args.request.url.includes(
|
||||
PATHS.ONBOARDING.INDEX
|
||||
)
|
||||
// '' is the initial state, 'completed' and 'dismissed' are the final states
|
||||
const hasValidOnboardingStatus =
|
||||
onboardingStatus.length === 0 ||
|
||||
!(onboardingStatus === 'completed' || onboardingStatus === 'dismissed')
|
||||
const shouldRedirectToOnboarding =
|
||||
notEnRouteToOnboarding && hasValidOnboardingStatus
|
||||
|
||||
if (shouldRedirectToOnboarding) {
|
||||
return redirect(
|
||||
makeUrlPathRelative(PATHS.ONBOARDING.INDEX) + onboardingStatus.slice(1)
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export const fileLoader: LoaderFunction = async (
|
||||
routerData
|
||||
): Promise<FileLoaderData | Response> => {
|
||||
|
@ -481,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' &&
|
||||
|