Compare commits

..

3 Commits

206 changed files with 939 additions and 33678 deletions

View File

@ -1,4 +1,3 @@
src/wasm-lib/*
*.typegen.ts
packages/codemirror-lsp-client/dist/*
e2e/playwright/snapshots/prompt-to-edit/*

View File

@ -7,11 +7,14 @@ on:
- main
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
- 'nightly-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)
env:
IS_RELEASE: ${{ github.ref_type == 'tag' && startsWith(github.ref_name, 'v') }}
IS_NIGHTLY: ${{ github.ref_type == 'tag' && startsWith(github.ref_name, 'nightly-v') }}
IS_RELEASE: ${{ github.ref_type == 'tag' }}
IS_NIGHTLY: ${{ github.event_name == 'schedule' }}
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
@ -29,7 +32,6 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
@ -50,10 +52,7 @@ jobs:
- name: Set nightly version, product name, release notes, and icons
if: ${{ env.IS_NIGHTLY == 'true' }}
run: |
export VERSION=${GITHUB_REF_NAME#nightly-v}
yarn files:set-version
yarn files:flip-to-nightly
run: yarn files:flip-to-nightly
- name: Set release version
if: ${{ env.IS_RELEASE == 'true' }}
@ -124,11 +123,9 @@ jobs:
cp prepared-files/assets/icon.ico assets/icon.ico
cp prepared-files/assets/icon.png assets/icon.png
- name: Sync node version and setup cache
uses: actions/setup-node@v4
- 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
@ -273,7 +270,7 @@ jobs:
runs-on: ubuntu-22.04
permissions:
contents: write
if: ${{ github.ref_type == 'tag' }}
if: ${{ github.ref_type == 'tag' || github.event_name == 'schedule' }}
env:
VERSION_NO_V: ${{ needs.prepare-files.outputs.version }}
VERSION: ${{ format('v{0}', needs.prepare-files.outputs.version) }}
@ -330,8 +327,8 @@ jobs:
env:
NOTES: ${{ needs.prepare-files.outputs.notes }}
PUB_DATE: ${{ github.event.repository.updated_at }}
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' }}
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' }}
run: |
RELEASE_DIR=https://${WEBSITE_DIR}
jq --null-input \
@ -414,3 +411,14 @@ jobs:
- name: Invalidate bucket cache on latest*.yml and last_download.json files
if: ${{ env.IS_NIGHTLY == 'true' }}
run: yarn files:invalidate-bucket:nightly
- name: Tag nightly commit
if: ${{ env.IS_NIGHTLY == 'true' }}
uses: actions/github-script@v7
with:
script: |
const { VERSION } = process.env
const { owner, repo } = context.repo
const { sha } = context
const ref = `refs/tags/nightly-${VERSION}`
github.rest.git.createRef({ owner, repo, sha, ref })

View File

@ -142,7 +142,7 @@ jobs:
# TODO: break this in its own job, for now it's not slowing down the overall execution as ubuntu is the quickest,
# but we could do better. This forces a large 1/1 shard of all 20 snapshot tests that runs in about 3 minutes.
run: |
PLATFORM=web yarn playwright test --config=playwright.config.ts --retries="3" --update-snapshots --grep=@snapshot --trace=on --shard=1/1
PLATFORM=web yarn playwright test --config=playwright.config.ts --retries="3" --update-snapshots --grep=@snapshot --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-snapshots-${{ matrix.os }}-snapshot-${{ matrix.shardIndex }}-${{ github.sha }}
name: playwright-report-${{ 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 e2e/playwright/snapshots
git add e2e/playwright/snapshot-tests.spec.ts-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 e2e/playwright/snapshots
git add e2e/playwright/snapshot-tests.spec.ts-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

View File

@ -1,39 +0,0 @@
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
View File

@ -24,7 +24,7 @@ yarn-debug.log*
yarn-error.log*
.idea
.vscode
# .vscode
.helix
src/wasm-lib/.idea
src/wasm-lib/.vscode
@ -71,4 +71,3 @@ venv
# electron
out/
*.snap.new

View File

@ -10,7 +10,6 @@ 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 Normal file
View File

@ -0,0 +1,5 @@
{
"rust-analyzer.linkedProjects": [
"src/wasm-lib/Cargo.toml"
]
}

View File

@ -1,5 +1,5 @@
---
title: "std::YZ"
title: "HALF_TURN"
excerpt: ""
layout: manual
---
@ -9,7 +9,7 @@ layout: manual
```js
std::YZ
HALF_TURN: number(deg) = 180deg
```

15
docs/kcl/QUARTER_TURN.md Normal file
View File

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

View File

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

View File

@ -1,5 +1,5 @@
---
title: "std::XY"
title: "ZERO"
excerpt: ""
layout: manual
---
@ -9,7 +9,7 @@ layout: manual
```js
std::XY
ZERO: number = 0
```

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

32
docs/kcl/const_E.md Normal file

File diff suppressed because one or more lines are too long

View File

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

28
docs/kcl/const_PI.md Normal file

File diff suppressed because one or more lines are too long

View File

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

32
docs/kcl/const_TAU.md Normal file

File diff suppressed because one or more lines are too long

View File

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

View File

@ -1,5 +1,5 @@
---
title: "std::XZ"
title: "ZERO"
excerpt: ""
layout: manual
---
@ -9,7 +9,7 @@ layout: manual
```js
std::XZ
ZERO: number = 0
```

File diff suppressed because one or more lines are too long

View File

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

View File

@ -6,22 +6,13 @@ layout: manual
## Table of Contents
### Language
* [`Types`](kcl/types)
* [`Modules`](kcl/modules)
* [`Settings`](kcl/settings)
* [`Known Issues`](kcl/known-issues)
### Standard library
* [Types](kcl/types)
* [Modules](kcl/modules)
* [Known Issues](kcl/KNOWN-ISSUES)
* **`std`**
* [`HALF_TURN`](kcl/const_std-HALF_TURN)
* [`QUARTER_TURN`](kcl/const_std-QUARTER_TURN)
* [`THREE_QUARTER_TURN`](kcl/const_std-THREE_QUARTER_TURN)
* [`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)
@ -99,9 +90,7 @@ 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)
@ -121,7 +110,6 @@ layout: manual
* [`tangentialArcToRelative`](kcl/tangentialArcToRelative)
* [`toDegrees`](kcl/toDegrees)
* [`toRadians`](kcl/toRadians)
* [`translate`](kcl/translate)
* [`xLine`](kcl/xLine)
* [`xLineTo`](kcl/xLineTo)
* [`yLine`](kcl/yLine)

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,30 +0,0 @@
---
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).

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

@ -74,15 +74,18 @@ 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:
KCL's early drafts used positional arguments, but we now use keyword arguments. If you declare a
function like this:
```
// 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)
```
@ -108,14 +111,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)
@ -130,12 +133,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.
@ -144,12 +147,12 @@ This helps keep your code neat and avoid unnecessary declarations.
Say you have a long pipeline of sketch functions, like this:
```norun
startSketchOn('XZ')
|> line(%, end = [3, 4])
|> line(%, end = [10, 10])
|> line(%, end = [-13, -14])
|> close(%)
```
startSketch()
|> 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
@ -159,12 +162,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
```norun
startSketchOn('XZ')
|> line(end = [3, 4])
|> line(end = [10, 10])
|> line(end = [-13, -14])
|> close()
```
startSketch()
|> 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
@ -181,7 +184,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)
@ -213,7 +216,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, %)
@ -241,7 +244,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, %)

View File

@ -34,18 +34,6 @@ A custom plane.
----
A custom plane which has not been sent to the engine. It must be sent before it is used.
**enum:** `Uninit`
----

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,5 @@
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'
import type { Page, Locator } from '@playwright/test'
import { expect } from '@playwright/test'
type CmdBarSerialised =
| {
@ -189,71 +187,4 @@ 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}`
)
}
}

View File

@ -171,22 +171,4 @@ export class EditorFixture {
{ text, placeCursor }
)
}
async selectText(text: string) {
// First make sure the code pane is open
const wasPaneOpen = await this.checkIfPaneIsOpen()
if (!wasPaneOpen) {
await this.openPane()
}
// Use Playwright's built-in text selection on the code content
// it seems to only select whole divs, which works out to align with syntax highlighting
// for code mirror, so you can probably select "sketch002 = startSketchOn('XZ')"
// but less so for exactly "sketch002 = startS"
await this.codeContent.getByText(text).first().selectText()
// Reset pane state if needed
if (!wasPaneOpen) {
await this.closePane()
}
}
}

View File

@ -1,98 +0,0 @@
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()
})
}
)

View File

@ -36,7 +36,7 @@ extrude003 = extrude(sketch003, length = 20)
`
test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
test.describe('Check the happy path, for basic changing color', () => {
test.fixme('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] = [128, 194, 88]
const green: [number, number, number] = [108, 152, 75]
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, 20)
await scene.expectPixelColor(green, greenCheckCoords, 15)
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,150 +195,4 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
await expect(failToast).toBeVisible()
})
})
test(`manual code selection rename`, async ({
context,
homePage,
cmdBar,
editor,
page,
scene,
}) => {
const body1CapCoords = { x: 571, y: 351 }
await context.addInitScript((file) => {
localStorage.setItem('persistCode', file)
}, file)
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
const submittingToast = page.getByText('Submitting to Text-to-CAD API...')
const successToast = page.getByText('Prompt to edit successful')
const acceptBtn = page.getByRole('button', { name: 'checkmark Accept' })
await test.step('wait for scene to load and select code in editor', async () => {
// Find and select the text "sketch002" in the editor
await editor.selectText('sketch002')
// Verify the selection was made
await editor.expectState({
highlightedCode: '',
activeLines: ["sketch002 = startSketchOn('XZ')"],
diagnostics: [],
})
})
await test.step('fire off edit prompt', async () => {
await scene.expectPixelColor([134, 134, 134], body1CapCoords, 15)
await cmdBar.openCmdBar('promptToEdit')
await page
.getByTestId('cmd-bar-arg-value')
.fill('Please rename to mySketch')
await page.waitForTimeout(100)
await cmdBar.progressCmdBar()
await expect(submittingToast).toBeVisible()
await expect(submittingToast).not.toBeVisible({
timeout: 2 * 60_000,
})
await expect(successToast).toBeVisible()
})
await test.step('verify rename change and accept it', async () => {
await editor.expectEditor.toContain('mySketch = startSketchOn')
await editor.expectEditor.not.toContain('sketch002 = startSketchOn')
await editor.expectEditor.toContain(
'extrude002 = extrude(mySketch, length = 50)'
)
await acceptBtn.click()
await expect(successToast).not.toBeVisible()
})
})
test('multiple body selections', async ({
context,
homePage,
cmdBar,
editor,
page,
scene,
}) => {
const body1CapCoords = { x: 571, y: 351 }
const body2WallCoords = { x: 620, y: 152 }
const [clickBody1Cap] = scene.makeMouseHelpers(
body1CapCoords.x,
body1CapCoords.y
)
const [clickBody2Cap] = scene.makeMouseHelpers(
body2WallCoords.x,
body2WallCoords.y
)
const grey: [number, number, number] = [132, 132, 132]
await context.addInitScript((file) => {
localStorage.setItem('persistCode', file)
}, file)
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
const submittingToast = page.getByText('Submitting to Text-to-CAD API...')
const successToast = page.getByText('Prompt to edit successful')
const acceptBtn = page.getByRole('button', { name: 'checkmark Accept' })
await test.step('select multiple bodies and fire prompt', async () => {
// Initial color check
await scene.expectPixelColor(grey, body1CapCoords, 15)
// Open command bar first (without selection)
await cmdBar.openCmdBar('promptToEdit')
// Select first body
await page.waitForTimeout(100)
await clickBody1Cap()
// Hold shift and select second body
await editor.expectState({
highlightedCode: '',
activeLines: ['|>startProfileAt([-73.64,-42.89],%)'],
diagnostics: [],
})
await page.keyboard.down('Shift')
await page.waitForTimeout(100)
await clickBody2Cap()
await editor.expectState({
highlightedCode:
'line(end=[121.13,56.63],tag=$seg02)extrude(profile001,length=200)',
activeLines: [
'|>line(end=[121.13,56.63],tag=$seg02)',
'|>startProfileAt([-73.64,-42.89],%)',
],
diagnostics: [],
})
await page.keyboard.up('Shift')
await page.waitForTimeout(100)
await cmdBar.progressCmdBar()
// Enter prompt and submit
await page
.getByTestId('cmd-bar-arg-value')
.fill('make these neon green please, use #39FF14')
await page.waitForTimeout(100)
await cmdBar.progressCmdBar()
// Wait for API response
await expect(submittingToast).toBeVisible()
await expect(submittingToast).not.toBeVisible({
timeout: 2 * 60_000,
})
await expect(successToast).toBeVisible()
})
await test.step('verify code changed', async () => {
await editor.expectEditor.toContain('appearance(')
// Accept changes
await acceptBtn.click()
await expect(successToast).not.toBeVisible()
})
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View File

@ -1,33 +0,0 @@
{
"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 Normal file
View File

@ -0,0 +1 @@
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 Normal file
View File

@ -0,0 +1 @@
sketch001=startSketchOn('XZ')|>startProfileAt([75.8,317.2],%)//[$startCapTag,$EndCapTag]|>angledLine([0,268.43],%,$rectangleSegmentA001)|>angledLine([segAng(rectangleSegmentA001)-90,217.26],%,$seg01)|>angledLine([segAng(rectangleSegmentA001),-segLen(rectangleSegmentA001)],%,$yo)|>line(endAbsolute=[profileStartX(%),profileStartY(%)],tag=$seg02)|>close()extrude001=extrude(sketch001,length=100)|>chamfer(length=30,tags=[getOppositeEdge(seg01)],tag=$seg03)|>chamfer(length=30,tags=[seg01],tag=$seg04)|>chamfer(length=30,tags=[getNextAdjacentEdge(seg02)],tag=$seg05)|>chamfer(length=30,tags=[getNextAdjacentEdge(yo)],tag=$seg06)sketch005=startSketchOn(extrude001,seg06)profile004=startProfileAt([-23.43,19.69],sketch005)|>angledLine([0,9.1],%,$rectangleSegmentA005)|>angledLine([segAng(rectangleSegmentA005)-90,84.07],%)|>angledLine([segAng(rectangleSegmentA005),-segLen(rectangleSegmentA005)],%)|>line(endAbsolute=[profileStartX(%),profileStartY(%)])|>close()sketch004=startSketchOn(extrude001,seg05)profile003=startProfileAt([82.57,322.96],sketch004)|>angledLine([0,11.16],%,$rectangleSegmentA004)|>angledLine([segAng(rectangleSegmentA004)-90,103.07],%)|>angledLine([segAng(rectangleSegmentA004),-segLen(rectangleSegmentA004)],%)|>line(endAbsolute=[profileStartX(%),profileStartY(%)])|>close()sketch003=startSketchOn(extrude001,seg04)profile002=startProfileAt([-209.64,255.28],sketch003)|>angledLine([0,11.56],%,$rectangleSegmentA003)|>angledLine([segAng(rectangleSegmentA003)-90,106.84],%)|>angledLine([segAng(rectangleSegmentA003),-segLen(rectangleSegmentA003)],%)|>line(endAbsolute=[profileStartX(%),profileStartY(%)])|>close()sketch002=startSketchOn(extrude001,seg03)profile001=startProfileAt([205.96,254.59],sketch002)|>angledLine([0,11.39],%,$rectangleSegmentA002)|>angledLine([segAng(rectangleSegmentA002)-90,105.26],%)|>angledLine([segAng(rectangleSegmentA002),-segLen(rectangleSegmentA002)],%)|>line(endAbsolute=[profileStartX(%),profileStartY(%)])|>close()

View File

@ -91,11 +91,11 @@
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages",
"fetch:wasm": "./scripts/get-latest-wasm-bundle.sh",
"fetch:wasm:windows": "./scripts/get-latest-wasm-bundle.ps1",
"fetch:samples": "echo \"Fetching latest KCL samples...\" && curl -o public/kcl-samples-manifest-fallback.json https://raw.githubusercontent.com/KittyCAD/kcl-samples/next/manifest.json",
"fetch:samples": "echo \"Fetching latest KCL samples...\" && curl -o public/kcl-samples-manifest-fallback.json https://raw.githubusercontent.com/KittyCAD/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 && ./scripts/copy-wasm.ps1 && 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",
"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",

View File

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

View File

@ -1,8 +1,10 @@
#!/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
@ -12,7 +14,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 (commit $COMMIT)" > release-notes.md
echo "Nightly build $VERSION (commit $COMMIT)" > release-notes.md
# icons
cp assets/icon-nightly.png assets/icon.png

View File

@ -24,7 +24,12 @@ import ModelingMachineProvider from 'components/ModelingMachineProvider'
import FileMachineProvider from 'components/FileMachineProvider'
import { MachineManagerProvider } from 'components/MachineManagerProvider'
import { PATHS } from 'lib/paths'
import { fileLoader, homeLoader, telemetryLoader } from 'lib/routeLoaders'
import {
fileLoader,
homeLoader,
onboardingRedirectLoader,
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'
@ -108,6 +113,11 @@ const router = createRouter([
{
id: PATHS.FILE + 'SETTINGS',
children: [
{
loader: onboardingRedirectLoader,
index: true,
element: <></>,
},
{
path: makeUrlPathRelative(PATHS.SETTINGS),
element: <Settings />,

View File

@ -17,9 +17,7 @@ export const CommandBar = () => {
const {
context: { selectedCommand, currentArgument, commands },
} = commandBarState
const isSelectionArgument =
currentArgument?.inputType === 'selection' ||
currentArgument?.inputType === 'selectionMixed'
const isSelectionArgument = currentArgument?.inputType === 'selection'
const WrapperComponent = isSelectionArgument ? Popover : Dialog
// Close the command bar when navigating

View File

@ -1,7 +1,6 @@
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'
@ -85,14 +84,6 @@ function ArgumentInput({
onSubmit={onSubmit}
/>
)
case 'selectionMixed':
return (
<CommandBarSelectionMixedInput
arg={arg}
stepBack={stepBack}
onSubmit={onSubmit}
/>
)
case 'kcl':
return (
<CommandBarKclInput arg={arg} stepBack={stepBack} onSubmit={onSubmit} />

View File

@ -124,8 +124,7 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
<span className="sr-only">:&nbsp;</span>
<span data-testid="header-arg-value">
{argValue ? (
arg.inputType === 'selection' ||
arg.inputType === 'selectionMixed' ? (
arg.inputType === 'selection' ? (
getSelectionTypeDisplayText(argValue as Selections)
) : arg.inputType === 'kcl' ? (
roundOff(

View File

@ -1,135 +0,0 @@
import { useEffect, useMemo, useRef, useState } from 'react'
import { CommandArgument } from 'lib/commandTypes'
import {
Selections,
canSubmitSelectionArg,
getSelectionCountByType,
getSelectionTypeDisplayText,
} from 'lib/selections'
import { useSelector } from '@xstate/react'
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
const selectionSelector = (snapshot: any) => snapshot?.context.selectionRanges
export default function CommandBarSelectionMixedInput({
arg,
stepBack,
onSubmit,
}: {
arg: CommandArgument<unknown> & { inputType: 'selectionMixed'; name: string }
stepBack: () => void
onSubmit: (data: unknown) => void
}) {
const inputRef = useRef<HTMLInputElement>(null)
const commandBarState = useCommandBarState()
const [hasSubmitted, setHasSubmitted] = useState(false)
const [hasAutoSkipped, setHasAutoSkipped] = useState(false)
const selection: Selections = useSelector(arg.machineActor, selectionSelector)
const selectionsByType = useMemo(() => {
return getSelectionCountByType(selection)
}, [selection])
const canSubmitSelection = useMemo<boolean>(() => {
if (!selection) return false
const isNonZeroRange = selection.graphSelections.some((sel) => {
const range = sel.codeRef.range
return range[1] - range[0] !== 0 // Non-zero range is always valid
})
if (isNonZeroRange) return true
return canSubmitSelectionArg(selectionsByType, arg)
}, [selectionsByType, selection])
useEffect(() => {
inputRef.current?.focus()
}, [selection, inputRef])
// Only auto-skip on initial mount if we have a valid selection
// different from the component CommandBarSelectionInput in the the dependency array
// is empty
useEffect(() => {
if (!hasAutoSkipped && canSubmitSelection && arg.skip) {
const argValue = commandBarState.context.argumentsToSubmit[arg.name]
if (argValue === undefined) {
handleSubmit()
setHasAutoSkipped(true)
}
}
}, [])
function handleChange() {
inputRef.current?.focus()
}
function handleSubmit(e?: React.FormEvent<HTMLFormElement>) {
e?.preventDefault()
if (!canSubmitSelection) {
setHasSubmitted(true)
return
}
onSubmit(selection)
}
const isMixedSelection = arg.inputType === 'selectionMixed'
const allowNoSelection = isMixedSelection && arg.allowNoSelection
const showSceneSelection =
isMixedSelection && arg.selectionSource?.allowSceneSelection
return (
<form id="arg-form" onSubmit={handleSubmit}>
<label
className={
'relative flex flex-col mx-4 my-4 ' +
(!hasSubmitted || canSubmitSelection || 'text-destroy-50')
}
>
{canSubmitSelection
? 'Select objects in the scene'
: 'Select code or objects in the scene'}
{showSceneSelection && (
<div className="scene-selection mt-2">
<p className="text-sm text-chalkboard-60">
Select objects in the scene
</p>
{/* Scene selection UI will be handled by the parent component */}
</div>
)}
{allowNoSelection && (
<button
type="button"
onClick={() => onSubmit(null)}
className="mt-2 px-4 py-2 rounded border border-chalkboard-30 text-chalkboard-90 dark:text-chalkboard-10 hover:bg-chalkboard-10 dark:hover:bg-chalkboard-90 transition-colors"
>
Continue without selection
</button>
)}
<span data-testid="cmd-bar-arg-name" className="sr-only">
{arg.name}
</span>
<input
id="selection"
name="selection"
ref={inputRef}
required
data-testid="cmd-bar-arg-value"
placeholder="Select an entity with your mouse"
className="absolute inset-0 w-full h-full opacity-0 cursor-default"
onKeyDown={(event) => {
if (event.key === 'Backspace') {
stepBack()
} else if (event.key === 'Escape') {
commandBarActor.send({ type: 'Close' })
}
}}
onChange={handleChange}
value={JSON.stringify(selection || {})}
/>
</label>
</form>
)
}

View File

@ -130,8 +130,6 @@ export const FileMachineProvider = ({
navigateToFile: ({ context, event }) => {
if (event.type !== 'xstate.done.actor.create-and-open-file') return
if (event.output && 'name' in event.output) {
// TODO: Technically this is not the same as the FileTree Onclick even if they are in the same page
// What is "Open file?"
commandBarActor.send({ type: 'Close' })
navigate(
`..${PATHS.FILE}/${encodeURIComponent(

View File

@ -23,8 +23,6 @@ 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})`
@ -160,7 +158,6 @@ const FileTreeItem = ({
level = 0,
treeSelection,
setTreeSelection,
runtimeErrors,
}: {
parentDir: FileEntry | undefined
project?: IndexLoaderData['project']
@ -180,7 +177,6 @@ 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()
@ -190,8 +186,6 @@ 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
@ -298,7 +292,7 @@ const FileTreeItem = ({
>
{!isRenaming ? (
<button
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"
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"
style={{ paddingInlineStart: getIndentationCSS(level) }}
onClick={(e) => {
e.currentTarget.focus()
@ -306,21 +300,11 @@ 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"
/>
<span className="pl-1">{fileOrDir.name}</span>
{fileOrDir.name}
</button>
) : (
<RenameForm
@ -430,7 +414,6 @@ const FileTreeItem = ({
key={level + '-' + child.path}
treeSelection={treeSelection}
setTreeSelection={setTreeSelection}
runtimeErrors={runtimeErrors}
/>
)
)}
@ -677,8 +660,6 @@ 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
@ -788,7 +769,6 @@ export const FileTreeInner = ({
key={fileOrDir.path}
treeSelection={treeSelection}
setTreeSelection={setTreeSelection}
runtimeErrors={runtimeErrors}
/>
)
)}

View File

@ -18,7 +18,6 @@ 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'
@ -31,10 +30,8 @@ export type SidebarType =
| 'variables'
export interface BadgeInfo {
value: (props: PaneCallbackProps) => boolean | number | string
value: (props: PaneCallbackProps) => boolean | number
onClick?: MouseEventHandler<any>
className?: string
title?: string
}
/**
@ -155,25 +152,6 @@ export const sidebarPanes: SidebarPane[] = [
},
keybinding: 'Shift + F',
hide: ({ platform }) => platform === 'web',
showBadge: {
value: (context) => {
// Only compute runtime errors! Compilation errors are not tracked here.
const errors = kclErrorsByFilename(context.kclContext.errors)
return errors.size > 0 ? 'x' : ''
},
onClick: (e) => {
e.preventDefault()
// TODO: When we have generic file open
// If badge is pressed
// Open the first error in the array of errors
// Then scroll to error
// Do you automatically open the project files
// editorManager.scrollToFirstErrorDiagnosticIfExists()
},
className:
'absolute m-0 p-0 bottom-4 left-4 w-3 h-3 flex items-center justify-center text-[9px] font-semibold text-white bg-primary hue-rotate-90 rounded-full border border-chalkboard-10 dark:border-chalkboard-80 z-50 hover:cursor-pointer hover:scale-[2] transition-transform duration-200',
title: 'Project files have runtime errors',
},
},
{
id: 'variables',

View File

@ -27,10 +27,8 @@ interface ModelingSidebarProps {
}
interface BadgeInfoComputed {
value: number | boolean | string
value: number | boolean
onClick?: MouseEventHandler<any>
className?: string
title?: string
}
function getPlatformString(): 'web' | 'desktop' {
@ -118,8 +116,6 @@ 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
@ -129,7 +125,6 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
// Clear any hidden panes from the `openPanes` array
useEffect(() => {
const panesToReset: SidebarType[] = []
sidebarPanes.forEach((pane) => {
if (
pane.hide === true ||
@ -344,31 +339,22 @@ function ModelingPaneButton({
<p
id={`${paneConfig.id}-badge`}
className={
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'
'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={
showBadge.title
? showBadge.title
: `Click to view ${showBadge.value} notification${
Number(showBadge.value) > 1 ? 's' : ''
}`
}
title={`Click to view ${showBadge.value} notification${
Number(showBadge.value) > 1 ? 's' : ''
}`}
>
<span className="sr-only">&nbsp;has&nbsp;</span>
{typeof showBadge.value === 'number' ||
typeof showBadge.value === 'string' ? (
{typeof showBadge.value === 'number' ? (
<span>{showBadge.value}</span>
) : (
<span className="sr-only">a</span>
)}
{typeof showBadge.value === 'number' && (
<span className="sr-only">
&nbsp;notification{Number(showBadge.value) > 1 ? 's' : ''}
</span>
)}
<span className="sr-only">
&nbsp;notification{Number(showBadge.value) > 1 ? 's' : ''}
</span>
</p>
)}
</div>

View File

@ -4,12 +4,11 @@ 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, useSettings } from 'machines/appMachine'
import { useAuthState } from 'machines/appMachine'
import { IndexLoaderData } from 'lib/types'
import { getAppSettingsFilePath } from 'lib/desktop'
import { isDesktop } from 'lib/isDesktop'
@ -17,9 +16,6 @@ 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({})
@ -33,7 +29,6 @@ export function RouteProvider({ children }: { children: ReactNode }) {
const navigation = useNavigation()
const navigate = useNavigate()
const location = useLocation()
const settings = useSettings()
const authState = useAuthState()
useEffect(() => {
@ -48,32 +43,6 @@ export function RouteProvider({ children }: { children: ReactNode }) {
markOnce('code/willLoadHome')
} else if (isFile) {
markOnce('code/willLoadFile')
/**
* TODO: Move to XState. This block has been moved from routerLoaders
* and is borrowing the `isFile` logic from the rest of this
* telemetry-focused `useEffect`. Once `appMachine` knows about
* the current route and navigation, this can be moved into settingsMachine
* to fire as soon as the user settings have been read.
*/
const onboardingStatus: OnboardingStatus =
settings.app.onboardingStatus.current || ''
// '' is the initial state, 'completed' and 'dismissed' are the final states
const needsToOnboard =
onboardingStatus.length === 0 ||
!(onboardingStatus === 'completed' || onboardingStatus === 'dismissed')
const shouldRedirectToOnboarding = isFile && needsToOnboard
if (
shouldRedirectToOnboarding &&
settingsActor.getSnapshot().matches('idle')
) {
navigate(
(first ? location.pathname : navigation.location?.pathname) +
PATHS.ONBOARDING.INDEX +
onboardingStatus.slice(1)
)
}
}
setFirstState(false)
}, [navigation])

View File

@ -293,13 +293,6 @@ export class KclManager {
return null
}
// GOTCHA:
// When we safeParse this is tied to execution because they clicked a new file to load
// Clear all previous errors and logs because they are old since they executed a new file
// If we decouple safeParse from execution we need to move this application logic.
this._kclErrorsCallBack([])
this._logsCallBack([])
this.addDiagnostics(complilationErrorsToDiagnostics(result.errors))
this.addDiagnostics(complilationErrorsToDiagnostics(result.warnings))
if (result.errors.length > 0) {

View File

@ -13,7 +13,6 @@ describe('test kclErrToDiagnostic', () => {
operations: [],
artifactCommands: [],
artifactGraph: defaultArtifactGraph(),
filenames: {},
},
{
name: '',
@ -24,7 +23,6 @@ describe('test kclErrToDiagnostic', () => {
operations: [],
artifactCommands: [],
artifactGraph: defaultArtifactGraph(),
filenames: {},
},
]
const diagnostics = kclErrorsToDiagnostics(errors)

View File

@ -1,7 +1,4 @@
import {
KclError,
KclError as RustKclError,
} from '../wasm-lib/kcl/bindings/KclError'
import { 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'
@ -16,7 +13,6 @@ 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 {
@ -26,7 +22,6 @@ export class KCLError extends Error {
operations: Operation[]
artifactCommands: ArtifactCommand[]
artifactGraph: ArtifactGraph
filenames: { [x: number]: ModulePath | undefined }
constructor(
kind: ExtractKind<RustKclError> | 'name',
@ -34,8 +29,7 @@ export class KCLError extends Error {
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[],
artifactGraph: ArtifactGraph,
filenames: { [x: number]: ModulePath | undefined }
artifactGraph: ArtifactGraph
) {
super()
this.kind = kind
@ -44,7 +38,6 @@ export class KCLError extends Error {
this.operations = operations
this.artifactCommands = artifactCommands
this.artifactGraph = artifactGraph
this.filenames = filenames
Object.setPrototypeOf(this, KCLError.prototype)
}
}
@ -55,8 +48,7 @@ export class KCLLexicalError extends KCLError {
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[],
artifactGraph: ArtifactGraph,
filenames: { [x: number]: ModulePath | undefined }
artifactGraph: ArtifactGraph
) {
super(
'lexical',
@ -64,8 +56,7 @@ export class KCLLexicalError extends KCLError {
sourceRange,
operations,
artifactCommands,
artifactGraph,
filenames
artifactGraph
)
Object.setPrototypeOf(this, KCLSyntaxError.prototype)
}
@ -77,8 +68,7 @@ export class KCLInternalError extends KCLError {
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[],
artifactGraph: ArtifactGraph,
filenames: { [x: number]: ModulePath | undefined }
artifactGraph: ArtifactGraph
) {
super(
'internal',
@ -86,8 +76,7 @@ export class KCLInternalError extends KCLError {
sourceRange,
operations,
artifactCommands,
artifactGraph,
filenames
artifactGraph
)
Object.setPrototypeOf(this, KCLSyntaxError.prototype)
}
@ -99,8 +88,7 @@ export class KCLSyntaxError extends KCLError {
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[],
artifactGraph: ArtifactGraph,
filenames: { [x: number]: ModulePath | undefined }
artifactGraph: ArtifactGraph
) {
super(
'syntax',
@ -108,8 +96,7 @@ export class KCLSyntaxError extends KCLError {
sourceRange,
operations,
artifactCommands,
artifactGraph,
filenames
artifactGraph
)
Object.setPrototypeOf(this, KCLSyntaxError.prototype)
}
@ -121,8 +108,7 @@ export class KCLSemanticError extends KCLError {
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[],
artifactGraph: ArtifactGraph,
filenames: { [x: number]: ModulePath | undefined }
artifactGraph: ArtifactGraph
) {
super(
'semantic',
@ -130,8 +116,7 @@ export class KCLSemanticError extends KCLError {
sourceRange,
operations,
artifactCommands,
artifactGraph,
filenames
artifactGraph
)
Object.setPrototypeOf(this, KCLSemanticError.prototype)
}
@ -143,18 +128,9 @@ export class KCLTypeError extends KCLError {
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[],
artifactGraph: ArtifactGraph,
filenames: { [x: number]: ModulePath | undefined }
artifactGraph: ArtifactGraph
) {
super(
'type',
msg,
sourceRange,
operations,
artifactCommands,
artifactGraph,
filenames
)
super('type', msg, sourceRange, operations, artifactCommands, artifactGraph)
Object.setPrototypeOf(this, KCLTypeError.prototype)
}
}
@ -165,8 +141,7 @@ export class KCLUnimplementedError extends KCLError {
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[],
artifactGraph: ArtifactGraph,
filenames: { [x: number]: ModulePath | undefined }
artifactGraph: ArtifactGraph
) {
super(
'unimplemented',
@ -174,8 +149,7 @@ export class KCLUnimplementedError extends KCLError {
sourceRange,
operations,
artifactCommands,
artifactGraph,
filenames
artifactGraph
)
Object.setPrototypeOf(this, KCLUnimplementedError.prototype)
}
@ -187,8 +161,7 @@ export class KCLUnexpectedError extends KCLError {
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[],
artifactGraph: ArtifactGraph,
filenames: { [x: number]: ModulePath | undefined }
artifactGraph: ArtifactGraph
) {
super(
'unexpected',
@ -196,8 +169,7 @@ export class KCLUnexpectedError extends KCLError {
sourceRange,
operations,
artifactCommands,
artifactGraph,
filenames
artifactGraph
)
Object.setPrototypeOf(this, KCLUnexpectedError.prototype)
}
@ -209,8 +181,7 @@ export class KCLValueAlreadyDefined extends KCLError {
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[],
artifactGraph: ArtifactGraph,
filenames: { [x: number]: ModulePath | undefined }
artifactGraph: ArtifactGraph
) {
super(
'name',
@ -218,8 +189,7 @@ export class KCLValueAlreadyDefined extends KCLError {
sourceRange,
operations,
artifactCommands,
artifactGraph,
filenames
artifactGraph
)
Object.setPrototypeOf(this, KCLValueAlreadyDefined.prototype)
}
@ -231,8 +201,7 @@ export class KCLUndefinedValueError extends KCLError {
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[],
artifactGraph: ArtifactGraph,
filenames: { [x: number]: ModulePath | undefined }
artifactGraph: ArtifactGraph
) {
super(
'name',
@ -240,8 +209,7 @@ export class KCLUndefinedValueError extends KCLError {
sourceRange,
operations,
artifactCommands,
artifactGraph,
filenames
artifactGraph
)
Object.setPrototypeOf(this, KCLUndefinedValueError.prototype)
}
@ -264,8 +232,7 @@ export function lspDiagnosticsToKclErrors(
[posToOffset(doc, range.start)!, posToOffset(doc, range.end)!, 0],
[],
[],
defaultArtifactGraph(),
{}
defaultArtifactGraph()
)
)
.sort((a, b) => {
@ -337,34 +304,3 @@ 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
}

View File

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

View File

@ -116,11 +116,7 @@ const runGetPathToExtrudeForSegmentSelectionTest = async (
}
if (!extrudeInSketchPipe) {
const init = expectedExtrudeNode.init
if (
init.type !== 'CallExpression' &&
init.type !== 'CallExpressionKw' &&
init.type !== 'PipeExpression'
) {
if (init.type !== 'CallExpression' && init.type !== 'PipeExpression') {
return new Error(
'Expected extrude expression is not a CallExpression or PipeExpression'
)
@ -133,33 +129,25 @@ const runGetPathToExtrudeForSegmentSelectionTest = async (
// ast
const ast = assertParse(code)
// range
// selection
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
artifactGraph,
dependencies
)
if (err(pathResult)) return pathResult
const { pathToExtrudeNode } = pathResult
@ -246,56 +234,6 @@ extrude003 = extrude(sketch003, length = -15)`
expectedExtrudeSnippet
)
})
it('should return the correct paths for a (piped) extrude based on the other body (face)', async () => {
const code = `sketch001 = startSketchOn('XY')
|> startProfileAt([-25, -25], %)
|> yLine(50, %)
|> xLine(50, %)
|> yLine(-50, %)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
|> extrude(length = 50)
sketch002 = startSketchOn(sketch001, 'END')
|> startProfileAt([-15, -15], %)
|> yLine(30, %)
|> xLine(30, %)
|> yLine(-30, %)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
|> extrude(length = 30)`
const selectedSegmentSnippet = `xLine(30, %)`
const expectedExtrudeSnippet = `extrude(length = 30)`
await runGetPathToExtrudeForSegmentSelectionTest(
code,
selectedSegmentSnippet,
expectedExtrudeSnippet
)
})
it('should return the correct paths for a (non-piped) extrude based on the other body (face)', async () => {
const code = `sketch001 = startSketchOn('XY')
|> startProfileAt([-25, -25], %)
|> yLine(50, %)
|> xLine(50, %)
|> yLine(-50, %)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(sketch001, length = 50)
sketch002 = startSketchOn(extrude001, 'END')
|> startProfileAt([-15, -15], %)
|> yLine(30, %)
|> xLine(30, %)
|> yLine(-30, %)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude002 = extrude(sketch002, length = 30)`
const selectedSegmentSnippet = `xLine(30, %)`
const expectedExtrudeSnippet = `extrude002 = extrude(sketch002, length = 30)`
await runGetPathToExtrudeForSegmentSelectionTest(
code,
selectedSegmentSnippet,
expectedExtrudeSnippet
)
})
it('should not return any path for missing extrusion', async () => {
const code = `sketch001 = startSketchOn('XY')
|> startProfileAt([-30, 30], %)

View File

@ -10,6 +10,7 @@ import {
Program,
VariableDeclaration,
VariableDeclarator,
sketchFromKclValue,
} from '../wasm'
import {
createCallExpressionStdLib,
@ -34,11 +35,11 @@ import {
sketchLineHelperMap,
sketchLineHelperMapKw,
} from '../std/sketch'
import { err } from 'lib/trap'
import { err, trap } from 'lib/trap'
import { Selection, Selections } from 'lib/selections'
import { KclCommandValue } from 'lib/commandTypes'
import { isArray } from 'lib/utils'
import { Artifact, getSweepArtifactFromSelection } from 'lang/std/artifactGraph'
import { Artifact, getSweepFromSuspectedPath } from 'lang/std/artifactGraph'
import { Node } from 'wasm-lib/kcl/bindings/Node'
import { findKwArg } from 'lang/util'
import { KclManager } from 'lang/KclSingleton'
@ -120,7 +121,8 @@ export function modifyAstWithEdgeTreatmentAndTag(
const result = getPathToExtrudeForSegmentSelection(
clonedAstForGetExtrude,
selection,
artifactGraph
artifactGraph,
dependencies
)
if (err(result)) return result
const { pathToSegmentNode, pathToExtrudeNode } = result
@ -277,19 +279,39 @@ function insertParametersIntoAst(
export function getPathToExtrudeForSegmentSelection(
ast: Program,
selection: Selection,
artifactGraph: ArtifactGraph
artifactGraph: ArtifactGraph,
dependencies: {
kclManager: KclManager
engineCommandManager: EngineCommandManager
editorManager: EditorManager
codeManager: CodeManager
}
): { pathToSegmentNode: PathToNode; pathToExtrudeNode: PathToNode } | Error {
const pathToSegmentNode = getNodePathFromSourceRange(
ast,
selection.codeRef?.range
)
const sweepArtifact = getSweepArtifactFromSelection(selection, artifactGraph)
if (err(sweepArtifact)) return sweepArtifact
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 pathToExtrudeNode = getNodePathFromSourceRange(
ast,
sweepArtifact.codeRef.range
extrusion.codeRef.range
)
if (err(pathToExtrudeNode)) return pathToExtrudeNode

View File

@ -13,23 +13,36 @@ 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)
@ -42,7 +55,8 @@ export function addShell({
const extrudeLookupResult = getPathToExtrudeForSegmentSelection(
clonedAstForGetExtrude,
graphSelection,
artifactGraph
artifactGraph,
dependencies
)
if (err(extrudeLookupResult)) {
return new Error("Couldn't find extrude")

View File

@ -18,7 +18,6 @@ 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'
@ -80,7 +79,7 @@ interface SegmentArtifactRich extends BaseArtifact {
interface SweepArtifactRich extends BaseArtifact {
type: 'sweep'
subType: 'extrusion' | 'revolve' | 'revolveAboutEdge' | 'loft' | 'sweep'
subType: 'extrusion' | 'revolve' | 'loft' | 'sweep'
path: PathArtifact
surfaces: Array<WallArtifact | CapArtifact>
edges: Array<SweepEdge>
@ -456,47 +455,6 @@ export function getSweepFromSuspectedPath(
)
}
export function getSweepArtifactFromSelection(
selection: Selection,
artifactGraph: ArtifactGraph
): SweepArtifact | Error {
let sweepArtifact: Artifact | null = null
if (selection.artifact?.type === 'sweepEdge') {
const _artifact = getArtifactOfTypes(
{ key: selection.artifact.sweepId, types: ['sweep'] },
artifactGraph
)
if (err(_artifact)) return _artifact
sweepArtifact = _artifact
} else if (selection.artifact?.type === 'segment') {
const _pathArtifact = getArtifactOfTypes(
{ key: selection.artifact.pathId, types: ['path'] },
artifactGraph
)
if (err(_pathArtifact)) return _pathArtifact
if (!_pathArtifact.sweepId) return new Error('Path does not have a sweepId')
const _artifact = getArtifactOfTypes(
{ key: _pathArtifact.sweepId, types: ['sweep'] },
artifactGraph
)
if (err(_artifact)) return _artifact
sweepArtifact = _artifact
} else if (
selection.artifact?.type === 'cap' ||
selection.artifact?.type === 'wall'
) {
const _artifact = getArtifactOfTypes(
{ key: selection.artifact.sweepId, types: ['sweep'] },
artifactGraph
)
if (err(_artifact)) return _artifact
sweepArtifact = _artifact
}
if (!sweepArtifact) return new Error('No sweep artifact found')
return sweepArtifact
}
export function getCodeRefsByArtifactId(
id: string,
artifactGraph: ArtifactGraph

View File

@ -60,7 +60,6 @@ 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'
@ -267,8 +266,7 @@ export const parse = (code: string | Error): ParseResult | Error => {
firstSourceRange(parsed),
[],
[],
defaultArtifactGraph(),
{}
defaultArtifactGraph()
)
}
}
@ -298,7 +296,6 @@ export interface ExecState {
artifactCommands: ArtifactCommand[]
artifactGraph: ArtifactGraph
errors: CompilationError[]
filenames: { [x: number]: ModulePath | undefined }
}
/**
@ -313,7 +310,6 @@ export function emptyExecState(): ExecState {
artifactCommands: [],
artifactGraph: defaultArtifactGraph(),
errors: [],
filenames: [],
}
}
@ -340,7 +336,6 @@ function execStateFromRust(
artifactCommands: execOutcome.artifactCommands,
artifactGraph,
errors: execOutcome.errors,
filenames: execOutcome.filenames,
}
}
@ -352,7 +347,6 @@ function mockExecStateFromRust(execOutcome: RustExecOutcome): ExecState {
artifactCommands: execOutcome.artifactCommands,
artifactGraph: new Map<ArtifactId, Artifact>(),
errors: execOutcome.errors,
filenames: execOutcome.filenames,
}
}
@ -480,7 +474,7 @@ const jsAppSettings = async () => {
}
const errFromErrWithOutputs = (e: any): KCLError => {
console.log(e)
console.log('execute error', e)
const parsed: KclErrorWithOutputs = JSON.parse(e.toString())
return new KCLError(
parsed.error.kind,
@ -488,8 +482,7 @@ const errFromErrWithOutputs = (e: any): KCLError => {
firstSourceRange(parsed.error),
parsed.operations,
parsed.artifactCommands,
rustArtifactGraphToMap(parsed.artifactGraph),
parsed.filenames
rustArtifactGraphToMap(parsed.artifactGraph)
)
}
@ -555,8 +548,7 @@ export const modifyAstForSketch = async (
firstSourceRange(parsed),
[],
[],
defaultArtifactGraph(),
{}
defaultArtifactGraph()
)
return Promise.reject(kclError)

View File

@ -666,7 +666,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
icon: 'chat',
args: {
selection: {
inputType: 'selectionMixed',
inputType: 'selection',
selectionTypes: [
'solid2d',
'segment',
@ -678,10 +678,6 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
],
multiple: true,
required: true,
selectionSource: {
allowSceneSelection: true,
allowCodeSelection: true,
},
skip: true,
},
prompt: {

View File

@ -16,7 +16,6 @@ const INPUT_TYPES = [
'text',
'kcl',
'selection',
'selectionMixed',
'boolean',
] as const
export interface KclExpression {
@ -157,23 +156,6 @@ 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
@ -270,23 +252,6 @@ export type CommandArgument<
context: CommandBarContext
}) => Promise<boolean | string>
}
| {
inputType: 'selectionMixed'
selectionTypes: Artifact['type'][]
multiple: boolean
allowNoSelection?: boolean
validation?: ({
data,
context,
}: {
data: any
context: CommandBarContext
}) => Promise<boolean | string>
selectionSource?: {
allowSceneSelection?: boolean
allowCodeSelection?: boolean
}
}
| {
inputType: 'kcl'
createVariableByDefault?: boolean

View File

@ -187,16 +187,6 @@ export function buildCommandArgument<
selectionTypes: arg.selectionTypes,
validation: arg.validation,
} satisfies CommandArgument<O, T> & { inputType: 'selection' }
} else if (arg.inputType === 'selectionMixed') {
return {
inputType: arg.inputType,
...baseCommandArgument,
multiple: arg.multiple,
selectionTypes: arg.selectionTypes,
validation: arg.validation,
allowNoSelection: arg.allowNoSelection,
selectionSource: arg.selectionSource,
} satisfies CommandArgument<O, T> & { inputType: 'selectionMixed' }
} else if (arg.inputType === 'kcl') {
return {
inputType: arg.inputType,

View File

@ -43,33 +43,15 @@ export async function submitPromptToEditToQueue({
projectName,
}: {
prompt: string
selections: Selections | null
selections: Selections
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') {
@ -171,17 +153,8 @@ 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,
@ -190,15 +163,6 @@ See later source ranges for more context. about the sweep`,
projectName !== '' && projectName !== 'browser' ? projectName : undefined,
kcl_version: kclManager.kclVersion,
}
return submitToApi(body, token)
}
// Helper function to handle API submission
async function submitToApi(
body: Models['TextToCadIterationBody_type'],
token?: string
): Promise<Models['TextToCadIteration_type'] | Error> {
const url = VITE_KC_API_BASE_URL + '/ml/text-to-cad/iteration'
const data: Models['TextToCadIteration_type'] | Error =
await crossPlatformFetch(

View File

@ -23,6 +23,30 @@ 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> => {

View File

@ -481,9 +481,7 @@ export function getSelectionTypeDisplayText(
export function canSubmitSelectionArg(
selectionsByType: 'none' | Map<ResolvedSelectionType, number>,
argument: CommandArgument<unknown> & {
inputType: 'selection' | 'selectionMixed'
}
argument: CommandArgument<unknown> & { inputType: 'selection' }
) {
return (
selectionsByType !== 'none' &&

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