Compare commits
59 Commits
nightly-v2
...
jtran/fix-
Author | SHA1 | Date | |
---|---|---|---|
0c2f63b399 | |||
363ae10658 | |||
ac4a6c84cf | |||
c6fad2e2dc | |||
013cb10961 | |||
6261083cb1 | |||
2b0ba37ed0 | |||
96174f3cf6 | |||
aed62ff912 | |||
9334d64608 | |||
4fa7d2d8c8 | |||
3e615dfdbc | |||
c9860af29f | |||
23a42f0195 | |||
a77fa639f3 | |||
0a5ad7c95b | |||
4a654523d2 | |||
73a7e2bfd6 | |||
eb0850fea9 | |||
029f76f273 | |||
28b5f7080c | |||
5b1dcfecd6 | |||
f89d191425 | |||
2f4e4b62a8 | |||
5ebd5c8dbb | |||
a9ceaf2678 | |||
c8afd3399b | |||
5dda4828c6 | |||
72acab752c | |||
81df38ad1c | |||
0576a2bef1 | |||
4b2f6b4647 | |||
69edaa4183 | |||
2eb7c382bf | |||
38913ecb98 | |||
debd06129f | |||
d38bd342a0 | |||
f026f10335 | |||
895d7ebc6d | |||
65edf17a44 | |||
0c2a0a8c07 | |||
97cef4d16c | |||
9358278f7b | |||
a174e084d4 | |||
df7246897a | |||
0c9f64dd7c | |||
d2b9d3a058 | |||
7e54f08778 | |||
d9c2dd376e | |||
275a2150e7 | |||
8b8feb8d68 | |||
e21ef3f122 | |||
66834931aa | |||
06c1bcaf2e | |||
fc7df7ecbe | |||
67cb7b33bb | |||
ea74b94fac | |||
529833c63f | |||
92da86391a |
@ -1,3 +1,3 @@
|
||||
[codespell]
|
||||
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,atleast,ue,afterall
|
||||
skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md,.yarn.lock,**/yarn.lock,./openapi/*.json,./src/lib/machine-api.d.ts,./packages/codemirror-lang-kcl/test/all.test.ts
|
||||
skip: **/target,node_modules,build,dist,./out,**/Cargo.lock,./docs/kcl/*.md,.yarn.lock,**/yarn.lock,./openapi/*.json,./packages/codemirror-lang-kcl/test/all.test.ts,tsconfig.tsbuildinfo
|
||||
|
12
.eslintrc
@ -5,16 +5,24 @@
|
||||
},
|
||||
"plugins": [
|
||||
"css-modules",
|
||||
"jest",
|
||||
"react",
|
||||
"suggest-no-throw",
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest",
|
||||
"plugin:css-modules/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
"@typescript-eslint/no-misused-promises": "error",
|
||||
"no-restricted-globals": [
|
||||
"error",
|
||||
{
|
||||
"name": "isNaN",
|
||||
"message": "Use Number.isNaN() instead."
|
||||
}
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"never"
|
||||
|
2
.github/ci-cd-scripts/playwright-electron.sh
vendored
@ -21,7 +21,7 @@ if [[ ! -f "test-results/.last-run.json" ]]; then
|
||||
fi
|
||||
|
||||
retry=1
|
||||
max_retrys=4
|
||||
max_retrys=5
|
||||
|
||||
# retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues
|
||||
while [[ $retry -le $max_retrys ]]; do
|
||||
|
46
.github/dependabot.yml
vendored
@ -5,24 +5,28 @@
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: 'npm' # See documentation for possible values
|
||||
directory: '/' # Location of package manifests
|
||||
schedule:
|
||||
interval: 'weekly'
|
||||
reviewers:
|
||||
- franknoirot
|
||||
- irev-dev
|
||||
- package-ecosystem: 'github-actions' # See documentation for possible values
|
||||
directory: '/' # Location of package manifests
|
||||
schedule:
|
||||
interval: 'weekly'
|
||||
reviewers:
|
||||
- adamchalmers
|
||||
- jessfraz
|
||||
- package-ecosystem: 'cargo' # See documentation for possible values
|
||||
directory: '/src/wasm-lib/' # Location of package manifests
|
||||
schedule:
|
||||
interval: 'weekly'
|
||||
reviewers:
|
||||
- adamchalmers
|
||||
- jessfraz
|
||||
- package-ecosystem: 'npm' # See documentation for possible values
|
||||
directory: '/' # Location of package manifests
|
||||
schedule:
|
||||
interval: 'weekly'
|
||||
reviewers:
|
||||
- franknoirot
|
||||
- irev-dev
|
||||
- package-ecosystem: 'github-actions' # See documentation for possible values
|
||||
directory: '/' # Location of package manifests
|
||||
schedule:
|
||||
interval: 'weekly'
|
||||
reviewers:
|
||||
- adamchalmers
|
||||
- jessfraz
|
||||
- package-ecosystem: 'cargo' # See documentation for possible values
|
||||
directory: '/src/wasm-lib/' # Location of package manifests
|
||||
schedule:
|
||||
interval: 'weekly'
|
||||
reviewers:
|
||||
- adamchalmers
|
||||
- jessfraz
|
||||
groups:
|
||||
serde-dependencies:
|
||||
patterns:
|
||||
- "serde*"
|
||||
|
6
.github/workflows/e2e-tests.yml
vendored
@ -127,9 +127,12 @@ jobs:
|
||||
shell: bash
|
||||
run: yarn tron:package
|
||||
- name: Run ubuntu/chrome snapshots
|
||||
if: ${{ matrix.os == 'namespace-profile-ubuntu-8-cores' && matrix.shardIndex == 1 }}
|
||||
shell: bash
|
||||
# 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=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
||||
PLATFORM=web yarn playwright test --config=playwright.config.ts --retries="3" --update-snapshots --grep=@snapshot --shard=1/1
|
||||
env:
|
||||
CI: true
|
||||
NODE_ENV: development
|
||||
@ -150,6 +153,7 @@ jobs:
|
||||
continue-on-error: true
|
||||
run: rm -r test-results
|
||||
- name: check for changes
|
||||
if: ${{ matrix.os == 'namespace-profile-ubuntu-8-cores' && matrix.shardIndex == 1 }}
|
||||
shell: bash
|
||||
id: git-check
|
||||
run: |
|
||||
|
40
README.md
@ -337,13 +337,47 @@ For individual testing:
|
||||
yarn test abstractSyntaxTree -t "unexpected closed curly brace" --silent=false
|
||||
```
|
||||
|
||||
Which will run our suite of [Vitest unit](https://vitest.dev/) and [React Testing Library E2E](https://testing-library.com/docs/react-testing-library/intro/) tests, in interactive mode by default.
|
||||
Which will run our suite of [Vitest unit](https://vitest.dev/) and [React Testing Library E2E](https://testing-library.com/docs/react-testing-library/intro) tests, in interactive mode by default.
|
||||
|
||||
### Rust tests
|
||||
|
||||
```bash
|
||||
**Dependencies**
|
||||
|
||||
- `KITTYCAD_API_TOKEN`
|
||||
- `cargo-nextest`
|
||||
- `just`
|
||||
|
||||
#### Setting KITTYCAD_API_TOKEN
|
||||
Use the production zoo.dev token, set this environment variable before running the tests
|
||||
|
||||
#### Installing cargonextest
|
||||
|
||||
```
|
||||
cd src/wasm-lib
|
||||
KITTYCAD_API_TOKEN=XXX cargo test -- --test-threads=1
|
||||
cargo search cargo-nextest
|
||||
cargo install cargo-nextest
|
||||
```
|
||||
|
||||
#### just
|
||||
install [`just`](https://github.com/casey/just?tab=readme-ov-file#pre-built-binaries)
|
||||
|
||||
#### Running the tests
|
||||
|
||||
```bash
|
||||
# With just
|
||||
# Make sure KITTYCAD_API_TOKEN=<prod zoo.dev token> is set
|
||||
# Make sure you installed cargo-nextest
|
||||
# Make sure you installed just
|
||||
cd src/wasm-lib
|
||||
just test
|
||||
```
|
||||
|
||||
```bash
|
||||
# Without just
|
||||
# Make sure KITTYCAD_API_TOKEN=<prod zoo.dev token> is set
|
||||
# Make sure you installed cargo-nextest
|
||||
cd src/wasm-lib
|
||||
export RUST_BRACKTRACE="full" && cargo nextest run --workspace --test-threads=1
|
||||
```
|
||||
|
||||
Where `XXX` is an API token from the production engine (NOT the dev environment).
|
||||
|
@ -24,3 +24,5 @@ once fixed in engine will just start working here with no language changes.
|
||||
chamfer cases work currently.
|
||||
|
||||
- **Appearance**: Changing the appearance on a loft does not work.
|
||||
|
||||
- **Helix**: Currently sweeping a helix does not work.
|
||||
|
43
docs/kcl/helixRevolutions.md
Normal file
@ -48,6 +48,7 @@ layout: manual
|
||||
* [`getOppositeEdge`](kcl/getOppositeEdge)
|
||||
* [`getPreviousAdjacentEdge`](kcl/getPreviousAdjacentEdge)
|
||||
* [`helix`](kcl/helix)
|
||||
* [`helixRevolutions`](kcl/helixRevolutions)
|
||||
* [`hole`](kcl/hole)
|
||||
* [`hollow`](kcl/hollow)
|
||||
* [`import`](kcl/import)
|
||||
@ -81,6 +82,7 @@ layout: manual
|
||||
* [`pi`](kcl/pi)
|
||||
* [`polar`](kcl/polar)
|
||||
* [`polygon`](kcl/polygon)
|
||||
* [`pop`](kcl/pop)
|
||||
* [`pow`](kcl/pow)
|
||||
* [`profileStart`](kcl/profileStart)
|
||||
* [`profileStartX`](kcl/profileStartX)
|
||||
|
39
docs/kcl/pop.md
Normal file
6781
docs/kcl/std.json
@ -1,19 +1,19 @@
|
||||
---
|
||||
title: "AxisOrEdgeReference"
|
||||
excerpt: "Axis or tagged edge."
|
||||
title: "Axis2dOrEdgeReference"
|
||||
excerpt: "A 2D axis or tagged edge."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
Axis or tagged edge.
|
||||
A 2D axis or tagged edge.
|
||||
|
||||
|
||||
|
||||
|
||||
**This schema accepts any of the following:**
|
||||
|
||||
Axis and origin.
|
||||
2D axis and origin.
|
||||
|
||||
[`AxisAndOrigin`](/docs/kcl/types/AxisAndOrigin)
|
||||
[`AxisAndOrigin2d`](/docs/kcl/types/AxisAndOrigin2d)
|
||||
|
||||
|
||||
|
42
docs/kcl/types/Axis3dOrEdgeReference.md
Normal file
@ -0,0 +1,42 @@
|
||||
---
|
||||
title: "Axis3dOrEdgeReference"
|
||||
excerpt: "A 3D axis or tagged edge."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
A 3D axis or tagged edge.
|
||||
|
||||
|
||||
|
||||
|
||||
**This schema accepts any of the following:**
|
||||
|
||||
3D axis and origin.
|
||||
|
||||
[`AxisAndOrigin3d`](/docs/kcl/types/AxisAndOrigin3d)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
----
|
||||
Tagged edge.
|
||||
|
||||
[`EdgeReference`](/docs/kcl/types/EdgeReference)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
----
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
---
|
||||
title: "AxisAndOrigin"
|
||||
excerpt: "Axis and origin."
|
||||
title: "AxisAndOrigin2d"
|
||||
excerpt: "A 2D axis and origin."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
Axis and origin.
|
||||
A 2D axis and origin.
|
||||
|
||||
|
||||
|
105
docs/kcl/types/AxisAndOrigin3d.md
Normal file
@ -0,0 +1,105 @@
|
||||
---
|
||||
title: "AxisAndOrigin3d"
|
||||
excerpt: "A 3D axis and origin."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
A 3D axis and origin.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
**This schema accepts exactly one of the following:**
|
||||
|
||||
X-axis.
|
||||
|
||||
**enum:** `X`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
----
|
||||
Y-axis.
|
||||
|
||||
**enum:** `Y`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
----
|
||||
Z-axis.
|
||||
|
||||
**enum:** `Z`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
----
|
||||
Flip the X-axis.
|
||||
|
||||
**enum:** `-X`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
----
|
||||
Flip the Y-axis.
|
||||
|
||||
**enum:** `-Y`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
----
|
||||
Flip the Z-axis.
|
||||
|
||||
**enum:** `-Z`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
----
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `custom` |`object`| | No |
|
||||
|
||||
|
||||
----
|
||||
|
||||
|
||||
|
||||
|
25
docs/kcl/types/Helix.md
Normal file
@ -0,0 +1,25 @@
|
||||
---
|
||||
title: "Helix"
|
||||
excerpt: "A helix."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
A helix.
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `value` |`string`| The id of the helix. | No |
|
||||
| `revolutions` |`number`| Number of revolutions. | No |
|
||||
| `angleStart` |`number`| Start angle (in degrees). | No |
|
||||
| `ccw` |`boolean`| Is the helix rotation counter clockwise? | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
---
|
||||
title: "HelixData"
|
||||
excerpt: "Data for helices."
|
||||
excerpt: "Data for a helix."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
Data for helices.
|
||||
Data for a helix.
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
@ -19,6 +19,8 @@ Data for helices.
|
||||
| `revolutions` |`number`| Number of revolutions. | No |
|
||||
| `angleStart` |`number`| Start angle (in degrees). | No |
|
||||
| `ccw` |`boolean`| Is the helix rotation counter clockwise? The default is `false`. | No |
|
||||
| `length` |`number`| Length of the helix. If this argument is not provided, the height of the solid is used. | No |
|
||||
| `length` |`number`| Length of the helix. | No |
|
||||
| `radius` |`number`| Radius of the helix. | No |
|
||||
| `axis` |[`Axis3dOrEdgeReference`](/docs/kcl/types/Axis3dOrEdgeReference)| Axis to use as mirror. | No |
|
||||
|
||||
|
||||
|
24
docs/kcl/types/HelixRevolutionsData.md
Normal file
@ -0,0 +1,24 @@
|
||||
---
|
||||
title: "HelixRevolutionsData"
|
||||
excerpt: "Data for helix revolutions."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
Data for helix revolutions.
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `revolutions` |`number`| Number of revolutions. | No |
|
||||
| `angleStart` |`number`| Start angle (in degrees). | No |
|
||||
| `ccw` |`boolean`| Is the helix rotation counter clockwise? The default is `false`. | No |
|
||||
| `length` |`number`| Length of the helix. If this argument is not provided, the height of the solid is used. | No |
|
||||
|
||||
|
25
docs/kcl/types/HelixValue.md
Normal file
@ -0,0 +1,25 @@
|
||||
---
|
||||
title: "HelixValue"
|
||||
excerpt: "A helix."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
A helix.
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `value` |`string`| The id of the helix. | No |
|
||||
| `revolutions` |`number`| Number of revolutions. | No |
|
||||
| `angleStart` |`number`| Start angle (in degrees). | No |
|
||||
| `ccw` |`boolean`| Is the helix rotation counter clockwise? | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||
|
||||
|
@ -285,6 +285,27 @@ An solid is a collection of extrude surfaces.
|
||||
| `value` |`[` [`Solid`](/docs/kcl/types/Solid) `]`| | No |
|
||||
|
||||
|
||||
----
|
||||
A helix.
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: [`Helix`](/docs/kcl/types/Helix)| | No |
|
||||
| `value` |`string`| The id of the helix. | No |
|
||||
| `revolutions` |`number`| Number of revolutions. | No |
|
||||
| `angleStart` |`number`| Start angle (in degrees). | No |
|
||||
| `ccw` |`boolean`| Is the helix rotation counter clockwise? | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||
|
||||
|
||||
----
|
||||
Data for an imported geometry.
|
||||
|
||||
|
@ -16,6 +16,6 @@ Data for a mirror.
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `axis` |[`AxisOrEdgeReference`](/docs/kcl/types/AxisOrEdgeReference)| Axis to use as mirror. | No |
|
||||
| `axis` |[`Axis2dOrEdgeReference`](/docs/kcl/types/Axis2dOrEdgeReference)| Axis to use as mirror. | No |
|
||||
|
||||
|
||||
|
@ -17,7 +17,7 @@ Data for revolution surfaces.
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `angle` |`number` (**maximum:** 360.0) (**minimum:** -360.0)| Angle to revolve (in degrees). Default is 360. | No |
|
||||
| `axis` |[`AxisOrEdgeReference`](/docs/kcl/types/AxisOrEdgeReference)| Axis of revolution. | No |
|
||||
| `axis` |[`Axis2dOrEdgeReference`](/docs/kcl/types/Axis2dOrEdgeReference)| Axis of revolution. | No |
|
||||
| `tolerance` |`number`| Tolerance for the revolve operation. | No |
|
||||
|
||||
|
||||
|
@ -16,7 +16,7 @@ Data for a sweep.
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `path` |[`Sketch`](/docs/kcl/types/Sketch)| The path to sweep along. | No |
|
||||
| `path` |[`SweepPath`](/docs/kcl/types/SweepPath)| The path to sweep along. | No |
|
||||
| `sectional` |`boolean`| If true, the sweep will be broken up into sub-sweeps (extrusions, revolves, sweeps) based on the trajectory path components. | No |
|
||||
| `tolerance` |`number`| Tolerance for the sweep operation. | No |
|
||||
|
||||
|
42
docs/kcl/types/SweepPath.md
Normal file
@ -0,0 +1,42 @@
|
||||
---
|
||||
title: "SweepPath"
|
||||
excerpt: "A path to sweep along."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
A path to sweep along.
|
||||
|
||||
|
||||
|
||||
|
||||
**This schema accepts any of the following:**
|
||||
|
||||
A path to sweep along.
|
||||
|
||||
[`Sketch`](/docs/kcl/types/Sketch)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
----
|
||||
A path to sweep along.
|
||||
|
||||
[`Helix`](/docs/kcl/types/Helix)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
----
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -36,7 +36,8 @@ type DragFromHandler = (
|
||||
|
||||
export class SceneFixture {
|
||||
public page: Page
|
||||
|
||||
public streamWrapper!: Locator
|
||||
public loadingIndicator!: Locator
|
||||
private exeIndicator!: Locator
|
||||
|
||||
constructor(page: Page) {
|
||||
@ -64,6 +65,8 @@ export class SceneFixture {
|
||||
this.page = page
|
||||
|
||||
this.exeIndicator = page.getByTestId('model-state-indicator-execution-done')
|
||||
this.streamWrapper = page.getByTestId('stream')
|
||||
this.loadingIndicator = this.streamWrapper.getByTestId('loading')
|
||||
}
|
||||
|
||||
makeMouseHelpers = (
|
||||
|
@ -14,6 +14,7 @@ export class ToolbarFixture {
|
||||
|
||||
extrudeButton!: Locator
|
||||
loftButton!: Locator
|
||||
sweepButton!: Locator
|
||||
shellButton!: Locator
|
||||
offsetPlaneButton!: Locator
|
||||
startSketchBtn!: Locator
|
||||
@ -40,6 +41,7 @@ export class ToolbarFixture {
|
||||
this.page = page
|
||||
this.extrudeButton = page.getByTestId('extrude')
|
||||
this.loftButton = page.getByTestId('loft')
|
||||
this.sweepButton = page.getByTestId('sweep')
|
||||
this.shellButton = page.getByTestId('shell')
|
||||
this.offsetPlaneButton = page.getByTestId('plane-offset')
|
||||
this.startSketchBtn = page.getByTestId('sketch')
|
||||
|
@ -756,6 +756,17 @@ test(`Offset plane point-and-click`, async ({
|
||||
})
|
||||
await scene.expectPixelColor([74, 74, 74], testPoint, 15)
|
||||
})
|
||||
|
||||
await test.step('Delete offset plane via feature tree selection', async () => {
|
||||
await editor.closePane()
|
||||
const operationButton = await toolbar.getFeatureTreeOperation(
|
||||
'Offset Plane',
|
||||
0
|
||||
)
|
||||
await operationButton.click({ button: 'left' })
|
||||
await page.keyboard.press('Backspace')
|
||||
await scene.expectPixelColor([50, 51, 96], testPoint, 15)
|
||||
})
|
||||
})
|
||||
|
||||
const loftPointAndClickCases = [
|
||||
@ -851,6 +862,173 @@ loftPointAndClickCases.forEach(({ shouldPreselect }) => {
|
||||
})
|
||||
await scene.expectPixelColor([89, 89, 89], testPoint, 15)
|
||||
})
|
||||
|
||||
await test.step('Delete loft via feature tree selection', async () => {
|
||||
await editor.closePane()
|
||||
const operationButton = await toolbar.getFeatureTreeOperation('Loft', 0)
|
||||
await operationButton.click({ button: 'left' })
|
||||
await page.keyboard.press('Backspace')
|
||||
await scene.expectPixelColor([254, 254, 254], testPoint, 15)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// TODO: merge with above test. Right now we're not able to delete a loft
|
||||
// right after creation via selection for some reason, so we go with a new instance
|
||||
test('Loft and offset plane deletion via selection', async ({
|
||||
context,
|
||||
page,
|
||||
homePage,
|
||||
scene,
|
||||
}) => {
|
||||
const initialCode = `sketch001 = startSketchOn('XZ')
|
||||
|> circle({ center = [0, 0], radius = 30 }, %)
|
||||
plane001 = offsetPlane('XZ', 50)
|
||||
sketch002 = startSketchOn(plane001)
|
||||
|> circle({ center = [0, 0], radius = 20 }, %)
|
||||
loft001 = loft([sketch001, sketch002])
|
||||
`
|
||||
await context.addInitScript((initialCode) => {
|
||||
localStorage.setItem('persistCode', initialCode)
|
||||
}, initialCode)
|
||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||
await homePage.goToModelingScene()
|
||||
|
||||
// One dumb hardcoded screen pixel value
|
||||
const testPoint = { x: 575, y: 200 }
|
||||
const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
|
||||
const [clickOnSketch2] = scene.makeMouseHelpers(testPoint.x, testPoint.y + 80)
|
||||
|
||||
await test.step(`Delete loft`, async () => {
|
||||
// Check for loft
|
||||
await scene.expectPixelColor([89, 89, 89], testPoint, 15)
|
||||
await clickOnSketch1()
|
||||
await expect(page.locator('.cm-activeLine')).toHaveText(`
|
||||
|> circle({ center = [0, 0], radius = 30 }, %)
|
||||
`)
|
||||
await page.keyboard.press('Backspace')
|
||||
// Check for sketch 1
|
||||
await scene.expectPixelColor([254, 254, 254], testPoint, 15)
|
||||
})
|
||||
|
||||
await test.step('Delete sketch002', async () => {
|
||||
await page.waitForTimeout(1000)
|
||||
await clickOnSketch2()
|
||||
await expect(page.locator('.cm-activeLine')).toHaveText(`
|
||||
|> circle({ center = [0, 0], radius = 20 }, %)
|
||||
`)
|
||||
await page.keyboard.press('Backspace')
|
||||
// Check for plane001
|
||||
await scene.expectPixelColor([228, 228, 228], testPoint, 15)
|
||||
})
|
||||
|
||||
await test.step('Delete plane001', async () => {
|
||||
await page.waitForTimeout(1000)
|
||||
await clickOnSketch2()
|
||||
await expect(page.locator('.cm-activeLine')).toHaveText(`
|
||||
plane001 = offsetPlane('XZ', 50)
|
||||
`)
|
||||
await page.keyboard.press('Backspace')
|
||||
// Check for sketch 1
|
||||
await scene.expectPixelColor([254, 254, 254], testPoint, 15)
|
||||
})
|
||||
})
|
||||
|
||||
test(`Sweep point-and-click`, async ({
|
||||
context,
|
||||
page,
|
||||
homePage,
|
||||
scene,
|
||||
editor,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
const initialCode = `sketch001 = startSketchOn('YZ')
|
||||
|> circle({
|
||||
center = [0, 0],
|
||||
radius = 500
|
||||
}, %)
|
||||
sketch002 = startSketchOn('XZ')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> xLine(-500, %)
|
||||
|> tangentialArcTo([-2000, 500], %)
|
||||
`
|
||||
await context.addInitScript((initialCode) => {
|
||||
localStorage.setItem('persistCode', initialCode)
|
||||
}, initialCode)
|
||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||
await homePage.goToModelingScene()
|
||||
await scene.waitForExecutionDone()
|
||||
|
||||
// One dumb hardcoded screen pixel value
|
||||
const testPoint = { x: 700, y: 250 }
|
||||
const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
|
||||
const [clickOnSketch2] = scene.makeMouseHelpers(testPoint.x - 50, testPoint.y)
|
||||
const sweepDeclaration = 'sweep001 = sweep({ path = sketch002 }, sketch001)'
|
||||
|
||||
await test.step(`Look for sketch001`, async () => {
|
||||
await toolbar.closePane('code')
|
||||
await scene.expectPixelColor([53, 53, 53], testPoint, 15)
|
||||
})
|
||||
|
||||
await test.step(`Go through the command bar flow`, async () => {
|
||||
await toolbar.sweepButton.click()
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Sweep',
|
||||
currentArgKey: 'profile',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Path: '',
|
||||
Profile: '',
|
||||
},
|
||||
highlightedHeaderArg: 'profile',
|
||||
stage: 'arguments',
|
||||
})
|
||||
await clickOnSketch1()
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Sweep',
|
||||
currentArgKey: 'path',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Path: '',
|
||||
Profile: '1 face',
|
||||
},
|
||||
highlightedHeaderArg: 'path',
|
||||
stage: 'arguments',
|
||||
})
|
||||
await clickOnSketch2()
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Sweep',
|
||||
headerArguments: {
|
||||
Path: '1 face',
|
||||
Profile: '1 face',
|
||||
},
|
||||
stage: 'review',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
})
|
||||
|
||||
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
|
||||
await scene.expectPixelColor([135, 64, 73], testPoint, 15)
|
||||
await toolbar.openPane('code')
|
||||
await editor.expectEditor.toContain(sweepDeclaration)
|
||||
await editor.expectState({
|
||||
diagnostics: [],
|
||||
activeLines: [sweepDeclaration],
|
||||
highlightedCode: '',
|
||||
})
|
||||
await toolbar.closePane('code')
|
||||
})
|
||||
|
||||
await test.step('Delete sweep via feature tree selection', async () => {
|
||||
await toolbar.openPane('feature-tree')
|
||||
await page.waitForTimeout(500)
|
||||
const operationButton = await toolbar.getFeatureTreeOperation('Sweep', 0)
|
||||
await operationButton.click({ button: 'left' })
|
||||
await page.keyboard.press('Backspace')
|
||||
await page.waitForTimeout(500)
|
||||
await toolbar.closePane('feature-tree')
|
||||
await scene.expectPixelColor([53, 53, 53], testPoint, 15)
|
||||
})
|
||||
})
|
||||
|
||||
@ -1030,4 +1208,104 @@ extrude001 = extrude(40, sketch001)
|
||||
})
|
||||
await scene.expectPixelColor([49, 49, 49], testPoint, 15)
|
||||
})
|
||||
|
||||
await test.step('Delete shell via feature tree selection', async () => {
|
||||
await editor.closePane()
|
||||
const operationButton = await toolbar.getFeatureTreeOperation('Shell', 0)
|
||||
await operationButton.click({ button: 'left' })
|
||||
await page.keyboard.press('Backspace')
|
||||
await scene.expectPixelColor([99, 99, 99], testPoint, 15)
|
||||
})
|
||||
})
|
||||
|
||||
const shellSketchOnFacesCases = [
|
||||
`sketch001 = startSketchOn('XZ')
|
||||
|> circle({ center = [0, 0], radius = 100 }, %)
|
||||
|> extrude(100, %)
|
||||
|
||||
sketch002 = startSketchOn(sketch001, 'END')
|
||||
|> circle({ center = [0, 0], radius = 50 }, %)
|
||||
|> extrude(50, %)
|
||||
`,
|
||||
`sketch001 = startSketchOn('XZ')
|
||||
|> circle({ center = [0, 0], radius = 100 }, %)
|
||||
extrude001 = extrude(100, sketch001)
|
||||
|
||||
sketch002 = startSketchOn(extrude001, 'END')
|
||||
|> circle({ center = [0, 0], radius = 50 }, %)
|
||||
extrude002 = extrude(50, sketch002)
|
||||
`,
|
||||
]
|
||||
shellSketchOnFacesCases.forEach((initialCode, index) => {
|
||||
const hasExtrudesInPipe = index === 0
|
||||
test(`Shell point-and-click sketch on face (extrudes in pipes: ${hasExtrudesInPipe})`, async ({
|
||||
context,
|
||||
page,
|
||||
homePage,
|
||||
scene,
|
||||
editor,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
await context.addInitScript((initialCode) => {
|
||||
localStorage.setItem('persistCode', initialCode)
|
||||
}, initialCode)
|
||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||
await homePage.goToModelingScene()
|
||||
await scene.waitForExecutionDone()
|
||||
|
||||
// One dumb hardcoded screen pixel value
|
||||
const testPoint = { x: 550, y: 295 }
|
||||
const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
|
||||
const shellDeclaration = `shell001 = shell({ faces = ['end'], thickness = 5 }, ${
|
||||
hasExtrudesInPipe ? 'sketch002' : 'extrude002'
|
||||
})`
|
||||
|
||||
await test.step(`Look for the grey of the shape`, async () => {
|
||||
await toolbar.closePane('code')
|
||||
await scene.expectPixelColor([128, 128, 128], testPoint, 15)
|
||||
})
|
||||
|
||||
await test.step(`Go through the command bar flow, selecting a cap and keeping default thickness`, async () => {
|
||||
await toolbar.shellButton.click()
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
currentArgKey: 'selection',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Selection: '',
|
||||
Thickness: '',
|
||||
},
|
||||
highlightedHeaderArg: 'selection',
|
||||
commandName: 'Shell',
|
||||
})
|
||||
await clickOnCap()
|
||||
await page.waitForTimeout(500)
|
||||
await cmdBar.progressCmdBar()
|
||||
await page.waitForTimeout(500)
|
||||
await cmdBar.progressCmdBar()
|
||||
await page.waitForTimeout(500)
|
||||
await cmdBar.expectState({
|
||||
stage: 'review',
|
||||
headerArguments: {
|
||||
Selection: '1 cap',
|
||||
Thickness: '5',
|
||||
},
|
||||
commandName: 'Shell',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
})
|
||||
|
||||
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
|
||||
await toolbar.openPane('code')
|
||||
await editor.expectEditor.toContain(shellDeclaration)
|
||||
await editor.expectState({
|
||||
diagnostics: [],
|
||||
activeLines: [shellDeclaration],
|
||||
highlightedCode: '',
|
||||
})
|
||||
await toolbar.closePane('code')
|
||||
await scene.expectPixelColor([73, 73, 73], testPoint, 15)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -115,7 +115,7 @@ test(
|
||||
)
|
||||
|
||||
test(
|
||||
'yyyyyyyyy open a file in a project works and renders, open another file in different project with errors, it should clear the scene',
|
||||
'open a file in a project works and renders, open another file in different project with errors, it should clear the scene',
|
||||
{ tag: '@electron' },
|
||||
async ({ context, page }, testInfo) => {
|
||||
await context.folderSetupFn(async (dir) => {
|
||||
@ -199,7 +199,7 @@ test(
|
||||
)
|
||||
|
||||
test(
|
||||
'aaayyyyyyyy open a file in a project works and renders, open another file in different project that is empty, it should clear the scene',
|
||||
'open a file in a project works and renders, open another file in different project that is empty, it should clear the scene',
|
||||
{ tag: '@electron' },
|
||||
async ({ context, page }, testInfo) => {
|
||||
await context.folderSetupFn(async (dir) => {
|
||||
@ -276,7 +276,7 @@ test(
|
||||
)
|
||||
|
||||
test(
|
||||
'nooooooooooooo open a file in a project works and renders, open empty file, it should clear the scene',
|
||||
'open a file in a project works and renders, open empty file, it should clear the scene',
|
||||
{ tag: '@electron' },
|
||||
async ({ context, page }, testInfo) => {
|
||||
await context.folderSetupFn(async (dir) => {
|
||||
@ -1885,3 +1885,48 @@ test.fixme(
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'project name with foreign characters should open',
|
||||
{ tag: '@electron' },
|
||||
async ({ context, page }, testInfo) => {
|
||||
await context.folderSetupFn(async (dir) => {
|
||||
const bracketDir = path.join(dir, 'اَلْعَرَبِيَّةُ')
|
||||
await fsp.mkdir(bracketDir, { recursive: true })
|
||||
await fsp.copyFile(
|
||||
executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
|
||||
path.join(bracketDir, 'main.kcl')
|
||||
)
|
||||
|
||||
await fsp.writeFile(path.join(bracketDir, 'empty.kcl'), '')
|
||||
})
|
||||
|
||||
await page.setBodyDimensions({ width: 1200, height: 500 })
|
||||
const u = await getUtils(page)
|
||||
|
||||
page.on('console', console.log)
|
||||
|
||||
const pointOnModel = { x: 630, y: 280 }
|
||||
|
||||
await test.step('Opening the اَلْعَرَبِيَّةُ project should load the stream', async () => {
|
||||
// expect to see the text bracket
|
||||
await expect(page.getByText('اَلْعَرَبِيَّةُ')).toBeVisible()
|
||||
|
||||
await page.getByText('اَلْعَرَبِيَّةُ').click()
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).toBeEnabled({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
// gray at this pixel means the stream has loaded in the most
|
||||
// user way we can verify it (pixel color)
|
||||
await expect
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
|
||||
timeout: 10_000,
|
||||
})
|
||||
.toBeLessThan(15)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
@ -614,6 +614,38 @@ extrude001 = extrude(50, sketch001)
|
||||
await expect(gizmo).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test(`Refreshing the app doesn't cause the stream to pause on long-executing files`, async ({
|
||||
context,
|
||||
homePage,
|
||||
scene,
|
||||
toolbar,
|
||||
viewport,
|
||||
}) => {
|
||||
await context.folderSetupFn(async (dir) => {
|
||||
const legoDir = path.join(dir, 'lego')
|
||||
await fsp.mkdir(legoDir, { recursive: true })
|
||||
await fsp.copyFile(
|
||||
executorInputPath('lego.kcl'),
|
||||
path.join(legoDir, 'main.kcl')
|
||||
)
|
||||
})
|
||||
|
||||
await test.step(`Test setup`, async () => {
|
||||
await homePage.openProject('lego')
|
||||
await toolbar.closePane('code')
|
||||
})
|
||||
await test.step(`Waiting for the loading spinner to disappear`, async () => {
|
||||
await scene.loadingIndicator.waitFor({ state: 'detached' })
|
||||
})
|
||||
await test.step(`The part should start loading quickly, not waiting until execution is complete`, async () => {
|
||||
await scene.expectPixelColor(
|
||||
[143, 143, 143],
|
||||
{ x: (viewport?.width ?? 1200) / 2, y: (viewport?.height ?? 500) / 2 },
|
||||
15
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
async function clickExportButton(page: Page) {
|
||||
|
@ -39,8 +39,8 @@ test.describe('Sketch tests', () => {
|
||||
${startProfileAt1}
|
||||
|> arc({
|
||||
radius = screwRadius,
|
||||
angle_start = 0,
|
||||
angle_end = 360
|
||||
angleStart = 0,
|
||||
angleEnd = 360
|
||||
}, %)
|
||||
|
||||
part001 = startSketchOn('XY')
|
||||
@ -60,8 +60,8 @@ test.describe('Sketch tests', () => {
|
||||
|> yLine(wireOffset, %)
|
||||
|> arc({
|
||||
radius = wireRadius,
|
||||
angle_start = 0,
|
||||
angle_end = 180
|
||||
angleStart = 0,
|
||||
angleEnd = 180
|
||||
}, %)
|
||||
|> yLine(-wireOffset, %)
|
||||
|> xLine(-width / 4, %)
|
||||
@ -1323,3 +1323,85 @@ test.describe(`Sketching with offset planes`, () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Regression test for https://github.com/KittyCAD/modeling-app/issues/4891
|
||||
test.describe(`Click based selection don't brick the app when clicked out of range after format using cache`, () => {
|
||||
test(`Can select a line that reformmed after entering sketch mode`, async ({
|
||||
context,
|
||||
page,
|
||||
scene,
|
||||
toolbar,
|
||||
editor,
|
||||
homePage,
|
||||
}) => {
|
||||
// We seed the scene with a single offset plane
|
||||
await context.addInitScript(() => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> line([3.14, 3.14], %)
|
||||
|> arcTo({
|
||||
end = [4, 2],
|
||||
interior = [1, 2]
|
||||
}, %)
|
||||
`
|
||||
)
|
||||
})
|
||||
|
||||
await homePage.goToModelingScene()
|
||||
await scene.waitForExecutionDone()
|
||||
|
||||
await test.step(`format the code`, async () => {
|
||||
// doesn't contain condensed version
|
||||
await editor.expectEditor.not.toContain(
|
||||
`arcTo({ end = [4, 2], interior = [1, 2] }, %)`
|
||||
)
|
||||
// click the code to enter sketch mode
|
||||
await page.getByText(`arcTo`).click()
|
||||
// Format the code.
|
||||
await page.locator('#code-pane button:first-child').click()
|
||||
await page.locator('button:has-text("Format code")').click()
|
||||
})
|
||||
|
||||
await test.step(`Ensure the code reformatted`, async () => {
|
||||
await editor.expectEditor.toContain(
|
||||
`arcTo({ end = [4, 2], interior = [1, 2] }, %)`
|
||||
)
|
||||
})
|
||||
|
||||
const [arcClick, arcHover] = scene.makeMouseHelpers(699, 337)
|
||||
await test.step('Ensure we can hover the arc', async () => {
|
||||
await arcHover()
|
||||
|
||||
// Check that the code is highlighted
|
||||
await editor.expectState({
|
||||
activeLines: ["sketch001=startSketchOn('XZ')"],
|
||||
diagnostics: [],
|
||||
highlightedCode: 'arcTo({end = [4, 2], interior = [1, 2]}, %)',
|
||||
})
|
||||
})
|
||||
|
||||
await test.step('reset the selection', async () => {
|
||||
// Move the mouse out of the way
|
||||
await page.mouse.move(655, 337)
|
||||
|
||||
await editor.expectState({
|
||||
activeLines: ["sketch001=startSketchOn('XZ')"],
|
||||
diagnostics: [],
|
||||
highlightedCode: '',
|
||||
})
|
||||
})
|
||||
|
||||
await test.step('Ensure we can click the arc', async () => {
|
||||
await arcClick()
|
||||
|
||||
// Check that the code is highlighted
|
||||
await editor.expectState({
|
||||
activeLines: [],
|
||||
diagnostics: [],
|
||||
highlightedCode: 'arcTo({end = [4, 2], interior = [1, 2]}, %)',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
Before Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
Before Width: | Height: | Size: 65 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 66 KiB |
Before Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 152 KiB |
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 144 KiB |
Before Width: | Height: | Size: 130 KiB |
Before Width: | Height: | Size: 136 KiB |
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 128 KiB |
Before Width: | Height: | Size: 112 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 38 KiB |
@ -389,25 +389,25 @@ test.describe('Testing selections', () => {
|
||||
await expect(u.codeLocator).toContainText(`sketch005 = startSketchOn({
|
||||
plane = {
|
||||
origin = { x = 0, y = -50, z = 0 },
|
||||
x_axis = { x = 1, y = 0, z = 0 },
|
||||
y_axis = { x = 0, y = 0, z = 1 },
|
||||
z_axis = { x = 0, y = -1, z = 0 }
|
||||
xAxis = { x = 1, y = 0, z = 0 },
|
||||
yAxis = { x = 0, y = 0, z = 1 },
|
||||
zAxis = { x = 0, y = -1, z = 0 }
|
||||
}
|
||||
})`)
|
||||
await expect(u.codeLocator).toContainText(`sketch003 = startSketchOn({
|
||||
plane = {
|
||||
origin = { x = 116.53, y = 0, z = 163.25 },
|
||||
x_axis = { x = -0.81, y = 0, z = 0.58 },
|
||||
y_axis = { x = 0, y = -1, z = 0 },
|
||||
z_axis = { x = 0.58, y = 0, z = 0.81 }
|
||||
xAxis = { x = -0.81, y = 0, z = 0.58 },
|
||||
yAxis = { x = 0, y = -1, z = 0 },
|
||||
zAxis = { x = 0.58, y = 0, z = 0.81 }
|
||||
}
|
||||
})`)
|
||||
await expect(u.codeLocator).toContainText(`sketch002 = startSketchOn({
|
||||
plane = {
|
||||
origin = { x = -91.74, y = 0, z = 80.89 },
|
||||
x_axis = { x = -0.66, y = 0, z = -0.75 },
|
||||
y_axis = { x = 0, y = -1, z = 0 },
|
||||
z_axis = { x = -0.75, y = 0, z = 0.66 }
|
||||
xAxis = { x = -0.66, y = 0, z = -0.75 },
|
||||
yAxis = { x = 0, y = -1, z = 0 },
|
||||
zAxis = { x = -0.75, y = 0, z = 0.66 }
|
||||
}
|
||||
})`)
|
||||
|
||||
|
@ -156,13 +156,13 @@ test.describe('Text-to-CAD tests', () => {
|
||||
const cmdSearchBar = page.getByPlaceholder('Search commands')
|
||||
await expect(cmdSearchBar).toBeVisible()
|
||||
|
||||
const textToCadCommand = page.getByText('Text-to-CAD')
|
||||
const textToCadCommand = page.getByRole('option', { name: 'Text-to-CAD' })
|
||||
await expect(textToCadCommand.first()).toBeVisible()
|
||||
// Click the Text-to-CAD command
|
||||
await textToCadCommand.first().click()
|
||||
|
||||
// Enter the prompt.
|
||||
const prompt = page.getByText('Prompt')
|
||||
const prompt = page.getByRole('textbox', { name: 'Prompt' })
|
||||
await expect(prompt.first()).toBeVisible()
|
||||
|
||||
// Type the prompt.
|
||||
@ -224,13 +224,13 @@ test.describe('Text-to-CAD tests', () => {
|
||||
const cmdSearchBar = page.getByPlaceholder('Search commands')
|
||||
await expect(cmdSearchBar).toBeVisible()
|
||||
|
||||
const textToCadCommand = page.getByText('Text-to-CAD')
|
||||
const textToCadCommand = page.getByRole('option', { name: 'Text-to-CAD' })
|
||||
await expect(textToCadCommand.first()).toBeVisible()
|
||||
// Click the Text-to-CAD command
|
||||
await textToCadCommand.first().click()
|
||||
|
||||
// Enter the prompt.
|
||||
const prompt = page.getByText('Prompt')
|
||||
const prompt = page.getByRole('textbox', { name: 'Prompt' })
|
||||
await expect(prompt.first()).toBeVisible()
|
||||
|
||||
const badPrompt = 'akjsndladf lajbhflauweyfaaaljhr472iouafyvsssssss'
|
||||
@ -314,13 +314,13 @@ test.describe('Text-to-CAD tests', () => {
|
||||
const cmdSearchBar = page.getByPlaceholder('Search commands')
|
||||
await expect(cmdSearchBar).toBeVisible()
|
||||
|
||||
const textToCadCommand = page.getByText('Text-to-CAD')
|
||||
const textToCadCommand = page.getByRole('option', { name: 'Text-to-CAD' })
|
||||
await expect(textToCadCommand.first()).toBeVisible()
|
||||
// Click the Text-to-CAD command
|
||||
await textToCadCommand.first().click()
|
||||
|
||||
// Enter the prompt.
|
||||
const prompt = page.getByText('Prompt')
|
||||
const prompt = page.getByRole('textbox', { name: 'Prompt' })
|
||||
await expect(prompt.first()).toBeVisible()
|
||||
|
||||
const badPrompt = 'akjsndladflajbhflauweyf15;'
|
||||
@ -392,13 +392,13 @@ test.describe('Text-to-CAD tests', () => {
|
||||
const cmdSearchBar = page.getByPlaceholder('Search commands')
|
||||
await expect(cmdSearchBar).toBeVisible()
|
||||
|
||||
const textToCadCommand = page.getByText('Text-to-CAD')
|
||||
const textToCadCommand = page.getByRole('option', { name: 'Text-to-CAD' })
|
||||
await expect(textToCadCommand.first()).toBeVisible()
|
||||
// Click the Text-to-CAD command
|
||||
await textToCadCommand.first().click()
|
||||
|
||||
// Enter the prompt.
|
||||
const prompt = page.getByText('Prompt')
|
||||
const prompt = page.getByRole('textbox', { name: 'Prompt' })
|
||||
await expect(prompt.first()).toBeVisible()
|
||||
|
||||
// Type the prompt.
|
||||
@ -604,7 +604,7 @@ async function sendPromptFromCommandBar(page: Page, promptStr: string) {
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
// Enter the prompt.
|
||||
const prompt = page.getByText('Prompt')
|
||||
const prompt = page.getByRole('textbox', { name: 'Prompt' })
|
||||
await expect(prompt.first()).toBeVisible()
|
||||
|
||||
// Type the prompt.
|
||||
|
18
flake.lock
generated
@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1721933792,
|
||||
"narHash": "sha256-zYVwABlQnxpbaHMfX6Wt9jhyQstFYwN2XjleOJV3VVg=",
|
||||
"lastModified": 1736320768,
|
||||
"narHash": "sha256-nIYdTAiKIGnFNugbomgBJR+Xv5F1ZQU+HfaBqJKroC0=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "2122a9b35b35719ad9a395fe783eabb092df01b1",
|
||||
"rev": "4bc9c909d9ac828a039f288cf872d16d38185db8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@ -18,11 +18,11 @@
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1718428119,
|
||||
"narHash": "sha256-WdWDpNaq6u1IPtxtYHHWpl5BmabtpmLnMAx0RdJ/vo8=",
|
||||
"lastModified": 1728538411,
|
||||
"narHash": "sha256-f0SBJz1eZ2yOuKUr5CA9BHULGXVSn6miBuUWdTyhUhU=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e6cea36f83499eb4e9cd184c8a8e823296b50ad5",
|
||||
"rev": "b69de56fac8c2b6f8fd27f2eca01dcda8e0a4221",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@ -43,11 +43,11 @@
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1721960387,
|
||||
"narHash": "sha256-o21ax+745ETGXrcgc/yUuLw1SI77ymp3xEpJt+w/kks=",
|
||||
"lastModified": 1736476219,
|
||||
"narHash": "sha256-+qyv3QqdZCdZ3cSO/cbpEY6tntyYjfe1bB12mdpNFaY=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "9cbf831c5b20a53354fc12758abd05966f9f1699",
|
||||
"rev": "de30cc5963da22e9742bbbbb9a3344570ed237b9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
8
interface.d.ts
vendored
@ -11,6 +11,13 @@ export interface IElectronAPI {
|
||||
open: typeof dialog.showOpenDialog
|
||||
save: typeof dialog.showSaveDialog
|
||||
openExternal: typeof shell.openExternal
|
||||
takeElectronWindowScreenshot: ({
|
||||
width,
|
||||
height,
|
||||
}: {
|
||||
width: number
|
||||
height: number
|
||||
}) => Promise<string>
|
||||
showInFolder: typeof shell.showItemInFolder
|
||||
/** Require to be called first before {@link loginWithDeviceFlow} */
|
||||
startDeviceFlow: (host: string) => Promise<string>
|
||||
@ -86,5 +93,6 @@ export interface IElectronAPI {
|
||||
declare global {
|
||||
interface Window {
|
||||
electron: IElectronAPI
|
||||
openExternalLink: (e: React.MouseEvent<HTMLAnchorElement>) => void
|
||||
}
|
||||
}
|
||||
|
26
package.json
@ -15,18 +15,18 @@
|
||||
"@codemirror/autocomplete": "^6.17.0",
|
||||
"@codemirror/commands": "^6.6.0",
|
||||
"@codemirror/language": "^6.10.3",
|
||||
"@codemirror/lint": "^6.8.1",
|
||||
"@codemirror/lint": "^6.8.4",
|
||||
"@codemirror/search": "^6.5.6",
|
||||
"@codemirror/state": "^6.4.1",
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@csstools/postcss-oklab-function": "^4.0.2",
|
||||
"@csstools/postcss-oklab-function": "^4.0.7",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.5.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@headlessui/react": "^1.7.19",
|
||||
"@headlessui/tailwindcss": "^0.2.0",
|
||||
"@kittycad/lib": "2.0.7",
|
||||
"@kittycad/lib": "2.0.13",
|
||||
"@lezer/highlight": "^1.2.1",
|
||||
"@lezer/lr": "^1.4.1",
|
||||
"@react-hook/resize-observer": "^2.0.1",
|
||||
@ -52,13 +52,13 @@
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-hotkeys-hook": "^4.5.1",
|
||||
"react-hotkeys-hook": "^4.6.1",
|
||||
"react-json-view": "^1.21.3",
|
||||
"react-modal": "^3.16.1",
|
||||
"react-modal": "^3.16.3",
|
||||
"react-modal-promise": "^1.0.2",
|
||||
"react-router-dom": "^6.28.0",
|
||||
"sketch-helpers": "^0.0.4",
|
||||
"three": "^0.166.1",
|
||||
"three": "^0.172.0",
|
||||
"ua-parser-js": "^1.0.37",
|
||||
"uuid": "^11.0.2",
|
||||
"vscode-jsonrpc": "^8.2.1",
|
||||
@ -91,8 +91,8 @@
|
||||
"build:wasm": "yarn wasm-prep && cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings && cd ../.. && yarn isomorphic-copy-wasm && 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 src e2e packages/codemirror-lsp-client",
|
||||
"lint": "eslint --max-warnings 0 src e2e packages/codemirror-lsp-client",
|
||||
"lint-fix": "eslint --fix --ext .ts --ext .tsx src e2e packages/codemirror-lsp-client/src",
|
||||
"lint": "eslint --max-warnings 0 --ext .ts --ext .tsx src e2e packages/codemirror-lsp-client/src",
|
||||
"files:set-version": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json",
|
||||
"files:set-notes": "./scripts/set-files-notes.sh",
|
||||
"files:flip-to-nightly": "./scripts/flip-files-to-nightly.sh",
|
||||
@ -166,13 +166,11 @@
|
||||
"@types/react": "^18.3.4",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@types/react-modal": "^3.16.3",
|
||||
"@types/three": "^0.163.0",
|
||||
"@types/three": "^0.172.0",
|
||||
"@types/ua-parser-js": "^0.7.39",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@types/wicg-file-system-access": "^2023.10.5",
|
||||
"@types/ws": "^8.5.13",
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||
"@typescript-eslint/parser": "^5.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.0",
|
||||
"@vitest/web-worker": "^1.5.0",
|
||||
"@xstate/cli": "^0.5.17",
|
||||
@ -182,11 +180,12 @@
|
||||
"electron-builder": "24.13.3",
|
||||
"electron-notarize": "1.2.2",
|
||||
"eslint": "^8.0.1",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-css-modules": "^2.12.0",
|
||||
"eslint-plugin-import": "^2.30.0",
|
||||
"eslint-plugin-jest": "^28.10.0",
|
||||
"eslint-plugin-react": "^7.37.3",
|
||||
"eslint-plugin-suggest-no-throw": "^1.0.0",
|
||||
"happy-dom": "^15.11.7",
|
||||
"happy-dom": "^16.3.0",
|
||||
"http-server": "^14.1.1",
|
||||
"husky": "^9.1.5",
|
||||
"kill-port": "^2.0.1",
|
||||
@ -200,6 +199,7 @@
|
||||
"tailwindcss": "^3.4.1",
|
||||
"ts-node": "^10.0.0",
|
||||
"typescript": "^5.7.2",
|
||||
"typescript-eslint": "^8.19.1",
|
||||
"vite": "^5.4.6",
|
||||
"vite-plugin-package-version": "^1.1.0",
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
|
@ -17,7 +17,7 @@
|
||||
|
||||
statement[@isGroup=Statement] {
|
||||
ImportStatement { kw<"import"> ImportItems ImportFrom String } |
|
||||
FunctionDeclaration { kw<"export">? kw<"fn"> VariableDefinition Equals ParamList Arrow Body } |
|
||||
FunctionDeclaration { kw<"export">? kw<"fn"> VariableDefinition Equals? ParamList Arrow? Body } |
|
||||
VariableDeclaration { kw<"export">? (kw<"var"> | kw<"let"> | kw<"const">)? VariableDefinition Equals expression } |
|
||||
ReturnStatement { kw<"return"> expression } |
|
||||
ExpressionStatement { expression }
|
||||
@ -57,7 +57,7 @@ expression[@isGroup=Expression] {
|
||||
|
||||
UnaryOp { AddOp | BangOp }
|
||||
|
||||
ObjectProperty { PropertyName ":" expression }
|
||||
ObjectProperty { PropertyName (":" | Equals) expression }
|
||||
|
||||
ArgumentList { "(" commaSep<expression> ")" }
|
||||
|
||||
@ -85,7 +85,7 @@ commaSep1NoTrailingComma<term> { term ("," term)* }
|
||||
@tokens {
|
||||
String[isolate] { "'" ("\\" _ | !['\\])* "'" | '"' ("\\" _ | !["\\])* '"' }
|
||||
|
||||
Number { "." @digit+ | @digit+ ("." @digit*)? }
|
||||
Number { "." @digit+ | @digit+ ("." @digit+)? }
|
||||
@precedence { Number, "." }
|
||||
|
||||
AddOp { "+" | "-" }
|
||||
|
60
packages/codemirror-lang-kcl/test/fn.txt
Normal file
@ -0,0 +1,60 @@
|
||||
# full
|
||||
|
||||
fn two = () => {
|
||||
return 2
|
||||
}
|
||||
|
||||
==>
|
||||
|
||||
Program(FunctionDeclaration(fn,
|
||||
VariableDefinition,
|
||||
Equals,
|
||||
ParamList,
|
||||
Arrow,
|
||||
Body(ReturnStatement(return,
|
||||
Number))))
|
||||
|
||||
# = is optional
|
||||
|
||||
fn one () => {
|
||||
return 1
|
||||
}
|
||||
|
||||
==>
|
||||
|
||||
Program(FunctionDeclaration(fn,
|
||||
VariableDefinition,
|
||||
ParamList,
|
||||
Arrow,
|
||||
Body(ReturnStatement(return,
|
||||
Number))))
|
||||
|
||||
# => is optional
|
||||
|
||||
fn one = () {
|
||||
return 1
|
||||
}
|
||||
|
||||
==>
|
||||
|
||||
Program(FunctionDeclaration(fn,
|
||||
VariableDefinition,
|
||||
Equals,
|
||||
ParamList,
|
||||
Body(ReturnStatement(return,
|
||||
Number))))
|
||||
|
||||
# terse
|
||||
|
||||
fn two() {
|
||||
return 2
|
||||
}
|
||||
|
||||
==>
|
||||
|
||||
Program(FunctionDeclaration(fn,
|
||||
VariableDefinition,
|
||||
ParamList,
|
||||
Body(ReturnStatement(return,
|
||||
Number))))
|
||||
|
20
packages/codemirror-lang-kcl/test/key.txt
Normal file
@ -0,0 +1,20 @@
|
||||
# colon (deprecated)
|
||||
|
||||
x = { k: 123 }
|
||||
|
||||
==>
|
||||
Program(VariableDeclaration(VariableDefinition,
|
||||
Equals,
|
||||
ObjectExpression(ObjectProperty(PropertyName,
|
||||
Number))))
|
||||
|
||||
# equal
|
||||
|
||||
x = { k = 123 }
|
||||
|
||||
==>
|
||||
Program(VariableDeclaration(VariableDefinition,
|
||||
Equals,
|
||||
ObjectExpression(ObjectProperty(PropertyName,
|
||||
Equals,
|
||||
Number))))
|
43
packages/codemirror-lang-kcl/test/range.txt
Normal file
@ -0,0 +1,43 @@
|
||||
# spaced
|
||||
|
||||
a = [0 .. 1]
|
||||
|
||||
==>
|
||||
Program(VariableDeclaration(VariableDefinition,
|
||||
Equals,
|
||||
ArrayExpression(IntegerRange(Number,
|
||||
Number))))
|
||||
|
||||
# compact
|
||||
|
||||
a = [0..1]
|
||||
|
||||
==>
|
||||
Program(VariableDeclaration(VariableDefinition,
|
||||
Equals,
|
||||
ArrayExpression(IntegerRange(Number,
|
||||
Number))))
|
||||
|
||||
# expr spaced
|
||||
|
||||
a = [start .. start + 10]
|
||||
|
||||
==>
|
||||
Program(VariableDeclaration(VariableDefinition,
|
||||
Equals,
|
||||
ArrayExpression(IntegerRange(VariableName,
|
||||
BinaryExpression(VariableName,
|
||||
AddOp,
|
||||
Number)))))
|
||||
|
||||
# expr compact
|
||||
|
||||
a = [start..start + 10]
|
||||
|
||||
==>
|
||||
Program(VariableDeclaration(VariableDefinition,
|
||||
Equals,
|
||||
ArrayExpression(IntegerRange(VariableName,
|
||||
BinaryExpression(VariableName,
|
||||
AddOp,
|
||||
Number)))))
|
@ -42,7 +42,7 @@ export default class StreamDemuxer extends Queue<Uint8Array> {
|
||||
// try to parse the content-length from the headers
|
||||
const length = parseInt(match[1])
|
||||
|
||||
if (isNaN(length))
|
||||
if (Number.isNaN(length))
|
||||
return Promise.reject(new Error('invalid content length'))
|
||||
|
||||
// slice the headers since we now have the content length
|
||||
|
@ -368,13 +368,20 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
sortText,
|
||||
filterText,
|
||||
}) => {
|
||||
const detailText = [
|
||||
deprecated ? 'Deprecated' : undefined,
|
||||
labelDetails ? labelDetails.detail : detail,
|
||||
]
|
||||
// Don't let undefined appear.
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
const completion: Completion & {
|
||||
filterText: string
|
||||
sortText?: string
|
||||
apply: string
|
||||
} = {
|
||||
label,
|
||||
detail: labelDetails ? labelDetails.detail : detail,
|
||||
detail: detailText,
|
||||
apply: label,
|
||||
type: kind && CompletionItemKindMap[kind].toLowerCase(),
|
||||
sortText: sortText ?? label,
|
||||
@ -382,7 +389,11 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
}
|
||||
if (documentation) {
|
||||
completion.info = () => {
|
||||
const htmlString = formatMarkdownContents(documentation)
|
||||
const deprecatedHtml = deprecated
|
||||
? '<p><strong>Deprecated</strong></p>'
|
||||
: ''
|
||||
const htmlString =
|
||||
deprecatedHtml + formatMarkdownContents(documentation)
|
||||
const htmlNode = document.createElement('div')
|
||||
htmlNode.style.display = 'contents'
|
||||
htmlNode.innerHTML = htmlString
|
||||
|
@ -32,10 +32,9 @@ export default defineConfig({
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'Google Chrome',
|
||||
name: 'chromium',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
channel: 'chrome',
|
||||
contextOptions: {
|
||||
/* Chromium is the only one with these permission types */
|
||||
permissions: ['clipboard-write', 'clipboard-read'],
|
||||
|
@ -1230,12 +1230,13 @@ export class SceneEntities {
|
||||
// lee: Well, it appears all our code in sceneEntities each act as their own
|
||||
// kind of classes. In this case, I'll keep utility functions pertaining to
|
||||
// circle3Point here. Feel free to extract as needed.
|
||||
entryDraftCircle3Point = async (
|
||||
entryDraftCircle3Point = (
|
||||
done: () => void,
|
||||
startSketchOnASTNodePath: PathToNode,
|
||||
forward: Vector3,
|
||||
up: Vector3,
|
||||
sketchOrigin: Vector3
|
||||
) => {
|
||||
): (() => void) => {
|
||||
// lee: Not a fan we need to re-iterate this dummy object all over the place
|
||||
// just to get the scale but okie dokie.
|
||||
const dummy = new Mesh()
|
||||
@ -1374,13 +1375,13 @@ export class SceneEntities {
|
||||
groupOfDrafts.add(groupCircle)
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
this.scene.remove(groupOfDrafts)
|
||||
}
|
||||
|
||||
// The target of our dragging
|
||||
let target: Object3D | undefined = undefined
|
||||
|
||||
const cleanupFn = () => {
|
||||
this.scene.remove(groupOfDrafts)
|
||||
}
|
||||
|
||||
sceneInfra.setCallbacks({
|
||||
async onDrag(args) {
|
||||
const draftPointsIntersected = args.intersects.filter(
|
||||
@ -1444,9 +1445,11 @@ export class SceneEntities {
|
||||
await kclManager.executeAstMock(astSnapshot)
|
||||
await codeManager.updateEditorWithAstAndWriteToFile(astSnapshot)
|
||||
|
||||
sceneInfra.modelingSend({ type: 'circle3PointsFinished', cleanup })
|
||||
done()
|
||||
},
|
||||
})
|
||||
|
||||
return cleanupFn
|
||||
}
|
||||
setupDraftCircle = async (
|
||||
sketchPathToNode: PathToNode,
|
||||
|
@ -75,6 +75,7 @@ function CommandBarTextareaInput({
|
||||
target.selectionStart = selectionStart + 1
|
||||
target.selectionEnd = selectionStart + 1
|
||||
} else if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
formRef.current?.dispatchEvent(
|
||||
new Event('submit', { bubbles: true })
|
||||
)
|
||||
|
@ -157,39 +157,38 @@ export const ModelingMachineProvider = ({
|
||||
'enable copilot': () => {
|
||||
editorManager.setCopilotEnabled(true)
|
||||
},
|
||||
// tsc reports this typing as perfectly fine, but eslint is complaining.
|
||||
// It's actually nonsensical, so I'm quieting.
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
'sketch exit execute': async ({
|
||||
context: { store },
|
||||
}): Promise<void> => {
|
||||
// When cancelling the sketch mode we should disable sketch mode within the engine.
|
||||
await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'sketch_mode_disable' },
|
||||
})
|
||||
|
||||
sceneInfra.camControls.syncDirection = 'clientToEngine'
|
||||
|
||||
if (cameraProjection.current === 'perspective') {
|
||||
await sceneInfra.camControls.snapToPerspectiveBeforeHandingBackControlToEngine()
|
||||
}
|
||||
|
||||
sceneInfra.camControls.syncDirection = 'engineToClient'
|
||||
|
||||
store.videoElement?.pause()
|
||||
|
||||
return kclManager
|
||||
.executeCode()
|
||||
.then(() => {
|
||||
if (engineCommandManager.engineConnection?.idleMode) return
|
||||
|
||||
store.videoElement?.play().catch((e) => {
|
||||
console.warn('Video playing was prevented', e)
|
||||
})
|
||||
'sketch exit execute': ({ context: { store } }) => {
|
||||
// TODO: Remove this async callback. For some reason eslint wouldn't
|
||||
// let me disable @typescript-eslint/no-misused-promises for the line.
|
||||
;(async () => {
|
||||
// When cancelling the sketch mode we should disable sketch mode within the engine.
|
||||
await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'sketch_mode_disable' },
|
||||
})
|
||||
.catch(reportRejection)
|
||||
|
||||
sceneInfra.camControls.syncDirection = 'clientToEngine'
|
||||
|
||||
if (cameraProjection.current === 'perspective') {
|
||||
await sceneInfra.camControls.snapToPerspectiveBeforeHandingBackControlToEngine()
|
||||
}
|
||||
|
||||
sceneInfra.camControls.syncDirection = 'engineToClient'
|
||||
|
||||
store.videoElement?.pause()
|
||||
|
||||
return kclManager
|
||||
.executeCode()
|
||||
.then(() => {
|
||||
if (engineCommandManager.engineConnection?.idleMode) return
|
||||
|
||||
store.videoElement?.play().catch((e) => {
|
||||
console.warn('Video playing was prevented', e)
|
||||
})
|
||||
})
|
||||
.catch(reportRejection)
|
||||
})().catch(reportRejection)
|
||||
},
|
||||
'Set mouse state': assign(({ context, event }) => {
|
||||
if (event.type !== 'Set mouse state') return {}
|
||||
@ -271,6 +270,7 @@ export const ModelingMachineProvider = ({
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_center_to_selection',
|
||||
camera_movement: 'vantage',
|
||||
},
|
||||
})
|
||||
.catch(reportRejection)
|
||||
|
@ -97,6 +97,7 @@ export const KclEditorPane = () => {
|
||||
if (!editorIsMounted || !lastSelectionEvent || !editorManager.editorView) {
|
||||
return
|
||||
}
|
||||
|
||||
editorManager.editorView.dispatch({
|
||||
selection: lastSelectionEvent.codeMirrorSelection,
|
||||
annotations: [modelingMachineEvent, Transaction.addToHistory.of(false)],
|
||||
|
@ -218,20 +218,6 @@ export const Stream = () => {
|
||||
}
|
||||
}, [IDLE, streamState])
|
||||
|
||||
/**
|
||||
* Play the vid
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!kclManager.isExecuting) {
|
||||
setTimeout(() => {
|
||||
// execute in the next event loop
|
||||
videoRef.current?.play().catch((e) => {
|
||||
console.warn('Video playing was prevented', e, videoRef.current)
|
||||
})
|
||||
})
|
||||
}
|
||||
}, [kclManager.isExecuting])
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
typeof window === 'undefined' ||
|
||||
@ -243,9 +229,15 @@ export const Stream = () => {
|
||||
|
||||
// The browser complains if we try to load a new stream without pausing first.
|
||||
// Do not immediately play the stream!
|
||||
// we instead use a setTimeout to play the stream in the next event loop
|
||||
try {
|
||||
videoRef.current.srcObject = mediaStream
|
||||
videoRef.current.pause()
|
||||
setTimeout(() => {
|
||||
videoRef.current?.play().catch((e) => {
|
||||
console.warn('Video playing was prevented', e, videoRef.current)
|
||||
})
|
||||
})
|
||||
} catch (e) {
|
||||
console.warn('Attempted to pause stream while play was still loading', e)
|
||||
}
|
||||
|
@ -470,6 +470,17 @@ export function ToastPromptToEditCadSuccess({
|
||||
onClick={() => {
|
||||
sendTelemetry(modelId, 'accepted', token).catch(reportRejection)
|
||||
toast.dismiss(toastId)
|
||||
|
||||
// Write new content to disk since they have accepted.
|
||||
codeManager
|
||||
.writeToFile()
|
||||
.then(() => {
|
||||
// no-op
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error('Failed to save prompt-to-edit to disk')
|
||||
console.error(e)
|
||||
})
|
||||
}}
|
||||
>
|
||||
Accept
|
||||
|
@ -150,4 +150,31 @@ describe('ToastUpdate tests', () => {
|
||||
expect(restartButton).toBeEnabled()
|
||||
expect(dismissButton).toBeEnabled()
|
||||
})
|
||||
|
||||
test('Happy path: external links render correctly', () => {
|
||||
const releaseNotesWithBreakingChanges = `
|
||||
## Some markdown release notes
|
||||
- [Zoo](https://zoo.dev/)
|
||||
`
|
||||
const onRestart = vi.fn()
|
||||
const onDismiss = vi.fn()
|
||||
|
||||
render(
|
||||
<ToastUpdate
|
||||
onRestart={onRestart}
|
||||
onDismiss={onDismiss}
|
||||
version={testData.version}
|
||||
releaseNotes={releaseNotesWithBreakingChanges}
|
||||
/>
|
||||
)
|
||||
|
||||
// Locators and other constants
|
||||
const zooDev = screen.getByText('Zoo', {
|
||||
selector: 'a',
|
||||
})
|
||||
|
||||
expect(zooDev).toHaveAttribute('href', 'https://zoo.dev/')
|
||||
expect(zooDev).toHaveAttribute('target', '_blank')
|
||||
expect(zooDev).toHaveAttribute('onClick')
|
||||
})
|
||||
})
|
||||
|
@ -1,8 +1,9 @@
|
||||
import toast from 'react-hot-toast'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||
import { Marked } from '@ts-stack/markdown'
|
||||
import { escape, Marked, MarkedOptions, unescape } from '@ts-stack/markdown'
|
||||
import { getReleaseUrl } from 'routes/Settings'
|
||||
import { SafeRenderer } from 'lib/markdown'
|
||||
|
||||
export function ToastUpdate({
|
||||
version,
|
||||
@ -19,6 +20,14 @@ export function ToastUpdate({
|
||||
?.toLocaleLowerCase()
|
||||
.includes('breaking')
|
||||
|
||||
const markedOptions: MarkedOptions = {
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
sanitize: true,
|
||||
unescape,
|
||||
escape,
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="inset-0 z-50 grid place-content-center rounded bg-chalkboard-110/50 shadow-md">
|
||||
<div className="max-w-3xl min-w-[35rem] p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
|
||||
@ -58,9 +67,8 @@ export function ToastUpdate({
|
||||
className="parsed-markdown py-2 px-4 mt-2 border-t border-chalkboard-30 dark:border-chalkboard-60 max-h-60 overflow-y-auto"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: Marked.parse(releaseNotes, {
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
sanitize: true,
|
||||
renderer: new SafeRenderer(markedOptions),
|
||||
...markedOptions,
|
||||
}),
|
||||
}}
|
||||
></div>
|
||||
|
@ -1,42 +0,0 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { vi } from 'vitest'
|
||||
import { UpdaterModal } from './UpdaterModal'
|
||||
|
||||
describe('UpdaterModal tests', () => {
|
||||
test('Renders the modal', () => {
|
||||
const callback = vi.fn()
|
||||
const data = {
|
||||
version: '1.2.3',
|
||||
date: '2021-22-23T21:22:23Z',
|
||||
body: 'This is the body.',
|
||||
}
|
||||
|
||||
render(
|
||||
<UpdaterModal
|
||||
isOpen={true}
|
||||
onReject={() => {}}
|
||||
onResolve={callback}
|
||||
instanceId=""
|
||||
open={false}
|
||||
close={(res) => {}}
|
||||
version={data.version}
|
||||
date={data.date}
|
||||
body={data.body}
|
||||
/>
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('update-version')).toHaveTextContent(data.version)
|
||||
|
||||
const updateButton = screen.getByTestId('update-button-update')
|
||||
expect(updateButton).toBeEnabled()
|
||||
fireEvent.click(updateButton)
|
||||
expect(callback.mock.calls).toHaveLength(1)
|
||||
expect(callback.mock.lastCall[0]).toEqual({ wantUpdate: true })
|
||||
|
||||
const cancelButton = screen.getByTestId('update-button-cancel')
|
||||
expect(cancelButton).toBeEnabled()
|
||||
fireEvent.click(cancelButton)
|
||||
expect(callback.mock.calls).toHaveLength(2)
|
||||
expect(callback.mock.lastCall[0]).toEqual({ wantUpdate: false })
|
||||
})
|
||||
})
|
@ -1,87 +0,0 @@
|
||||
import { create, InstanceProps } from 'react-modal-promise'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import { Logo } from './Logo'
|
||||
import { Marked } from '@ts-stack/markdown'
|
||||
|
||||
type ModalResolve = {
|
||||
wantUpdate: boolean
|
||||
}
|
||||
|
||||
type ModalReject = boolean
|
||||
|
||||
type UpdaterModalProps = InstanceProps<ModalResolve, ModalReject> & {
|
||||
version: string
|
||||
date?: string
|
||||
body?: string
|
||||
}
|
||||
|
||||
export const createUpdaterModal = create<
|
||||
UpdaterModalProps,
|
||||
ModalResolve,
|
||||
ModalReject
|
||||
>
|
||||
|
||||
export const UpdaterModal = ({
|
||||
onResolve,
|
||||
version,
|
||||
date,
|
||||
body,
|
||||
}: UpdaterModalProps) => (
|
||||
<div className="fixed inset-0 z-50 grid place-content-center bg-chalkboard-110/50">
|
||||
<div className="max-w-3xl min-w-[45rem] p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
|
||||
<div className="flex items-center">
|
||||
<h1 className="flex-grow text-3xl font-bold">New version available!</h1>
|
||||
<Logo className="h-9" />
|
||||
</div>
|
||||
<div className="my-4 flex items-baseline">
|
||||
<span
|
||||
className="px-3 py-1 text-xl rounded-full bg-energy-10 text-energy-80"
|
||||
data-testid="update-version"
|
||||
>
|
||||
v{version}
|
||||
</span>
|
||||
<span className="ml-4 text-sm text-gray-400">Published on {date}</span>
|
||||
</div>
|
||||
{/* TODO: fix list bullets */}
|
||||
{body && (
|
||||
<div
|
||||
className="my-4 max-h-60 overflow-y-auto"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: Marked.parse(body, {
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
sanitize: true,
|
||||
}),
|
||||
}}
|
||||
></div>
|
||||
)}
|
||||
<div className="flex justify-between">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() => onResolve({ wantUpdate: false })}
|
||||
iconStart={{
|
||||
icon: 'close',
|
||||
bgClassName: 'bg-destroy-80',
|
||||
iconClassName: 'text-destroy-20 group-hover:text-destroy-10',
|
||||
}}
|
||||
className="hover:border-destroy-40 hover:bg-destroy-10/50 dark:hover:bg-destroy-80/50"
|
||||
data-testid="update-button-cancel"
|
||||
>
|
||||
Not now
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() => onResolve({ wantUpdate: true })}
|
||||
iconStart={{
|
||||
icon: 'arrowRight',
|
||||
bgClassName: 'dark:bg-chalkboard-80',
|
||||
}}
|
||||
className="dark:hover:bg-chalkboard-80/50"
|
||||
data-testid="update-button-update"
|
||||
>
|
||||
Update
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
@ -40,6 +40,7 @@ export const setDiagnosticsEvent = setDiagnosticsAnnotation.of(true)
|
||||
export default class EditorManager {
|
||||
private _copilotEnabled: boolean = true
|
||||
|
||||
private _isAllTextSelected: boolean = false
|
||||
private _isShiftDown: boolean = false
|
||||
private _selectionRanges: Selections = {
|
||||
otherSelections: [],
|
||||
@ -117,6 +118,10 @@ export default class EditorManager {
|
||||
})
|
||||
}
|
||||
|
||||
get isAllTextSelected() {
|
||||
return this._isAllTextSelected
|
||||
}
|
||||
|
||||
get editorView(): EditorView | null {
|
||||
return this._editorView
|
||||
}
|
||||
@ -129,6 +134,21 @@ export default class EditorManager {
|
||||
this._isShiftDown = isShiftDown
|
||||
}
|
||||
|
||||
private selectionsWithSafeEnds(
|
||||
selection: Array<Selection['codeRef']['range']>
|
||||
): Array<[number, number]> {
|
||||
if (!this._editorView) {
|
||||
return selection.map((s): [number, number] => {
|
||||
return [s[0], s[1]]
|
||||
})
|
||||
}
|
||||
|
||||
return selection.map((s): [number, number] => {
|
||||
const safeEnd = Math.min(s[1], this._editorView?.state.doc.length || s[1])
|
||||
return [s[0], safeEnd]
|
||||
})
|
||||
}
|
||||
|
||||
set selectionRanges(selectionRanges: Selections) {
|
||||
this._selectionRanges = selectionRanges
|
||||
}
|
||||
@ -154,14 +174,9 @@ export default class EditorManager {
|
||||
}
|
||||
|
||||
setHighlightRange(range: Array<Selection['codeRef']['range']>): void {
|
||||
this._highlightRange = range.map((s): [number, number] => {
|
||||
return [s[0], s[1]]
|
||||
})
|
||||
const selectionsWithSafeEnds = this.selectionsWithSafeEnds(range)
|
||||
|
||||
const selectionsWithSafeEnds = range.map((s): [number, number] => {
|
||||
const safeEnd = Math.min(s[1], this._editorView?.state.doc.length || s[1])
|
||||
return [s[0], safeEnd]
|
||||
})
|
||||
this._highlightRange = selectionsWithSafeEnds
|
||||
|
||||
if (this._editorView) {
|
||||
this._editorView.dispatch({
|
||||
@ -302,20 +317,20 @@ export default class EditorManager {
|
||||
}
|
||||
let codeBasedSelections = []
|
||||
for (const selection of selections.graphSelections) {
|
||||
const safeEnd = Math.min(
|
||||
selection.codeRef.range[1],
|
||||
this._editorView?.state.doc.length || selection.codeRef.range[1]
|
||||
)
|
||||
codeBasedSelections.push(
|
||||
EditorSelection.range(
|
||||
selection.codeRef.range[0],
|
||||
selection.codeRef.range[1]
|
||||
)
|
||||
EditorSelection.range(selection.codeRef.range[0], safeEnd)
|
||||
)
|
||||
}
|
||||
|
||||
codeBasedSelections.push(
|
||||
EditorSelection.cursor(
|
||||
selections.graphSelections[selections.graphSelections.length - 1]
|
||||
.codeRef.range[1]
|
||||
)
|
||||
)
|
||||
const end =
|
||||
selections.graphSelections[selections.graphSelections.length - 1].codeRef
|
||||
.range[1]
|
||||
const safeEnd = Math.min(end, this._editorView?.state.doc.length || end)
|
||||
codeBasedSelections.push(EditorSelection.cursor(safeEnd))
|
||||
|
||||
if (!this._editorView) {
|
||||
return
|
||||
@ -352,6 +367,16 @@ export default class EditorManager {
|
||||
return
|
||||
}
|
||||
|
||||
this._isAllTextSelected = viewUpdate.state.selection.ranges.some(
|
||||
(selection) => {
|
||||
return (
|
||||
// The user will need to select the empty new lines as well to be considered all of the text.
|
||||
// CTRL+A is the best way to select all the text
|
||||
selection.from === 0 && selection.to === viewUpdate.state.doc.length
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
const eventInfo = processCodeMirrorRanges({
|
||||
codeMirrorRanges: viewUpdate.state.selection.ranges,
|
||||
selectionRanges: this._selectionRanges,
|
||||
|
@ -10,8 +10,11 @@ import { AppStreamProvider } from 'AppState'
|
||||
import { ToastUpdate } from 'components/ToastUpdate'
|
||||
import { markOnce } from 'lib/performance'
|
||||
import { AUTO_UPDATER_TOAST_ID } from 'lib/constants'
|
||||
import { initializeWindowExceptionHandler } from 'lib/exceptions'
|
||||
|
||||
markOnce('code/willAuth')
|
||||
initializeWindowExceptionHandler()
|
||||
|
||||
// uncomment for xstate inspector
|
||||
// import { DEV } from 'env'
|
||||
// import { inspect } from '@xstate/inspect'
|
||||
|
@ -376,7 +376,11 @@ export class KclManager {
|
||||
}
|
||||
this.ast = { ...ast }
|
||||
// updateArtifactGraph relies on updated executeState/programMemory
|
||||
await this.engineCommandManager.updateArtifactGraph(this.ast)
|
||||
await this.engineCommandManager.updateArtifactGraph(
|
||||
this.ast,
|
||||
execState.artifactCommands,
|
||||
execState.artifacts
|
||||
)
|
||||
this._executeCallback()
|
||||
if (!isInterrupted) {
|
||||
sceneInfra.modelingSend({ type: 'code edit during sketch' })
|
||||
@ -390,6 +394,24 @@ export class KclManager {
|
||||
this._cancelTokens.delete(currentExecutionId)
|
||||
markOnce('code/endExecuteAst')
|
||||
}
|
||||
|
||||
/**
|
||||
* This cleanup function is external and internal to the KclSingleton class.
|
||||
* Since the WASM runtime can panic and the error cannot be caught in executeAst
|
||||
* we need a global exception handler in exceptions.ts
|
||||
* This file will interface with this cleanup as if it caught the original error
|
||||
* to properly restore the TS application state.
|
||||
*/
|
||||
executeAstCleanUp() {
|
||||
this.isExecuting = false
|
||||
this.executeIsStale = null
|
||||
this.engineCommandManager.addCommandLog({
|
||||
type: 'execution-done',
|
||||
data: null,
|
||||
})
|
||||
markOnce('code/endExecuteAst')
|
||||
}
|
||||
|
||||
// NOTE: this always updates the code state and editor.
|
||||
// DO NOT CALL THIS from codemirror ever.
|
||||
async executeAstMock(
|
||||
@ -464,13 +486,42 @@ export class KclManager {
|
||||
}
|
||||
async executeCode(zoomToFit?: boolean): Promise<void> {
|
||||
const ast = await this.safeParse(codeManager.code)
|
||||
|
||||
if (!ast) {
|
||||
this.clearAst()
|
||||
return
|
||||
}
|
||||
|
||||
zoomToFit = this.tryToZoomToFitOnCodeUpdate(ast, zoomToFit)
|
||||
|
||||
this.ast = { ...ast }
|
||||
return this.executeAst({ zoomToFit })
|
||||
}
|
||||
/**
|
||||
* This will override the zoom to fit to zoom into the model if the previous AST was empty.
|
||||
* Workflows this improves,
|
||||
* When someone comments the entire file then uncomments the entire file it zooms to the model
|
||||
* When someone CRTL+A and deletes the code then adds the code back it zooms to the model
|
||||
* When someone CRTL+A and copies new code into the editor it zooms to the model
|
||||
*/
|
||||
tryToZoomToFitOnCodeUpdate(
|
||||
ast: Node<Program>,
|
||||
zoomToFit: boolean | undefined
|
||||
) {
|
||||
const isAstEmpty = this._isAstEmpty(this._ast)
|
||||
const isRequestedAstEmpty = this._isAstEmpty(ast)
|
||||
|
||||
// If the AST went from empty to not empty or
|
||||
// If the user has all of the content selected and they copy new code in
|
||||
if (
|
||||
(isAstEmpty && !isRequestedAstEmpty) ||
|
||||
editorManager.isAllTextSelected
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
return zoomToFit
|
||||
}
|
||||
async format() {
|
||||
const originalCode = codeManager.code
|
||||
const ast = await this.safeParse(originalCode)
|
||||
|
@ -47,7 +47,7 @@ describe('parsing errors', () => {
|
||||
const result = parse(code)
|
||||
if (err(result)) throw result
|
||||
const error = result.errors[0]
|
||||
expect(error.message).toBe('Unexpected token: (')
|
||||
expect(error.sourceRange).toEqual([27, 28, 0])
|
||||
expect(error.message).toBe('Array is missing a closing bracket(`]`)')
|
||||
expect(error.sourceRange).toEqual([28, 29, 0])
|
||||
})
|
||||
})
|
||||
|