Compare commits

..

4 Commits

Author SHA1 Message Date
0b5cad701f fix import 2024-10-12 00:58:33 -04:00
71f7124913 Added tests 2024-10-11 13:45:43 -04:00
b6a3c552c9 Reload FileTree and File when changed externally 2024-10-11 13:45:43 -04:00
b303b678ad 2024-10-11 13:45:43 -04:00
259 changed files with 4871 additions and 8153 deletions

View File

@ -1,3 +1,3 @@
[codespell] [codespell]
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey,atleast,ue,afterall ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey,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 skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md,.yarn.lock,**/yarn.lock

View File

@ -15,7 +15,6 @@ on:
env: env:
CUT_RELEASE_PR: ${{ github.event_name == 'pull_request' && (contains(github.event.pull_request.title, 'Cut release v')) }} CUT_RELEASE_PR: ${{ github.event_name == 'pull_request' && (contains(github.event.pull_request.title, 'Cut release v')) }}
BUILD_RELEASE: ${{ github.event_name == 'release' || github.event_name == 'schedule' || github.event_name == 'pull_request' && (contains(github.event.pull_request.title, 'Cut release v')) }} BUILD_RELEASE: ${{ github.event_name == 'release' || github.event_name == 'schedule' || github.event_name == 'pull_request' && (contains(github.event.pull_request.title, 'Cut release v')) }}
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Non-release build, commit {0}', github.sha) }}
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
@ -26,6 +25,7 @@ jobs:
runs-on: ubuntu-22.04 # seperate job on Ubuntu for easy string manipulations (compared to Windows) runs-on: ubuntu-22.04 # seperate job on Ubuntu for easy string manipulations (compared to Windows)
outputs: outputs:
version: ${{ steps.export_version.outputs.version }} version: ${{ steps.export_version.outputs.version }}
notes: ${{ steps.export_version.outputs.notes }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -55,6 +55,8 @@ jobs:
# TODO: see if we need to inject updater nightly URL here https://dl.zoo.dev/releases/modeling-app/nightly/last_update.json # TODO: see if we need to inject updater nightly URL here https://dl.zoo.dev/releases/modeling-app/nightly/last_update.json
- name: Generate release notes - name: Generate release notes
env:
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Non-release build, commit {0}', github.sha) }}
run: | run: |
echo "$NOTES" > release-notes.md echo "$NOTES" > release-notes.md
cat release-notes.md cat release-notes.md
@ -70,17 +72,8 @@ jobs:
- id: export_version - id: export_version
run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT" run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
- name: Prepare electron-builder.yml file for nightly - id: export_notes
if: ${{ github.event_name == 'schedule' }} run: echo "notes=`cat release-notes.md'`" >> "$GITHUB_OUTPUT"
run: |
yq -i '.publish[0].url = "https://dl.zoo.dev/releases/modeling-app/nightly"' electron-builder.yml
- uses: actions/upload-artifact@v3
if: ${{ github.event_name == 'schedule' }}
with:
name: prepared-files-nightly
path: |
electron-builder.yml
- name: Prepare electron-builder.yml file for updater test - name: Prepare electron-builder.yml file for updater test
if: ${{ env.CUT_RELEASE_PR == 'true' }} if: ${{ env.CUT_RELEASE_PR == 'true' }}
@ -88,7 +81,6 @@ jobs:
yq -i '.publish[0].url = "https://dl.zoo.dev/releases/modeling-app/updater-test-release-notes"' electron-builder.yml yq -i '.publish[0].url = "https://dl.zoo.dev/releases/modeling-app/updater-test-release-notes"' electron-builder.yml
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' }}
with: with:
name: prepared-files-updater-test name: prepared-files-updater-test
path: | path: |
@ -100,13 +92,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
include: os: [macos-14, windows-2022, ubuntu-22.04]
- os: macos-14
platform: mac
- os: windows-2022
platform: win
- os: ubuntu-22.04
platform: linux
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
env: env:
APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID: ${{ secrets.APPLE_ID }}
@ -135,16 +121,6 @@ jobs:
cp prepared-files/src/wasm-lib/pkg/wasm_lib* src/wasm-lib/pkg cp prepared-files/src/wasm-lib/pkg/wasm_lib* src/wasm-lib/pkg
cp prepared-files/release-notes.md release-notes.md cp prepared-files/release-notes.md release-notes.md
- uses: actions/download-artifact@v3
if: ${{ github.event_name == 'schedule' }}
name: prepared-files-nightly
- name: Copy updated electron-builder.yml file for nightly build
if: ${{ github.event_name == 'schedule' }}
run: |
ls -R prepared-files-nightly
cp prepared-files-nightly/electron-builder.yml electron-builder.yml
- name: Sync node version and setup cache - name: Sync node version and setup cache
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
@ -189,27 +165,9 @@ jobs:
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
with: with:
name: out-arm64-${{ matrix.platform }} name: out-${{ matrix.os }}
# first two will pick both Zoo Modeling App-$VERSION-arm64-win.exe and Zoo Modeling App-$VERSION-win.exe
path: |
out/*-${{ env.VERSION_NO_V }}-win.*
out/*-${{ env.VERSION_NO_V }}-arm64-win.*
out/*-arm64-mac.*
out/*-arm64-linux.*
- uses: actions/upload-artifact@v3
with:
name: out-x64-${{ matrix.platform }}
path: |
out/*-x64-win.*
out/*-x64-mac.*
out/*-x86_64-linux.*
- uses: actions/upload-artifact@v3
if: ${{ env.BUILD_RELEASE == 'true' }}
with:
name: out-yml
path: | path: |
out/Zoo*.*
out/latest*.yml out/latest*.yml
# TODO: add the 'Build for Mac TestFlight (nightly)' stage back # TODO: add the 'Build for Mac TestFlight (nightly)' stage back
@ -231,20 +189,10 @@ jobs:
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' }} if: ${{ env.CUT_RELEASE_PR == 'true' }}
with: with:
name: updater-test-arm64-${{ matrix.platform }} name: updater-test-${{ matrix.os }}
path: | path: |
out/*-arm64-win.exe out/Zoo*.*
out/*-arm64-mac.dmg out/latest*.yml
out/*-arm64-linux.AppImage
- uses: actions/upload-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' }}
with:
name: updater-test-x64-${{ matrix.platform }}
path: |
out/*-x64-win.exe
out/*-x64-mac.dmg
out/*-x86_64-linux.AppImage
publish-apps-release: publish-apps-release:
@ -257,6 +205,7 @@ jobs:
VERSION_NO_V: ${{ needs.prepare-files.outputs.version }} VERSION_NO_V: ${{ needs.prepare-files.outputs.version }}
VERSION: ${{ github.event_name == 'schedule' && needs.prepare-files.outputs.version || format('v{0}', needs.prepare-files.outputs.version) }} VERSION: ${{ github.event_name == 'schedule' && needs.prepare-files.outputs.version || format('v{0}', needs.prepare-files.outputs.version) }}
PUB_DATE: ${{ github.event_name == 'release' && github.event.release.created_at || github.event.repository.updated_at }} PUB_DATE: ${{ github.event_name == 'release' && github.event.release.created_at || github.event.repository.updated_at }}
NOTES: ${{ needs.prepare-files.outputs.notes }}
BUCKET_DIR: ${{ github.event_name == 'schedule' && 'dl.kittycad.io/releases/modeling-app/nightly' || 'dl.kittycad.io/releases/modeling-app' }} BUCKET_DIR: ${{ github.event_name == 'schedule' && 'dl.kittycad.io/releases/modeling-app/nightly' || 'dl.kittycad.io/releases/modeling-app' }}
WEBSITE_DIR: ${{ github.event_name == 'schedule' && 'dl.zoo.dev/releases/modeling-app/nightly' || 'dl.zoo.dev/releases/modeling-app' }} 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' }} URL_CODED_NAME: ${{ github.event_name == 'schedule' && 'Zoo%20Modeling%20App%20%28Nightly%29' || 'Zoo%20Modeling%20App' }}
@ -265,37 +214,17 @@ jobs:
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v3
with: with:
name: out-arm64-win name: out-windows-2022
path: out path: out
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v3
with: with:
name: out-x64-win name: out-macos-14
path: out path: out
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v3
with: with:
name: out-arm64-mac name: out-ubuntu-22.04
path: out
- uses: actions/download-artifact@v3
with:
name: out-x64-mac
path: out
- uses: actions/download-artifact@v3
with:
name: out-arm64-linux
path: out
- uses: actions/download-artifact@v3
with:
name: out-x64-linux
path: out
- uses: actions/download-artifact@v3
with:
name: out-yml
path: out path: out
- name: Generate the download static endpoint - name: Generate the download static endpoint

View File

@ -37,6 +37,10 @@ jobs:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
cache: 'yarn' cache: 'yarn'
- run: yarn install - run: yarn install
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- run: yarn build:wasm - run: yarn build:wasm
yarn-tsc: yarn-tsc:
@ -66,6 +70,10 @@ jobs:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
cache: 'yarn' cache: 'yarn'
- run: yarn install - run: yarn install
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- run: yarn lint - run: yarn lint
python-codespell: python-codespell:
@ -93,6 +101,11 @@ jobs:
cache: 'yarn' cache: 'yarn'
- run: yarn install - run: yarn install
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- run: yarn build:wasm - run: yarn build:wasm
- run: yarn simpleserver:bg - run: yarn simpleserver:bg

View File

@ -158,29 +158,11 @@ The PR may then serve as a place to discuss the human-readable changelog and ext
#### 3. Manually test artifacts from the Cut Release PR #### 3. Manually test artifacts from the Cut Release PR
##### Release builds The release builds can be find under the `artifact` zip, at the very bottom of the `ci` action page for each commit on this branch.
The release builds can be found under the `out-{platform}` zip, at the very bottom of the `build-publish-apps` summary page for each commit on this branch.
Manually test against this [list](https://github.com/KittyCAD/modeling-app/issues/3588) across Windows, MacOS, Linux and posting results as comments in the Cut Release PR. Manually test against this [list](https://github.com/KittyCAD/modeling-app/issues/3588) across Windows, MacOS, Linux and posting results as comments in the Cut Release PR.
##### Updater-test builds The other `ci` output in Cut Release PRs is `updater-test`, because we don't have a way to test this fully automated, we have a semi-automated process. Download updater-test zip file, install the app, run it, expect an updater prompt to a dummy v0.99.99, install it and check that the app comes back at that version (on both macOS and Windows).
The other `build-publish-apps` output in Cut Release PRs is `updater-test-{platform}`. As we don't have a way to test this fully automatically, we have a semi-automated process. For macOS, Windows, and Linux, download the corresponding updater-test artifact file, install the app, run it, expect an updater prompt to a dummy v0.255.255, install it and check that the app comes back at that version.
The only difference with these builds is that they point to a different update location on the release bucket, with this dummy v0.255.255 always available. This helps ensuring that the version we release will be able to update to the next one available.
If the prompt doesn't show up, start the app in command line to grab the electron-updater logs. This is likely an issue with the current build that needs addressing (or the updater-test location in the storage bucket).
```
# Windows (PowerShell)
& 'C:\Program Files\Zoo Modeling App\Zoo Modeling App.exe'
# macOS
/Applications/Zoo\ Modeling\ App.app/Contents/MacOS/Zoo\ Modeling\ App
# Linux
./Zoo Modeling App-{version}-{arch}-linux.AppImage
```
#### 4. Merge the Cut Release PR #### 4. Merge the Cut Release PR

File diff suppressed because one or more lines are too long

View File

@ -36,7 +36,7 @@ exampleSketch = startSketchOn('XZ')
|> close(%) |> close(%)
|> patternCircular2d({ |> patternCircular2d({
center: [0, 0], center: [0, 0],
instances: 13, repetitions: 12,
arcDegrees: 360, arcDegrees: 360,
rotateDuplicates: true rotateDuplicates: true
}, %) }, %)

View File

@ -35,7 +35,7 @@ example = extrude(-5, exampleSketch)
|> patternCircular3d({ |> patternCircular3d({
axis: [1, -1, 0], axis: [1, -1, 0],
center: [10, -20, 0], center: [10, -20, 0],
instances: 11, repetitions: 10,
arcDegrees: 360, arcDegrees: 360,
rotateDuplicates: true rotateDuplicates: true
}, %) }, %)

View File

@ -32,7 +32,7 @@ exampleSketch = startSketchOn('XZ')
|> circle({ center: [0, 0], radius: 1 }, %) |> circle({ center: [0, 0], radius: 1 }, %)
|> patternLinear2d({ |> patternLinear2d({
axis: [1, 0], axis: [1, 0],
instances: 7, repetitions: 6,
distance: 4 distance: 4
}, %) }, %)

View File

@ -38,7 +38,7 @@ exampleSketch = startSketchOn('XZ')
example = extrude(1, exampleSketch) example = extrude(1, exampleSketch)
|> patternLinear3d({ |> patternLinear3d({
axis: [1, 0, 1], axis: [1, 0, 1],
instances: 7, repetitions: 6,
distance: 6 distance: 6
}, %) }, %)
``` ```

View File

@ -32,7 +32,7 @@ reduce(array: [KclValue], start: KclValue, reduce_fn: FunctionParam) -> KclValue
fn decagon = (radius) => { fn decagon = (radius) => {
step = 1 / 10 * tau() step = 1 / 10 * tau()
sketch001 = startSketchAt([cos(0) * radius, sin(0) * radius]) sketch001 = startSketchAt([cos(0) * radius, sin(0) * radius])
return reduce([1..10], sketch001, (i, sg) => { return reduce([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], sketch001, (i, sg) => {
x = cos(step * i) * radius x = cos(step * i) * radius
y = sin(step * i) * radius y = sin(step * i) * radius
return lineTo([x, y], sg) return lineTo([x, y], sg)

File diff suppressed because it is too large Load Diff

View File

@ -82,78 +82,6 @@ Raise a number to a power.
----
Are two numbers equal?
**enum:** `==`
----
Are two numbers not equal?
**enum:** `!=`
----
Is left greater than right
**enum:** `>`
----
Is left greater than or equal to right
**enum:** `>=`
----
Is left less than right
**enum:** `<`
----
Is left less than or equal to right
**enum:** `<=`
---- ----

View File

@ -18,27 +18,6 @@ layout: manual
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ImportStatement`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `items` |`[` [`ImportItem`](/docs/kcl/types/ImportItem) `]`| | No |
| `path` |`string`| | No |
| `raw_path` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
----
**Type:** `object`
## Properties ## Properties
| Property | Type | Description | Required | | Property | Type | Description | Required |
@ -66,7 +45,6 @@ layout: manual
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No | | `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No | | `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `declarations` |`[` [`VariableDeclarator`](/docs/kcl/types/VariableDeclarator) `]`| | No | | `declarations` |`[` [`VariableDeclarator`](/docs/kcl/types/VariableDeclarator) `]`| | No |
| `visibility` |[`ItemVisibility`](/docs/kcl/types/ItemVisibility)| | No |
| `kind` |[`VariableKind`](/docs/kcl/types/VariableKind)| | No | | `kind` |[`VariableKind`](/docs/kcl/types/VariableKind)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No | | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |

View File

@ -16,7 +16,7 @@ Data for a circular pattern on a 2D sketch.
| Property | Type | Description | Required | | Property | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `instances` |[`Uint`](/docs/kcl/types/Uint)| The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | No | | `repetitions` |[`Uint`](/docs/kcl/types/Uint)| The number of repetitions. Must be greater than 0. This excludes the original entity. For example, if `repetitions` is 1, the original entity will be copied once. | No |
| `center` |`[number, number]`| The center about which to make the pattern. This is a 2D vector. | No | | `center` |`[number, number]`| The center about which to make the pattern. This is a 2D vector. | No |
| `arcDegrees` |`number`| The arc angle (in degrees) to place the repetitions. Must be greater than 0. | No | | `arcDegrees` |`number`| The arc angle (in degrees) to place the repetitions. Must be greater than 0. | No |
| `rotateDuplicates` |`boolean`| Whether or not to rotate the duplicates as they are copied. | No | | `rotateDuplicates` |`boolean`| Whether or not to rotate the duplicates as they are copied. | No |

View File

@ -16,7 +16,7 @@ Data for a circular pattern on a 3D model.
| Property | Type | Description | Required | | Property | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `instances` |[`Uint`](/docs/kcl/types/Uint)| The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | No | | `repetitions` |[`Uint`](/docs/kcl/types/Uint)| The number of repetitions. Must be greater than 0. This excludes the original entity. For example, if `repetitions` is 1, the original entity will be copied once. | No |
| `axis` |`[number, number, number]`| The axis around which to make the pattern. This is a 3D vector. | No | | `axis` |`[number, number, number]`| The axis around which to make the pattern. This is a 3D vector. | No |
| `center` |`[number, number, number]`| The center about which to make the pattern. This is a 3D vector. | No | | `center` |`[number, number, number]`| The center about which to make the pattern. This is a 3D vector. | No |
| `arcDegrees` |`number`| The arc angle (in degrees) to place the repetitions. Must be greater than 0. | No | | `arcDegrees` |`number`| The arc angle (in degrees) to place the repetitions. Must be greater than 0. | No |

View File

@ -197,27 +197,6 @@ An expression can be evaluated to yield a single KCL value.
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ArrayRangeExpression`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `startElement` |[`Expr`](/docs/kcl/types/Expr)| An expression can be evaluated to yield a single KCL value. | No |
| `endElement` |[`Expr`](/docs/kcl/types/Expr)| An expression can be evaluated to yield a single KCL value. | No |
| `endInclusive` |`boolean`| Is the `end_element` included in the range? | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
----
**Type:** `object`
## Properties ## Properties
| Property | Type | Description | Required | | Property | Type | Description | Required |

View File

@ -1,24 +0,0 @@
---
title: "ImportItem"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `name` |[`Identifier`](/docs/kcl/types/Identifier)| Name of the item to import. | No |
| `alias` |[`Identifier`](/docs/kcl/types/Identifier)| Rename the item using an identifier after "as". | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |

View File

@ -1,16 +0,0 @@
---
title: "ItemVisibility"
excerpt: ""
layout: manual
---
**enum:** `default`, `export`

View File

@ -16,7 +16,7 @@ Data for a linear pattern on a 2D sketch.
| Property | Type | Description | Required | | Property | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `instances` |[`Uint`](/docs/kcl/types/Uint)| The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | No | | `repetitions` |[`Uint`](/docs/kcl/types/Uint)| The number of repetitions. Must be greater than 0. This excludes the original entity. For example, if `repetitions` is 1, the original entity will be copied once. | No |
| `distance` |`number`| The distance between each repetition. This can also be referred to as spacing. | No | | `distance` |`number`| The distance between each repetition. This can also be referred to as spacing. | No |
| `axis` |`[number, number]`| The axis of the pattern. This is a 2D vector. | No | | `axis` |`[number, number]`| The axis of the pattern. This is a 2D vector. | No |

View File

@ -16,7 +16,7 @@ Data for a linear pattern on a 3D model.
| Property | Type | Description | Required | | Property | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `instances` |[`Uint`](/docs/kcl/types/Uint)| The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | No | | `repetitions` |[`Uint`](/docs/kcl/types/Uint)| The number of repetitions. Must be greater than 0. This excludes the original entity. For example, if `repetitions` is 1, the original entity will be copied once. | No |
| `distance` |`number`| The distance between each repetition. This can also be referred to as spacing. | No | | `distance` |`number`| The distance between each repetition. This can also be referred to as spacing. | No |
| `axis` |`[number, number, number]`| The axis of the pattern. | No | | `axis` |`[number, number, number]`| The axis of the pattern. | No |

View File

@ -16,8 +16,8 @@ A sketch is a collection of paths.
| Property | Type | Description | Required | | Property | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `id` |`string`| The id of the sketch (this will change when the engine's reference to it changes). | No | | `id` |`string`| The id of the sketch (this will change when the engine's reference to it changes. | No |
| `paths` |`[` [`Path`](/docs/kcl/types/Path) `]`| The paths in the sketch. | No | | `value` |`[` [`Path`](/docs/kcl/types/Path) `]`| The paths in the sketch. | No |
| `on` |[`SketchSurface`](/docs/kcl/types/SketchSurface)| What the sketch is on (can be a plane or a face). | No | | `on` |[`SketchSurface`](/docs/kcl/types/SketchSurface)| What the sketch is on (can be a plane or a face). | No |
| `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No | | `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No |
| `tags` |`object`| Tag identifiers that have been declared in this sketch. | No | | `tags` |`object`| Tag identifiers that have been declared in this sketch. | No |

View File

@ -25,8 +25,8 @@ A sketch is a collection of paths.
| Property | Type | Description | Required | | Property | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `type` |enum: `sketch`| | No | | `type` |enum: `sketch`| | No |
| `id` |`string`| The id of the sketch (this will change when the engine's reference to it changes). | No | | `id` |`string`| The id of the sketch (this will change when the engine's reference to it changes. | No |
| `paths` |`[` [`Path`](/docs/kcl/types/Path) `]`| The paths in the sketch. | No | | `value` |`[` [`Path`](/docs/kcl/types/Path) `]`| The paths in the sketch. | No |
| `on` |[`SketchSurface`](/docs/kcl/types/SketchSurface)| What the sketch is on (can be a plane or a face). | No | | `on` |[`SketchSurface`](/docs/kcl/types/SketchSurface)| What the sketch is on (can be a plane or a face). | No |
| `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No | | `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No |
| `tags` |`object`| Tag identifiers that have been declared in this sketch. | No | | `tags` |`object`| Tag identifiers that have been declared in this sketch. | No |

View File

@ -104,7 +104,7 @@ test(
}, },
{ timeout: 15_000 } { timeout: 15_000 }
) )
.toBeGreaterThan(300_000) .toBe(431341)
// clean up output.gltf // clean up output.gltf
await fsp.rm('output.gltf') await fsp.rm('output.gltf')
@ -179,7 +179,7 @@ test(
}, },
{ timeout: 15_000 } { timeout: 15_000 }
) )
.toBeGreaterThan(100_000) .toBe(102040)
// clean up output.gltf // clean up output.gltf
await fsp.rm('output.gltf') await fsp.rm('output.gltf')

View File

@ -1,16 +1,6 @@
import { test, expect } from '@playwright/test' import { test, expect } from '@playwright/test'
import fsp from 'fs/promises'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
import { import { getUtils, setup, tearDown } from './test-utils'
darkModeBgColor,
darkModePlaneColorXZ,
executorInputPath,
getUtils,
setup,
setupElectron,
tearDown,
} from './test-utils'
import { join } from 'path'
test.beforeEach(async ({ context, page }, testInfo) => { test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo) await setup(context, page, testInfo)
@ -984,84 +974,4 @@ test.describe('Editor tests', () => {
|> close(%) |> close(%)
|> extrude(5, %)`) |> extrude(5, %)`)
}) })
test(
`Can use the import stdlib function on a local OBJ file`,
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
const bracketDir = join(dir, 'cube')
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile(
executorInputPath('cube.obj'),
join(bracketDir, 'cube.obj')
)
await fsp.writeFile(join(bracketDir, 'main.kcl'), '')
},
})
const viewportSize = { width: 1200, height: 500 }
await page.setViewportSize(viewportSize)
// Locators and constants
const u = await getUtils(page)
const projectLink = page.getByRole('link', { name: 'cube' })
const gizmo = page.locator('[aria-label*=gizmo]')
const resetCameraButton = page.getByRole('button', { name: 'Reset view' })
const locationToHavColor = async (
position: { x: number; y: number },
color: [number, number, number]
) => {
return u.getGreatestPixDiff(position, color)
}
const notTheOrigin = {
x: viewportSize.width * 0.55,
y: viewportSize.height * 0.3,
}
const origin = { x: viewportSize.width / 2, y: viewportSize.height / 2 }
const errorIndicators = page.locator('.cm-lint-marker-error')
await test.step(`Open the empty file, see the default planes`, async () => {
await projectLink.click()
await u.waitForPageLoad()
await expect
.poll(
async () => locationToHavColor(notTheOrigin, darkModePlaneColorXZ),
{
timeout: 5000,
message: 'XZ plane color is visible',
}
)
.toBeLessThan(15)
})
await test.step(`Write the import function line`, async () => {
await u.codeLocator.fill(`import('cube.obj')`)
await page.waitForTimeout(800)
})
await test.step(`Reset the camera before checking`, async () => {
await u.doAndWaitForCmd(async () => {
await gizmo.click({ button: 'right' })
await resetCameraButton.click()
}, 'zoom_to_fit')
})
await test.step(`Verify that we see the imported geometry and no errors`, async () => {
await expect(errorIndicators).toHaveCount(0)
await expect
.poll(async () => locationToHavColor(origin, darkModePlaneColorXZ), {
timeout: 3000,
message: 'Plane color should not be visible',
})
.toBeGreaterThan(15)
await expect
.poll(async () => locationToHavColor(origin, darkModeBgColor), {
timeout: 3000,
message: 'Background color should not be visible',
})
.toBeGreaterThan(15)
})
await electronApp.close()
}
)
}) })

View File

@ -136,9 +136,6 @@ test.describe('when using the file tree to', () => {
) )
await pasteCodeInEditor(kclCube) await pasteCodeInEditor(kclCube)
// TODO: We have a timeout of 1s between edits to write to disk. If you reload the page too quickly it won't write to disk.
await tronApp.page.waitForTimeout(2000)
await renameFile(fromFile, toFile) await renameFile(fromFile, toFile)
await tronApp.page.reload() await tronApp.page.reload()
@ -225,11 +222,9 @@ test.describe('when using the file tree to', () => {
) )
await pasteCodeInEditor(kclCube) await pasteCodeInEditor(kclCube)
// TODO: We have a timeout of 1s between edits to write to disk. If you reload the page too quickly it won't write to disk.
await tronApp.page.waitForTimeout(2000)
const kcl1 = 'main.kcl' const kcl1 = 'main.kcl'
const kcl2 = '2.kcl' const kcl2 = '2.kcl'
await createNewFileAndSelect(kcl2) await createNewFileAndSelect(kcl2)
const kclCylinder = await fsp.readFile( const kclCylinder = await fsp.readFile(
'src/wasm-lib/tests/executor/inputs/cylinder.kcl', 'src/wasm-lib/tests/executor/inputs/cylinder.kcl',
@ -237,9 +232,6 @@ test.describe('when using the file tree to', () => {
) )
await pasteCodeInEditor(kclCylinder) await pasteCodeInEditor(kclCylinder)
// TODO: We have a timeout of 1s between edits to write to disk. If you reload the page too quickly it won't write to disk.
await tronApp.page.waitForTimeout(2000)
await renameFile(kcl2, kcl1) await renameFile(kcl2, kcl1)
await test.step(`Postcondition: ${kcl1} still has the original content`, async () => { await test.step(`Postcondition: ${kcl1} still has the original content`, async () => {

View File

@ -55,53 +55,6 @@ test.describe('Onboarding tests', () => {
await expect(page.locator('.cm-content')).toContainText('// Shelf Bracket') await expect(page.locator('.cm-content')).toContainText('// Shelf Bracket')
}) })
test(
'Desktop: fresh onboarding executes and loads',
{ tag: '@electron' },
async ({ browserName: _ }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
appSettings: {
app: {
onboardingStatus: 'incomplete',
},
},
cleanProjectDir: true,
})
const u = await getUtils(page)
const viewportSize = { width: 1200, height: 500 }
await page.setViewportSize(viewportSize)
// Locators and constants
const newProjectButton = page.getByRole('button', { name: 'New project' })
const projectLink = page.getByTestId('project-link')
await test.step(`Create a project and open to the onboarding`, async () => {
await newProjectButton.click()
await projectLink.click()
await test.step(`Ensure the engine connection works by testing the sketch button`, async () => {
await u.waitForPageLoad()
})
})
await test.step(`Ensure we see the onboarding stuff`, async () => {
// Test that the onboarding pane loaded
await expect(
page.getByText('Welcome to Modeling App! This')
).toBeVisible()
// *and* that the code is shown in the editor
await expect(page.locator('.cm-content')).toContainText(
'// Shelf Bracket'
)
})
await electronApp.close()
}
)
test('Code resets after confirmation', async ({ page }) => { test('Code resets after confirmation', async ({ page }) => {
const initialCode = `sketch001 = startSketchOn('XZ')` const initialCode = `sketch001 = startSketchOn('XZ')`

View File

@ -255,7 +255,7 @@ test.describe('Can export from electron app', () => {
}, },
{ timeout: 15_000 } { timeout: 15_000 }
) )
.toBeGreaterThan(300_000) .toBe(431341)
// clean up output.gltf // clean up output.gltf
await fsp.rm('output.gltf') await fsp.rm('output.gltf')
@ -851,7 +851,7 @@ test(
} }
) )
test.fixme( test(
'When the project folder is empty, user can create new project and open it.', 'When the project folder is empty, user can create new project and open it.',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ browserName }, testInfo) => {
@ -861,12 +861,6 @@ test.fixme(
page.on('console', console.log) page.on('console', console.log)
// Locators and constants
const gizmo = page.locator('[aria-label*=gizmo]')
const resetCameraButton = page.getByRole('button', { name: 'Reset view' })
const pointOnModel = { x: 660, y: 250 }
const expectedStartCamZPosition = 15633.47
// expect to see text "No Projects found" // expect to see text "No Projects found"
await expect(page.getByText('No Projects found')).toBeVisible() await expect(page.getByText('No Projects found')).toBeVisible()
@ -879,7 +873,16 @@ test.fixme(
await page.getByText('project-000').click() await page.getByText('project-000').click()
await u.waitForPageLoad() await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
await page.locator('.cm-content').fill(`sketch001 = startSketchOn('XZ') await page.locator('.cm-content').fill(`sketch001 = startSketchOn('XZ')
|> startProfileAt([-87.4, 282.92], %) |> startProfileAt([-87.4, 282.92], %)
@ -889,28 +892,8 @@ test.fixme(
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
extrude001 = extrude(200, sketch001)`) extrude001 = extrude(200, sketch001)`)
await page.waitForTimeout(800)
async function getCameraZValue() { const pointOnModel = { x: 660, y: 250 }
return page
.getByTestId('cam-z-position')
.inputValue()
.then((value) => parseFloat(value))
}
await test.step(`Reset camera`, async () => {
await u.openDebugPanel()
await u.clearCommandLogs()
await u.doAndWaitForCmd(async () => {
await gizmo.click({ button: 'right' })
await resetCameraButton.click()
}, 'zoom_to_fit')
await expect
.poll(getCameraZValue, {
message: 'Camera Z should be at expected position after reset',
})
.toEqual(expectedStartCamZPosition)
})
// gray at this pixel means the stream has loaded in the most // gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color) // user way we can verify it (pixel color)
@ -918,7 +901,7 @@ extrude001 = extrude(200, sketch001)`)
.poll(() => u.getGreatestPixDiff(pointOnModel, [143, 143, 143]), { .poll(() => u.getGreatestPixDiff(pointOnModel, [143, 143, 143]), {
timeout: 10_000, timeout: 10_000,
}) })
.toBeLessThan(30) .toBeLessThan(15)
await expect(async () => { await expect(async () => {
await page.mouse.move(0, 0, { steps: 5 }) await page.mouse.move(0, 0, { steps: 5 })

View File

@ -471,7 +471,7 @@ test(
await page.mouse.move(startXPx + PUR * 30, 500 - PUR * 20, { steps: 10 }) await page.mouse.move(startXPx + PUR * 30, 500 - PUR * 20, { steps: 10 })
await page.waitForTimeout(1000) await page.waitForTimeout(300)
await expect(page).toHaveScreenshot({ await expect(page).toHaveScreenshot({
maxDiffPixels: 100, maxDiffPixels: 100,
@ -528,7 +528,6 @@ test(
// Draw the rectangle // Draw the rectangle
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 30) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 30)
await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 10, { steps: 5 }) await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 10, { steps: 5 })
await page.waitForTimeout(800)
// Ensure the draft rectangle looks the same as it usually does // Ensure the draft rectangle looks the same as it usually does
await expect(page).toHaveScreenshot({ await expect(page).toHaveScreenshot({
@ -670,7 +669,6 @@ test.describe(
// screen shot should show the sketch // screen shot should show the sketch
await expect(page).toHaveScreenshot({ await expect(page).toHaveScreenshot({
maxDiffPixels: 100, maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
}) })
// exit sketch // exit sketch
@ -688,7 +686,6 @@ test.describe(
// second screen shot should look almost identical, i.e. scale should be the same. // second screen shot should look almost identical, i.e. scale should be the same.
await expect(page).toHaveScreenshot({ await expect(page).toHaveScreenshot({
maxDiffPixels: 100, maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
}) })
}) })
@ -896,7 +893,7 @@ test(
// Wait for the second extrusion to appear // Wait for the second extrusion to appear
// TODO: Find a way to truly know that the objects have finished // TODO: Find a way to truly know that the objects have finished
// rendering, because an execution-done message is not sufficient. // rendering, because an execution-done message is not sufficient.
await page.waitForTimeout(2000) await page.waitForTimeout(1000)
await expect(page).toHaveScreenshot({ await expect(page).toHaveScreenshot({
maxDiffPixels: 100, maxDiffPixels: 100,
@ -940,7 +937,7 @@ test(
// Wait for the second extrusion to appear // Wait for the second extrusion to appear
// TODO: Find a way to truly know that the objects have finished // TODO: Find a way to truly know that the objects have finished
// rendering, because an execution-done message is not sufficient. // rendering, because an execution-done message is not sufficient.
await page.waitForTimeout(2000) await page.waitForTimeout(1000)
await expect(page).toHaveScreenshot({ await expect(page).toHaveScreenshot({
maxDiffPixels: 100, maxDiffPixels: 100,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 50 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: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 48 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: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -47,14 +47,6 @@ export const commonPoints = {
num2: 14.44, num2: 14.44,
} }
/** A semi-reliable color to check the default XZ plane on
* in dark mode in the default camera position
*/
export const darkModePlaneColorXZ: [number, number, number] = [50, 50, 99]
/** A semi-reliable color to check the default dark mode bg color against */
export const darkModeBgColor: [number, number, number] = [27, 27, 27]
export const editorSelector = '[role="textbox"][data-language="kcl"]' export const editorSelector = '[role="textbox"][data-language="kcl"]'
type PaneId = 'variables' | 'code' | 'files' | 'logs' type PaneId = 'variables' | 'code' | 'files' | 'logs'
@ -471,9 +463,6 @@ export async function getUtils(page: Page, test_?: typeof test) {
return test_?.step( return test_?.step(
`Create and select project with text "${hasText}"`, `Create and select project with text "${hasText}"`,
async () => { async () => {
// Without this, we get unreliable project creation. It's probably
// due to a race between the FS being read and clicking doing something.
await page.waitForTimeout(100)
await page.getByTestId('home-new-file').click() await page.getByTestId('home-new-file').click()
const projectLinksPost = page.getByTestId('project-link') const projectLinksPost = page.getByTestId('project-link')
await projectLinksPost.filter({ hasText }).click() await projectLinksPost.filter({ hasText }).click()
@ -503,11 +492,6 @@ export async function getUtils(page: Page, test_?: typeof test) {
createNewFile: async (name: string) => { createNewFile: async (name: string) => {
return test?.step(`Create a file named ${name}`, async () => { return test?.step(`Create a file named ${name}`, async () => {
// If the application is in the middle of connecting a stream
// then creating a new file won't work in the end.
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await page.getByTestId('create-file-button').click() await page.getByTestId('create-file-button').click()
await page.getByTestId('file-rename-field').fill(name) await page.getByTestId('file-rename-field').fill(name)
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
@ -888,20 +872,10 @@ export async function setupElectron({
const tempSettingsFilePath = join(projectDirName, SETTINGS_FILE_NAME) const tempSettingsFilePath = join(projectDirName, SETTINGS_FILE_NAME)
const settingsOverrides = TOML.stringify( const settingsOverrides = TOML.stringify(
appSettings appSettings
? { ? { settings: appSettings }
settings: {
...TEST_SETTINGS,
...appSettings,
app: {
...TEST_SETTINGS.app,
projectDirectory: projectDirName,
...appSettings.app,
},
},
}
: { : {
settings: {
...TEST_SETTINGS, ...TEST_SETTINGS,
settings: {
app: { app: {
...TEST_SETTINGS.app, ...TEST_SETTINGS.app,
projectDirectory: projectDirName, projectDirectory: projectDirName,

View File

@ -292,7 +292,7 @@ test.describe(`Testing gizmo, fixture-based`, () => {
await test.step(`Verify the camera moved`, async () => { await test.step(`Verify the camera moved`, async () => {
await scene.expectState({ await scene.expectState({
camera: { camera: {
position: [0, -23865.37, 11073.53], position: [0, -23865.37, 11073.54],
target: [0, 0, 0], target: [0, 0, 0],
}, },
}) })

View File

@ -430,6 +430,7 @@ test.describe('Testing settings', () => {
await test.step('Check color of logo changed when in modeling view', async () => { await test.step('Check color of logo changed when in modeling view', async () => {
await page.getByRole('button', { name: 'New project' }).click() await page.getByRole('button', { name: 'New project' }).click()
await page.getByTestId('project-link').first().click() await page.getByTestId('project-link').first().click()
await page.getByRole('button', { name: 'Dismiss' }).click()
await changeColor('58') await changeColor('58')
await expect(logoLink).toHaveCSS('--primary-hue', '58') await expect(logoLink).toHaveCSS('--primary-hue', '58')
}) })

6
interface.d.ts vendored
View File

@ -23,8 +23,8 @@ export interface IElectronAPI {
key: string, key: string,
callback: (eventType: string, path: string) => void callback: (eventType: string, path: string) => void
) => void ) => void
readFile: typeof fs.readFile
watchFileOff: (path: string, key: string) => void watchFileOff: (path: string, key: string) => void
readFile: (path: string) => ReturnType<fs.readFile>
writeFile: ( writeFile: (
path: string, path: string,
data: string | Uint8Array data: string | Uint8Array
@ -68,13 +68,13 @@ export interface IElectronAPI {
} }
} }
kittycad: (access: string, args: any) => any kittycad: (access: string, args: any) => any
listMachines: (machineApiIp: string) => Promise<MachinesListing> listMachines: () => Promise<MachinesListing>
getMachineApiIp: () => Promise<string | null> getMachineApiIp: () => Promise<string | null>
onUpdateDownloadStart: ( onUpdateDownloadStart: (
callback: (value: { version: string }) => void callback: (value: { version: string }) => void
) => Electron.IpcRenderer ) => Electron.IpcRenderer
onUpdateDownloaded: ( onUpdateDownloaded: (
callback: (value: { version: string; releaseNotes: string }) => void callback: (value: string) => void
) => Electron.IpcRenderer ) => Electron.IpcRenderer
onUpdateError: (callback: (value: { error: Error }) => void) => Electron onUpdateError: (callback: (value: { error: Error }) => void) => Electron
appRestart: () => void appRestart: () => void

View File

@ -70,7 +70,7 @@ echo ""
echo "Suggested changelog:" echo "Suggested changelog:"
echo "\`\`\`" echo "\`\`\`"
echo "## What's Changed" echo "## What's Changed"
git log $(git describe --tags --match="v[0-9]*" --abbrev=0)..HEAD --oneline --pretty=format:%s | grep -v Bump | grep -v 'Cut release v' | awk '{print "* "toupper(substr($0,0,1))substr($0,2)}' git log $(git describe --tags --abbrev=0)..HEAD --oneline --pretty=format:%s | grep -v Bump | grep -v 'Cut release v' | awk '{print "* "toupper(substr($0,0,1))substr($0,2)}'
echo "" echo ""
echo "**Full Changelog**: https://github.com/KittyCAD/modeling-app/compare/${latest_tag}...${new_version}" echo "**Full Changelog**: https://github.com/KittyCAD/modeling-app/compare/${latest_tag}...${new_version}"
echo "\`\`\`" echo "\`\`\`"

View File

@ -36,319 +36,38 @@
"description": "Extra machine-specific information regarding a connected machine.", "description": "Extra machine-specific information regarding a connected machine.",
"oneOf": [ "oneOf": [
{ {
"additionalProperties": false,
"properties": { "properties": {
"type": { "Moonraker": {
"enum": [ "type": "object"
"moonraker"
],
"type": "string"
} }
}, },
"required": [ "required": [
"type" "Moonraker"
], ],
"type": "object" "type": "object"
}, },
{ {
"additionalProperties": false,
"properties": { "properties": {
"type": { "Usb": {
"enum": [ "type": "object"
"usb"
],
"type": "string"
} }
}, },
"required": [ "required": [
"type" "Usb"
], ],
"type": "object" "type": "object"
}, },
{ {
"additionalProperties": false,
"properties": { "properties": {
"current_stage": { "Bambu": {
"allOf": [
{
"$ref": "#/components/schemas/Stage"
}
],
"description": "The current stage of the machine as defined by Bambu which can include errors, etc.",
"nullable": true
},
"nozzle_diameter": {
"allOf": [
{
"$ref": "#/components/schemas/NozzleDiameter"
}
],
"description": "The nozzle diameter of the machine."
},
"type": {
"enum": [
"bambu"
],
"type": "string"
}
},
"required": [
"nozzle_diameter",
"type"
],
"type": "object" "type": "object"
} }
]
},
"FdmHardwareConfiguration": {
"description": "Configuration for a FDM-based printer.",
"properties": {
"filaments": {
"description": "The filaments the printer has access to.",
"items": {
"$ref": "#/components/schemas/Filament"
},
"type": "array"
},
"loaded_filament_idx": {
"description": "The currently loaded filament index.",
"format": "uint",
"minimum": 0,
"nullable": true,
"type": "integer"
},
"nozzle_diameter": {
"description": "Diameter of the extrusion nozzle, in mm.",
"format": "double",
"type": "number"
}
}, },
"required": [ "required": [
"filaments", "Bambu"
"nozzle_diameter"
],
"type": "object"
},
"Filament": {
"description": "Information about the filament being used in a FDM printer.",
"properties": {
"color": {
"description": "The color (as hex without the `#`) of the filament, this is likely specific to the manufacturer.",
"maxLength": 6,
"minLength": 6,
"nullable": true,
"type": "string"
},
"material": {
"allOf": [
{
"$ref": "#/components/schemas/FilamentMaterial"
}
],
"description": "The material that the filament is made of."
},
"name": {
"description": "The name of the filament, this is likely specfic to the manufacturer.",
"nullable": true,
"type": "string"
}
},
"required": [
"material"
],
"type": "object"
},
"FilamentMaterial": {
"description": "The material that the filament is made of.",
"oneOf": [
{
"description": "Polylactic acid based plastics",
"properties": {
"type": {
"enum": [
"pla"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
},
{
"description": "Pla support",
"properties": {
"type": {
"enum": [
"pla_support"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
},
{
"description": "acrylonitrile butadiene styrene based plastics",
"properties": {
"type": {
"enum": [
"abs"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
},
{
"description": "polyethylene terephthalate glycol based plastics",
"properties": {
"type": {
"enum": [
"petg"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
},
{
"description": "unsuprisingly, nylon based",
"properties": {
"type": {
"enum": [
"nylon"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
},
{
"description": "thermoplastic polyurethane based urethane material",
"properties": {
"type": {
"enum": [
"tpu"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
},
{
"description": "polyvinyl alcohol based material",
"properties": {
"type": {
"enum": [
"pva"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
},
{
"description": "high impact polystyrene based material",
"properties": {
"type": {
"enum": [
"hips"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
},
{
"description": "composite material with stuff in other stuff, something like PLA mixed with carbon fiber, kevlar, or fiberglass",
"properties": {
"type": {
"enum": [
"composite"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
},
{
"description": "Unknown material",
"properties": {
"type": {
"enum": [
"unknown"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
}
]
},
"HardwareConfiguration": {
"description": "The hardware configuration of a machine.",
"oneOf": [
{
"description": "No configuration is possible. This isn't the same conceptually as an `Option<HardwareConfiguration>`, because this indicates we positively know there is no possible configuration changes that are possible with this method of manufcture.",
"properties": {
"type": {
"enum": [
"none"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
},
{
"description": "Hardware configuration specific to FDM based printers",
"properties": {
"config": {
"allOf": [
{
"$ref": "#/components/schemas/FdmHardwareConfiguration"
}
],
"description": "The configuration for the FDM printer."
},
"type": {
"enum": [
"fdm"
],
"type": "string"
}
},
"required": [
"config",
"type"
], ],
"type": "object" "type": "object"
} }
@ -366,14 +85,6 @@
"description": "Additional, per-machine information which is specific to the underlying machine type.", "description": "Additional, per-machine information which is specific to the underlying machine type.",
"nullable": true "nullable": true
}, },
"hardware_configuration": {
"allOf": [
{
"$ref": "#/components/schemas/HardwareConfiguration"
}
],
"description": "Information about how the Machine is currently configured."
},
"id": { "id": {
"description": "Machine Identifier (ID) for the specific Machine.", "description": "Machine Identifier (ID) for the specific Machine.",
"type": "string" "type": "string"
@ -403,12 +114,6 @@
"description": "Maximum part size that can be manufactured by this device. This may be some sort of theoretical upper bound, getting close to this limit seems like maybe a bad idea.\n\nThis may be `None` if the maximum size is not knowable by the Machine API.\n\nWhat \"close\" means is up to you!", "description": "Maximum part size that can be manufactured by this device. This may be some sort of theoretical upper bound, getting close to this limit seems like maybe a bad idea.\n\nThis may be `None` if the maximum size is not knowable by the Machine API.\n\nWhat \"close\" means is up to you!",
"nullable": true "nullable": true
}, },
"progress": {
"description": "Progress of the current print, if printing.",
"format": "double",
"nullable": true,
"type": "number"
},
"state": { "state": {
"allOf": [ "allOf": [
{ {
@ -419,7 +124,6 @@
} }
}, },
"required": [ "required": [
"hardware_configuration",
"id", "id",
"machine_type", "machine_type",
"make_model", "make_model",
@ -453,111 +157,57 @@
"oneOf": [ "oneOf": [
{ {
"description": "If a print state can not be resolved at this time, an Unknown may be returned.", "description": "If a print state can not be resolved at this time, an Unknown may be returned.",
"properties": {
"state": {
"enum": [ "enum": [
"unknown" "Unknown"
], ],
"type": "string" "type": "string"
}
},
"required": [
"state"
],
"type": "object"
}, },
{ {
"description": "Idle, and ready for another job.", "description": "Idle, and ready for another job.",
"properties": {
"state": {
"enum": [ "enum": [
"idle" "Idle"
], ],
"type": "string" "type": "string"
}
},
"required": [
"state"
],
"type": "object"
}, },
{ {
"description": "Running a job -- 3D printing or CNC-ing a part.", "description": "Running a job -- 3D printing or CNC-ing a part.",
"properties": {
"state": {
"enum": [ "enum": [
"running" "Running"
], ],
"type": "string" "type": "string"
}
},
"required": [
"state"
],
"type": "object"
}, },
{ {
"description": "Machine is currently offline or unreachable.", "description": "Machine is currently offline or unreachable.",
"properties": {
"state": {
"enum": [ "enum": [
"offline" "Offline"
], ],
"type": "string" "type": "string"
}
},
"required": [
"state"
],
"type": "object"
}, },
{ {
"description": "Job is underway but halted, waiting for some action to take place.", "description": "Job is underway but halted, waiting for some action to take place.",
"properties": {
"state": {
"enum": [ "enum": [
"paused" "Paused"
], ],
"type": "string" "type": "string"
}
},
"required": [
"state"
],
"type": "object"
}, },
{ {
"description": "Job is finished, but waiting manual action to move back to Idle.", "description": "Job is finished, but waiting manual action to move back to Idle.",
"properties": {
"state": {
"enum": [ "enum": [
"complete" "Complete"
], ],
"type": "string" "type": "string"
}
},
"required": [
"state"
],
"type": "object"
}, },
{ {
"additionalProperties": false,
"description": "The printer has failed and is in an unknown state that may require manual attention to resolve. The inner value is a human readable description of what specifically has failed.", "description": "The printer has failed and is in an unknown state that may require manual attention to resolve. The inner value is a human readable description of what specifically has failed.",
"properties": { "properties": {
"message": { "Failed": {
"description": "A human-readable message describing the failure.",
"nullable": true, "nullable": true,
"type": "string" "type": "string"
},
"state": {
"enum": [
"failed"
],
"type": "string"
} }
}, },
"required": [ "required": [
"state" "Failed"
], ],
"type": "object" "type": "object"
} }
@ -569,54 +219,21 @@
{ {
"description": "Use light to cure a resin to build up layers.", "description": "Use light to cure a resin to build up layers.",
"enum": [ "enum": [
"stereolithography" "Stereolithography"
], ],
"type": "string" "type": "string"
}, },
{ {
"description": "Fused Deposition Modeling, layers of melted plastic.", "description": "Fused Deposition Modeling, layers of melted plastic.",
"enum": [ "enum": [
"fused_deposition" "FusedDeposition"
], ],
"type": "string" "type": "string"
}, },
{ {
"description": "\"Computer numerical control\" - machine that grinds away material from a hunk of material to construct a part.", "description": "\"Computer numerical control\" - machine that grinds away material from a hunk of material to construct a part.",
"enum": [ "enum": [
"cnc" "Cnc"
],
"type": "string"
}
]
},
"NozzleDiameter": {
"description": "A nozzle diameter.",
"oneOf": [
{
"description": "0.2mm.",
"enum": [
"0.2"
],
"type": "string"
},
{
"description": "0.4mm.",
"enum": [
"0.4"
],
"type": "string"
},
{
"description": "0.6mm.",
"enum": [
"0.6"
],
"type": "string"
},
{
"description": "0.8mm.",
"enum": [
"0.8"
], ],
"type": "string" "type": "string"
} }
@ -667,15 +284,6 @@
"machine_id": { "machine_id": {
"description": "The machine id to print to.", "description": "The machine id to print to.",
"type": "string" "type": "string"
},
"slicer_configuration": {
"allOf": [
{
"$ref": "#/components/schemas/SlicerConfiguration"
}
],
"description": "Requested design-specific slicer configurations.",
"nullable": true
} }
}, },
"required": [ "required": [
@ -684,283 +292,6 @@
], ],
"type": "object" "type": "object"
}, },
"SlicerConfiguration": {
"description": "The slicer configuration is a set of parameters that are passed to the slicer to control how the gcode is generated.",
"properties": {
"filament_idx": {
"description": "The filament to use for the print.",
"format": "uint",
"minimum": 0,
"nullable": true,
"type": "integer"
}
},
"type": "object"
},
"Stage": {
"description": "The print stage. These come from: https://github.com/SoftFever/OrcaSlicer/blob/431978baf17961df90f0d01871b0ad1d839d7f5d/src/slic3r/GUI/DeviceManager.cpp#L78",
"oneOf": [
{
"description": "Nothing.",
"enum": [
"nothing"
],
"type": "string"
},
{
"description": "Empty.",
"enum": [
"empty"
],
"type": "string"
},
{
"description": "Auto bed leveling.",
"enum": [
"auto_bed_leveling"
],
"type": "string"
},
{
"description": "Heatbed preheating.",
"enum": [
"heatbed_preheating"
],
"type": "string"
},
{
"description": "Sweeping XY mech mode.",
"enum": [
"sweeping_xy_mech_mode"
],
"type": "string"
},
{
"description": "Changing filament.",
"enum": [
"changing_filament"
],
"type": "string"
},
{
"description": "M400 pause.",
"enum": [
"m400_pause"
],
"type": "string"
},
{
"description": "Paused due to filament runout.",
"enum": [
"paused_due_to_filament_runout"
],
"type": "string"
},
{
"description": "Heating hotend.",
"enum": [
"heating_hotend"
],
"type": "string"
},
{
"description": "Calibrating extrusion.",
"enum": [
"calibrating_extrusion"
],
"type": "string"
},
{
"description": "Scanning bed surface.",
"enum": [
"scanning_bed_surface"
],
"type": "string"
},
{
"description": "Inspecting first layer.",
"enum": [
"inspecting_first_layer"
],
"type": "string"
},
{
"description": "Identifying build plate type.",
"enum": [
"identifying_build_plate_type"
],
"type": "string"
},
{
"description": "Calibrating micro lidar.",
"enum": [
"calibrating_micro_lidar"
],
"type": "string"
},
{
"description": "Homing toolhead.",
"enum": [
"homing_toolhead"
],
"type": "string"
},
{
"description": "Cleaning nozzle tip.",
"enum": [
"cleaning_nozzle_tip"
],
"type": "string"
},
{
"description": "Checking extruder temperature.",
"enum": [
"checking_extruder_temperature"
],
"type": "string"
},
{
"description": "Printing was paused by the user.",
"enum": [
"printing_was_paused_by_the_user"
],
"type": "string"
},
{
"description": "Pause of front cover falling.",
"enum": [
"pause_of_front_cover_falling"
],
"type": "string"
},
{
"description": "Calibrating micro lidar.",
"enum": [
"calibrating_micro_lidar2"
],
"type": "string"
},
{
"description": "Calibrating extrusion flow.",
"enum": [
"calibrating_extrusion_flow"
],
"type": "string"
},
{
"description": "Paused due to nozzle temperature malfunction.",
"enum": [
"paused_due_to_nozzle_temperature_malfunction"
],
"type": "string"
},
{
"description": "Paused due to heat bed temperature malfunction.",
"enum": [
"paused_due_to_heat_bed_temperature_malfunction"
],
"type": "string"
},
{
"description": "Filament unloading.",
"enum": [
"filament_unloading"
],
"type": "string"
},
{
"description": "Skip step pause.",
"enum": [
"skip_step_pause"
],
"type": "string"
},
{
"description": "Filament loading.",
"enum": [
"filament_loading"
],
"type": "string"
},
{
"description": "Motor noise calibration.",
"enum": [
"motor_noise_calibration"
],
"type": "string"
},
{
"description": "Paused due to AMS lost.",
"enum": [
"paused_due_to_ams_lost"
],
"type": "string"
},
{
"description": "Paused due to low speed of the heat break fan.",
"enum": [
"paused_due_to_low_speed_of_the_heat_break_fan"
],
"type": "string"
},
{
"description": "Paused due to chamber temperature control error.",
"enum": [
"paused_due_to_chamber_temperature_control_error"
],
"type": "string"
},
{
"description": "Cooling chamber.",
"enum": [
"cooling_chamber"
],
"type": "string"
},
{
"description": "Paused by the Gcode inserted by the user.",
"enum": [
"paused_by_the_gcode_inserted_by_the_user"
],
"type": "string"
},
{
"description": "Motor noise showoff.",
"enum": [
"motor_noise_showoff"
],
"type": "string"
},
{
"description": "Nozzle filament covered detected pause.",
"enum": [
"nozzle_filament_covered_detected_pause"
],
"type": "string"
},
{
"description": "Cutter error pause.",
"enum": [
"cutter_error_pause"
],
"type": "string"
},
{
"description": "First layer error pause.",
"enum": [
"first_layer_error_pause"
],
"type": "string"
},
{
"description": "Nozzle clog pause.",
"enum": [
"nozzle_clog_pause"
],
"type": "string"
}
]
},
"Volume": { "Volume": {
"description": "Set of three values to represent the extent of a 3-D Volume. This contains the width, depth, and height values, generally used to represent some maximum or minimum.\n\nAll measurements are in millimeters.", "description": "Set of three values to represent the extent of a 3-D Volume. This contains the width, depth, and height values, generally used to represent some maximum or minimum.\n\nAll measurements are in millimeters.",
"properties": { "properties": {
@ -1094,34 +425,6 @@
] ]
} }
}, },
"/metrics": {
"get": {
"operationId": "get_metrics",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"title": "String",
"type": "string"
}
}
},
"description": "successful operation"
},
"4XX": {
"$ref": "#/components/responses/Error"
},
"5XX": {
"$ref": "#/components/responses/Error"
}
},
"summary": "List available machines and their statuses",
"tags": [
"hidden"
]
}
},
"/ping": { "/ping": {
"get": { "get": {
"operationId": "ping", "operationId": "ping",
@ -1189,13 +492,6 @@
} }
}, },
"tags": [ "tags": [
{
"description": "Hidden API endpoints that should not show up in the docs.",
"externalDocs": {
"url": "https://docs.zoo.dev/api/machines"
},
"name": "hidden"
},
{ {
"description": "Utilities for making parts and discovering machines.", "description": "Utilities for making parts and discovering machines.",
"externalDocs": { "externalDocs": {

View File

@ -1,6 +1,6 @@
{ {
"name": "zoo-modeling-app", "name": "zoo-modeling-app",
"version": "0.26.1", "version": "0.25.6",
"private": true, "private": true,
"productName": "Zoo Modeling App", "productName": "Zoo Modeling App",
"author": { "author": {
@ -161,7 +161,7 @@
"@types/isomorphic-fetch": "^0.0.39", "@types/isomorphic-fetch": "^0.0.39",
"@types/minimist": "^1.2.5", "@types/minimist": "^1.2.5",
"@types/mocha": "^10.0.6", "@types/mocha": "^10.0.6",
"@types/node": "^22.7.8", "@types/node": "^22.5.0",
"@types/pixelmatch": "^5.2.6", "@types/pixelmatch": "^5.2.6",
"@types/pngjs": "^6.0.4", "@types/pngjs": "^6.0.4",
"@types/react": "^18.3.4", "@types/react": "^18.3.4",

View File

@ -64,27 +64,6 @@ export type ReactCameraProperties =
const lastCmdDelay = 50 const lastCmdDelay = 50
class CameraRateLimiter {
lastSend?: Date = undefined
rateLimitMs: number = 16 //60 FPS
send = (f: () => void) => {
let now = new Date()
if (
this.lastSend === undefined ||
now.getTime() - this.lastSend.getTime() > this.rateLimitMs
) {
f()
this.lastSend = now
}
}
reset = () => {
this.lastSend = undefined
}
}
export class CameraControls { export class CameraControls {
engineCommandManager: EngineCommandManager engineCommandManager: EngineCommandManager
syncDirection: 'clientToEngine' | 'engineToClient' = 'engineToClient' syncDirection: 'clientToEngine' | 'engineToClient' = 'engineToClient'
@ -92,15 +71,15 @@ export class CameraControls {
target: Vector3 target: Vector3
domElement: HTMLCanvasElement domElement: HTMLCanvasElement
isDragging: boolean isDragging: boolean
wasDragging: boolean
mouseDownPosition: Vector2 mouseDownPosition: Vector2
mouseNewPosition: Vector2 mouseNewPosition: Vector2
rotationSpeed = 0.3 rotationSpeed = 0.3
enableRotate = true enableRotate = true
enablePan = true enablePan = true
enableZoom = true enableZoom = true
moveSender: CameraRateLimiter = new CameraRateLimiter() zoomDataFromLastFrame?: number = undefined
zoomSender: CameraRateLimiter = new CameraRateLimiter() // holds coordinates, and interaction
moveDataFromLastFrame?: [number, number, string] = undefined
lastPerspectiveFov: number = 45 lastPerspectiveFov: number = 45
pendingZoom: number | null = null pendingZoom: number | null = null
pendingRotation: Vector2 | null = null pendingRotation: Vector2 | null = null
@ -192,36 +171,6 @@ export class CameraControls {
} }
} }
doMove = (interaction: any, coordinates: any) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'camera_drag_move',
interaction: interaction,
window: {
x: coordinates[0],
y: coordinates[1],
},
},
cmd_id: uuidv4(),
})
}
doZoom = (zoom: number) => {
this.handleStart()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'default_camera_zoom',
magnitude: (-1 * zoom) / window.devicePixelRatio,
},
cmd_id: uuidv4(),
})
this.handleEnd()
}
constructor( constructor(
isOrtho = false, isOrtho = false,
domElement: HTMLCanvasElement, domElement: HTMLCanvasElement,
@ -234,7 +183,6 @@ export class CameraControls {
this.target = new Vector3() this.target = new Vector3()
this.domElement = domElement this.domElement = domElement
this.isDragging = false this.isDragging = false
this.wasDragging = false
this.mouseDownPosition = new Vector2() this.mouseDownPosition = new Vector2()
this.mouseNewPosition = new Vector2() this.mouseNewPosition = new Vector2()
@ -310,6 +258,49 @@ export class CameraControls {
this.onCameraChange() this.onCameraChange()
} }
// Our stream is never more than 60fps.
// We can get away with capping our "virtual fps" to 60 then.
const FPS_VIRTUAL = 60
const doZoom = () => {
if (this.zoomDataFromLastFrame !== undefined) {
this.handleStart()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'default_camera_zoom',
magnitude:
(-1 * this.zoomDataFromLastFrame) / window.devicePixelRatio,
},
cmd_id: uuidv4(),
})
this.handleEnd()
}
this.zoomDataFromLastFrame = undefined
}
setInterval(doZoom, 1000 / FPS_VIRTUAL)
const doMove = () => {
if (this.moveDataFromLastFrame !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'camera_drag_move',
interaction: this.moveDataFromLastFrame[2] as any,
window: {
x: this.moveDataFromLastFrame[0],
y: this.moveDataFromLastFrame[1],
},
},
cmd_id: uuidv4(),
})
}
this.moveDataFromLastFrame = undefined
}
setInterval(doMove, 1000 / FPS_VIRTUAL)
setTimeout(() => { setTimeout(() => {
this.engineCommandManager.subscribeTo({ this.engineCommandManager.subscribeTo({
event: 'camera_drag_end', event: 'camera_drag_end',
@ -365,8 +356,6 @@ export class CameraControls {
onMouseDown = (event: PointerEvent) => { onMouseDown = (event: PointerEvent) => {
this.domElement.setPointerCapture(event.pointerId) this.domElement.setPointerCapture(event.pointerId)
this.isDragging = true this.isDragging = true
// Reset the wasDragging flag to false when starting a new drag
this.wasDragging = false
this.mouseDownPosition.set(event.clientX, event.clientY) this.mouseDownPosition.set(event.clientX, event.clientY)
let interaction = this.getInteractionType(event) let interaction = this.getInteractionType(event)
if (interaction === 'none') return if (interaction === 'none') return
@ -396,18 +385,11 @@ export class CameraControls {
const interaction = this.getInteractionType(event) const interaction = this.getInteractionType(event)
if (interaction === 'none') return if (interaction === 'none') return
// If there's a valid interaction and the mouse is moving,
// our past (and current) interaction was a drag.
this.wasDragging = true
if (this.syncDirection === 'engineToClient') { if (this.syncDirection === 'engineToClient') {
this.moveSender.send(() => { this.moveDataFromLastFrame = [event.clientX, event.clientY, interaction]
this.doMove(interaction, [event.clientX, event.clientY])
})
return return
} }
// else "clientToEngine" (Sketch Mode) or forceUpdate
// Implement camera movement logic here based on deltaMove // Implement camera movement logic here based on deltaMove
// For example, for rotating the camera around the target: // For example, for rotating the camera around the target:
if (interaction === 'rotate') { if (interaction === 'rotate') {
@ -436,9 +418,6 @@ export class CameraControls {
* under the cursor. This recently moved from being handled in App.tsx. * under the cursor. This recently moved from being handled in App.tsx.
* This might not be the right spot, but it is more consolidated. * This might not be the right spot, but it is more consolidated.
*/ */
// Clear any previous drag state
this.wasDragging = false
if (this.syncDirection === 'engineToClient') { if (this.syncDirection === 'engineToClient') {
const newCmdId = uuidv4() const newCmdId = uuidv4()
@ -480,9 +459,7 @@ export class CameraControls {
if (this.syncDirection === 'engineToClient') { if (this.syncDirection === 'engineToClient') {
if (interaction === 'zoom') { if (interaction === 'zoom') {
this.zoomSender.send(() => { this.zoomDataFromLastFrame = event.deltaY
this.doZoom(event.deltaY)
})
} else { } else {
// This case will get handled when we add pan and rotate using Apple trackpad. // This case will get handled when we add pan and rotate using Apple trackpad.
console.error( console.error(

View File

@ -338,11 +338,6 @@ export class SceneEntities {
sceneInfra.setCallbacks({ sceneInfra.setCallbacks({
onClick: async (args) => { onClick: async (args) => {
if (!args) return if (!args) return
// If there is a valid camera interaction that matches, do that instead
const interaction = sceneInfra.camControls.getInteractionType(
args.mouseEvent
)
if (interaction !== 'none') return
if (args.mouseEvent.which !== 1) return if (args.mouseEvent.which !== 1) return
const { intersectionPoint } = args const { intersectionPoint } = args
if (!intersectionPoint?.twoD || !sketchDetails?.sketchPathToNode) return if (!intersectionPoint?.twoD || !sketchDetails?.sketchPathToNode) return
@ -412,7 +407,7 @@ export class SceneEntities {
if (err(sketch)) return Promise.reject(sketch) if (err(sketch)) return Promise.reject(sketch)
if (!sketch) return Promise.reject('sketch not found') if (!sketch) return Promise.reject('sketch not found')
if (!isArray(sketch?.paths)) if (!isArray(sketch?.value))
return { return {
truncatedAst, truncatedAst,
programMemoryOverride, programMemoryOverride,
@ -440,7 +435,7 @@ export class SceneEntities {
maybeModdedAst, maybeModdedAst,
sketch.start.__geoMeta.sourceRange sketch.start.__geoMeta.sourceRange
) )
if (sketch?.paths?.[0]?.type !== 'Circle') { if (sketch?.value?.[0]?.type !== 'Circle') {
const _profileStart = createProfileStartHandle({ const _profileStart = createProfileStartHandle({
from: sketch.start.from, from: sketch.start.from,
id: sketch.start.__geoMeta.id, id: sketch.start.__geoMeta.id,
@ -456,16 +451,16 @@ export class SceneEntities {
this.activeSegments[JSON.stringify(segPathToNode)] = _profileStart this.activeSegments[JSON.stringify(segPathToNode)] = _profileStart
} }
const callbacks: (() => SegmentOverlayPayload | null)[] = [] const callbacks: (() => SegmentOverlayPayload | null)[] = []
sketch.paths.forEach((segment, index) => { sketch.value.forEach((segment, index) => {
let segPathToNode = getNodePathFromSourceRange( let segPathToNode = getNodePathFromSourceRange(
maybeModdedAst, maybeModdedAst,
segment.__geoMeta.sourceRange segment.__geoMeta.sourceRange
) )
if ( if (
draftExpressionsIndices && draftExpressionsIndices &&
(sketch.paths[index - 1] || sketch.start) (sketch.value[index - 1] || sketch.start)
) { ) {
const previousSegment = sketch.paths[index - 1] || sketch.start const previousSegment = sketch.value[index - 1] || sketch.start
const previousSegmentPathToNode = getNodePathFromSourceRange( const previousSegmentPathToNode = getNodePathFromSourceRange(
maybeModdedAst, maybeModdedAst,
previousSegment.__geoMeta.sourceRange previousSegment.__geoMeta.sourceRange
@ -516,7 +511,7 @@ export class SceneEntities {
to: segment.to, to: segment.to,
} }
const result = initSegment({ const result = initSegment({
prevSegment: sketch.paths[index - 1], prevSegment: sketch.value[index - 1],
callExpName, callExpName,
input, input,
id: segment.__geoMeta.id, id: segment.__geoMeta.id,
@ -615,9 +610,9 @@ export class SceneEntities {
variableDeclarationName variableDeclarationName
) )
if (err(sg)) return Promise.reject(sg) if (err(sg)) return Promise.reject(sg)
const lastSeg = sg?.paths?.slice(-1)[0] || sg.start const lastSeg = sg?.value?.slice(-1)[0] || sg.start
const index = sg.paths.length // because we've added a new segment that's not in the memory yet, no need for `-1` const index = sg.value.length // because we've added a new segment that's not in the memory yet, no need for `-1`
const mod = addNewSketchLn({ const mod = addNewSketchLn({
node: _ast, node: _ast,
programMemory: kclManager.programMemory, programMemory: kclManager.programMemory,
@ -650,13 +645,7 @@ export class SceneEntities {
sceneInfra.setCallbacks({ sceneInfra.setCallbacks({
onClick: async (args) => { onClick: async (args) => {
if (!args) return if (!args) return
// If there is a valid camera interaction that matches, do that instead
const interaction = sceneInfra.camControls.getInteractionType(
args.mouseEvent
)
if (interaction !== 'none') return
if (args.mouseEvent.which !== 1) return if (args.mouseEvent.which !== 1) return
const { intersectionPoint } = args const { intersectionPoint } = args
let intersection2d = intersectionPoint?.twoD let intersection2d = intersectionPoint?.twoD
const profileStart = args.intersects const profileStart = args.intersects
@ -665,7 +654,7 @@ export class SceneEntities {
let modifiedAst let modifiedAst
if (profileStart) { if (profileStart) {
const lastSegment = sketch.paths.slice(-1)[0] const lastSegment = sketch.value.slice(-1)[0]
modifiedAst = addCallExpressionsToPipe({ modifiedAst = addCallExpressionsToPipe({
node: kclManager.ast, node: kclManager.ast,
programMemory: kclManager.programMemory, programMemory: kclManager.programMemory,
@ -697,7 +686,7 @@ export class SceneEntities {
}) })
if (trap(modifiedAst)) return Promise.reject(modifiedAst) if (trap(modifiedAst)) return Promise.reject(modifiedAst)
} else if (intersection2d) { } else if (intersection2d) {
const lastSegment = sketch.paths.slice(-1)[0] const lastSegment = sketch.value.slice(-1)[0]
const tmp = addNewSketchLn({ const tmp = addNewSketchLn({
node: kclManager.ast, node: kclManager.ast,
programMemory: kclManager.programMemory, programMemory: kclManager.programMemory,
@ -828,7 +817,7 @@ export class SceneEntities {
variableDeclarationName variableDeclarationName
) )
if (err(sketch)) return Promise.reject(sketch) if (err(sketch)) return Promise.reject(sketch)
const sgPaths = sketch.paths const sgPaths = sketch.value
const orthoFactor = orthoScale(sceneInfra.camControls.camera) const orthoFactor = orthoScale(sceneInfra.camControls.camera)
this.updateSegment(sketch.start, 0, 0, _ast, orthoFactor, sketch) this.updateSegment(sketch.start, 0, 0, _ast, orthoFactor, sketch)
@ -837,11 +826,6 @@ export class SceneEntities {
) )
}, },
onClick: async (args) => { onClick: async (args) => {
// If there is a valid camera interaction that matches, do that instead
const interaction = sceneInfra.camControls.getInteractionType(
args.mouseEvent
)
if (interaction !== 'none') return
// Commit the rectangle to the full AST/code and return to sketch.idle // Commit the rectangle to the full AST/code and return to sketch.idle
const cornerPoint = args.intersectionPoint?.twoD const cornerPoint = args.intersectionPoint?.twoD
if (!cornerPoint || args.mouseEvent.button !== 0) return if (!cornerPoint || args.mouseEvent.button !== 0) return
@ -884,7 +868,7 @@ export class SceneEntities {
variableDeclarationName variableDeclarationName
) )
if (err(sketch)) return if (err(sketch)) return
const sgPaths = sketch.paths const sgPaths = sketch.value
const orthoFactor = orthoScale(sceneInfra.camControls.camera) const orthoFactor = orthoScale(sceneInfra.camControls.camera)
// Update the starting segment of the THREEjs scene // Update the starting segment of the THREEjs scene
@ -1001,7 +985,7 @@ export class SceneEntities {
variableDeclarationName variableDeclarationName
) )
if (err(sketch)) return if (err(sketch)) return
const sgPaths = sketch.paths const sgPaths = sketch.value
const orthoFactor = orthoScale(sceneInfra.camControls.camera) const orthoFactor = orthoScale(sceneInfra.camControls.camera)
this.updateSegment(sketch.start, 0, 0, _ast, orthoFactor, sketch) this.updateSegment(sketch.start, 0, 0, _ast, orthoFactor, sketch)
@ -1010,11 +994,6 @@ export class SceneEntities {
) )
}, },
onClick: async (args) => { onClick: async (args) => {
// If there is a valid camera interaction that matches, do that instead
const interaction = sceneInfra.camControls.getInteractionType(
args.mouseEvent
)
if (interaction !== 'none') return
// Commit the rectangle to the full AST/code and return to sketch.idle // Commit the rectangle to the full AST/code and return to sketch.idle
const cornerPoint = args.intersectionPoint?.twoD const cornerPoint = args.intersectionPoint?.twoD
if (!cornerPoint || args.mouseEvent.button !== 0) return if (!cornerPoint || args.mouseEvent.button !== 0) return
@ -1126,7 +1105,7 @@ export class SceneEntities {
const pipeIndex = pathToNode[pathToNodeIndex + 1][0] as number const pipeIndex = pathToNode[pathToNodeIndex + 1][0] as number
if (addingNewSegmentStatus === 'nothing') { if (addingNewSegmentStatus === 'nothing') {
const prevSegment = sketch.paths[pipeIndex - 2] const prevSegment = sketch.value[pipeIndex - 2]
const mod = addNewSketchLn({ const mod = addNewSketchLn({
node: kclManager.ast, node: kclManager.ast,
programMemory: kclManager.programMemory, programMemory: kclManager.programMemory,
@ -1178,11 +1157,6 @@ export class SceneEntities {
}, },
onMove: () => {}, onMove: () => {},
onClick: (args) => { onClick: (args) => {
// If there is a valid camera interaction that matches, do that instead
const interaction = sceneInfra.camControls.getInteractionType(
args.mouseEvent
)
if (interaction !== 'none') return
if (args?.mouseEvent.which !== 1) return if (args?.mouseEvent.which !== 1) return
if (!args || !args.selected) { if (!args || !args.selected) {
sceneInfra.modelingSend({ sceneInfra.modelingSend({
@ -1371,7 +1345,7 @@ export class SceneEntities {
} }
if (!sketch) return if (!sketch) return
const sgPaths = sketch.paths const sgPaths = sketch.value
const orthoFactor = orthoScale(sceneInfra.camControls.camera) const orthoFactor = orthoScale(sceneInfra.camControls.camera)
this.updateSegment( this.updateSegment(
@ -1419,7 +1393,7 @@ export class SceneEntities {
modifiedAst, modifiedAst,
segment.__geoMeta.sourceRange segment.__geoMeta.sourceRange
) )
const sgPaths = sketch.paths const sgPaths = sketch.value
const originalPathToNodeStr = JSON.stringify(segPathToNode) const originalPathToNodeStr = JSON.stringify(segPathToNode)
segPathToNode[1][0] = varDecIndex segPathToNode[1][0] = varDecIndex
const pathToNodeStr = JSON.stringify(segPathToNode) const pathToNodeStr = JSON.stringify(segPathToNode)
@ -1727,7 +1701,7 @@ function prepareTruncatedMemoryAndAst(
variableDeclarationName variableDeclarationName
) )
if (err(sg)) return sg if (err(sg)) return sg
const lastSeg = sg?.paths.slice(-1)[0] const lastSeg = sg?.value.slice(-1)[0]
if (draftSegment) { if (draftSegment) {
// truncatedAst needs to setup with another segment at the end // truncatedAst needs to setup with another segment at the end
let newSegment let newSegment

View File

@ -58,7 +58,7 @@ import { err } from 'lib/trap'
interface CreateSegmentArgs { interface CreateSegmentArgs {
input: SegmentInputs input: SegmentInputs
prevSegment: Sketch['paths'][number] prevSegment: Sketch['value'][number]
id: string id: string
pathToNode: PathToNode pathToNode: PathToNode
isDraftSegment?: boolean isDraftSegment?: boolean
@ -72,7 +72,7 @@ interface CreateSegmentArgs {
interface UpdateSegmentArgs { interface UpdateSegmentArgs {
input: SegmentInputs input: SegmentInputs
prevSegment: Sketch['paths'][number] prevSegment: Sketch['value'][number]
group: Group group: Group
sceneInfra: SceneInfra sceneInfra: SceneInfra
scale?: number scale?: number

View File

@ -135,9 +135,7 @@ function CommandArgOptionInput({
<Combobox.Input <Combobox.Input
id="option-input" id="option-input"
ref={inputRef} ref={inputRef}
onChange={(event) => onChange={(event) => setQuery(event.target.value)}
!event.target.disabled && setQuery(event.target.value)
}
className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none" className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none"
onKeyDown={(event) => { onKeyDown={(event) => {
if (event.metaKey && event.key === 'k') if (event.metaKey && event.key === 'k')
@ -177,18 +175,9 @@ function CommandArgOptionInput({
<Combobox.Option <Combobox.Option
key={option.name} key={option.name}
value={option} value={option}
disabled={option.disabled}
className="flex items-center gap-2 px-4 py-1 first:mt-2 last:mb-2 ui-active:bg-primary/10 dark:ui-active:bg-chalkboard-90" className="flex items-center gap-2 px-4 py-1 first:mt-2 last:mb-2 ui-active:bg-primary/10 dark:ui-active:bg-chalkboard-90"
> >
<p <p className="flex-grow">{option.name} </p>
className={`flex-grow ${
(option.disabled &&
'text-chalkboard-70 dark:text-chalkboard-50 cursor-not-allowed') ||
''
}`}
>
{option.name}
</p>
{option.value === currentOption?.value && ( {option.value === currentOption?.value && (
<small className="text-chalkboard-70 dark:text-chalkboard-50"> <small className="text-chalkboard-70 dark:text-chalkboard-50">
current current

View File

@ -140,7 +140,7 @@ const FileTreeItem = ({
async (eventType, path) => { async (eventType, path) => {
// Don't try to read a file that was removed. // Don't try to read a file that was removed.
if (isCurrentFile && eventType !== 'unlink') { if (isCurrentFile && eventType !== 'unlink') {
let code = await window.electron.readFile(path, { encoding: 'utf-8' }) let code = await window.electron.readFile(path, 'utf-8')
code = normalizeLineEndings(code) code = normalizeLineEndings(code)
codeManager.updateCodeStateEditor(code) codeManager.updateCodeStateEditor(code)
} }
@ -488,12 +488,6 @@ export const FileTreeInner = ({
// Refresh the file tree when there are changes. // Refresh the file tree when there are changes.
useFileSystemWatcher( useFileSystemWatcher(
async (eventType, path) => { async (eventType, path) => {
// Our other watcher races with this watcher on the current file changes,
// so we need to stop this one from reacting at all, otherwise Bad Things
// Happen™.
const isCurrentFile = loaderData.file?.path === path
const hasChanged = eventType === 'change'
if (isCurrentFile && hasChanged) return
fileSend({ type: 'Refresh' }) fileSend({ type: 'Refresh' })
}, },
[loaderData?.project?.path, fileContext.selectedDirectory.path].filter( [loaderData?.project?.path, fileContext.selectedDirectory.path].filter(

View File

@ -69,7 +69,7 @@ import { exportFromEngine } from 'lib/exportFromEngine'
import { Models } from '@kittycad/lib/dist/types/src' import { Models } from '@kittycad/lib/dist/types/src'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { EditorSelection, Transaction } from '@codemirror/state' import { EditorSelection, Transaction } from '@codemirror/state'
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom' import { useNavigate, useSearchParams } from 'react-router-dom'
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls' import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
import { getVarNameModal } from 'hooks/useToolbarGuards' import { getVarNameModal } from 'hooks/useToolbarGuards'
import { err, reportRejection, trap } from 'lib/trap' import { err, reportRejection, trap } from 'lib/trap'
@ -84,7 +84,6 @@ import {
import { submitAndAwaitTextToKcl } from 'lib/textToCad' import { submitAndAwaitTextToKcl } from 'lib/textToCad'
import { useFileContext } from 'hooks/useFileContext' import { useFileContext } from 'hooks/useFileContext'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
import { IndexLoaderData } from 'lib/types'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -117,7 +116,6 @@ export const ModelingMachineProvider = ({
} = useSettingsAuthContext() } = useSettingsAuthContext()
const navigate = useNavigate() const navigate = useNavigate()
const { context, send: fileMachineSend } = useFileContext() const { context, send: fileMachineSend } = useFileContext()
const { file } = useLoaderData() as IndexLoaderData
const token = auth?.context?.token const token = auth?.context?.token
const streamRef = useRef<HTMLDivElement>(null) const streamRef = useRef<HTMLDivElement>(null)
const persistedContext = useMemo(() => getPersistedContext(), []) const persistedContext = useMemo(() => getPersistedContext(), [])
@ -411,15 +409,12 @@ export const ModelingMachineProvider = ({
Make: ({ event }) => { Make: ({ event }) => {
if (event.type !== 'Make') return if (event.type !== 'Make') return
// Check if we already have an export intent. // Check if we already have an export intent.
if (engineCommandManager.exportInfo) { if (engineCommandManager.exportIntent) {
toast.error('Already exporting') toast.error('Already exporting')
return return
} }
// Set the export intent. // Set the export intent.
engineCommandManager.exportInfo = { engineCommandManager.exportIntent = ExportIntent.Make
intent: ExportIntent.Make,
name: file?.name || '',
}
// Set the current machine. // Set the current machine.
machineManager.currentMachine = event.data.machine machineManager.currentMachine = event.data.machine
@ -448,16 +443,12 @@ export const ModelingMachineProvider = ({
}, },
'Engine export': ({ event }) => { 'Engine export': ({ event }) => {
if (event.type !== 'Export') return if (event.type !== 'Export') return
if (engineCommandManager.exportInfo) { if (engineCommandManager.exportIntent) {
toast.error('Already exporting') toast.error('Already exporting')
return return
} }
// Set the export intent. // Set the export intent.
engineCommandManager.exportInfo = { engineCommandManager.exportIntent = ExportIntent.Save
intent: ExportIntent.Save,
// This never gets used its only for make.
name: '',
}
const format = { const format = {
...event.data, ...event.data,
@ -644,7 +635,6 @@ export const ModelingMachineProvider = ({
input.plane input.plane
) )
await kclManager.updateAst(modifiedAst, false) await kclManager.updateAst(modifiedAst, false)
sceneInfra.camControls.enableRotate = false
sceneInfra.camControls.syncDirection = 'clientToEngine' sceneInfra.camControls.syncDirection = 'clientToEngine'
await letEngineAnimateAndSyncCamAfter( await letEngineAnimateAndSyncCamAfter(

View File

@ -95,7 +95,7 @@ export const processMemory = (programMemory: ProgramMemory) => {
return rest return rest
}) })
} else if (!err(sg)) { } else if (!err(sg)) {
processedMemory[key] = sg.paths.map(({ __geoMeta, ...rest }: Path) => { processedMemory[key] = sg.value.map(({ __geoMeta, ...rest }: Path) => {
return rest return rest
}) })
} else if ((val.type as any) === 'Function') { } else if ((val.type as any) === 'Function') {

View File

@ -11,7 +11,6 @@ export const NetworkMachineIndicator = ({
}) => { }) => {
const machineCount = machineManager.machineCount() const machineCount = machineManager.machineCount()
const reason = machineManager.noMachinesReason() const reason = machineManager.noMachinesReason()
const machines = machineManager.machines
return isDesktop() ? ( return isDesktop() ? (
<Popover className="relative"> <Popover className="relative">
@ -47,34 +46,20 @@ export const NetworkMachineIndicator = ({
</div> </div>
{machineCount > 0 && ( {machineCount > 0 && (
<ul className="divide-y divide-chalkboard-20 dark:divide-chalkboard-80"> <ul className="divide-y divide-chalkboard-20 dark:divide-chalkboard-80">
{machines.map((machine) => { {Object.entries(machineManager.machines).map(
return ( ([hostname, machine]) => (
<li key={machine.id} className={'px-2 py-4 gap-1 last:mb-0 '}> <li key={hostname} className={'px-2 py-4 gap-1 last:mb-0 '}>
<p className="">{machine.id.toUpperCase()}</p> <p className="">
<p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs"> {machine.make_model.model ||
{machine.make_model.model} machine.make_model.manufacturer ||
'Unknown Machine'}
</p> </p>
{machine.extra &&
machine.extra.type === 'bambu' &&
machine.extra.nozzle_diameter && (
<p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs"> <p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs">
Nozzle Diameter: {machine.extra.nozzle_diameter} Hostname {hostname}
</p>
)}
<p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs">
{`Status: ${machine.state.state
.charAt(0)
.toUpperCase()}${machine.state.state.slice(1)}`}
{machine.state.state === 'failed' && machine.state.message
? ` (${machine.state.message})`
: ''}
{machine.state.state === 'running' && machine.progress
? ` (${Math.round(machine.progress)}%)`
: ''}
</p> </p>
</li> </li>
) )
})} )}
</ul> </ul>
)} )}
</Popover.Panel> </Popover.Panel>

View File

@ -255,14 +255,10 @@ export const Stream = () => {
}, [mediaStream]) }, [mediaStream])
const handleMouseUp: MouseEventHandler<HTMLDivElement> = (e) => { const handleMouseUp: MouseEventHandler<HTMLDivElement> = (e) => {
// If we've got no stream or connection, don't do anything
if (!isNetworkOkay) return if (!isNetworkOkay) return
if (!videoRef.current) return if (!videoRef.current) return
// If we're in sketch mode, don't send a engine-side select event
if (state.matches('Sketch')) return if (state.matches('Sketch')) return
if (state.matches({ idle: 'showPlanes' })) return if (state.matches({ idle: 'showPlanes' })) return
// If we're mousing up from a camera drag, don't send a select event
if (sceneInfra.camControls.wasDragging === true) return
if (btnName(e.nativeEvent).left) { if (btnName(e.nativeEvent).left) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises

View File

@ -1,153 +0,0 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { vi } from 'vitest'
import { ToastUpdate } from './ToastUpdate'
describe('ToastUpdate tests', () => {
const testData = {
version: '0.255.255',
files: [
{
url: 'Zoo Modeling App-0.255.255-x64-mac.zip',
sha512:
'VJb0qlrqNr+rVx3QLATz+B28dtHw3osQb5/+UUmQUIMuF9t0i8dTKOVL/2lyJSmLJVw2/SGDB4Ud6VlTPJ6oFw==',
size: 141277345,
},
{
url: 'Zoo Modeling App-0.255.255-arm64-mac.zip',
sha512:
'b+ugdg7A4LhYYJaFkPRxh1RvmGGMlPJJj7inkLg9PwRtCnR9ePMlktj2VRciXF1iLh59XW4bLc4dK1dFQHMULA==',
size: 135278259,
},
{
url: 'Zoo Modeling App-0.255.255-x64-mac.dmg',
sha512:
'gCUqww05yj8OYwPiTq6bo5GbkpngSbXGtenmDD7+kUm0UyVK8WD3dMAfQJtGNG5HY23aHCHe9myE2W4mbZGmiQ==',
size: 146004232,
},
{
url: 'Zoo Modeling App-0.255.255-arm64-mac.dmg',
sha512:
'ND871ayf81F1ZT+iWVLYTc2jdf/Py6KThuxX2QFWz14ebmIbJPL07lNtxQOexOFiuk0MwRhlCy1RzOSG1b9bmw==',
size: 140021522,
},
],
path: 'Zoo Modeling App-0.255.255-x64-mac.zip',
sha512:
'VJb0qlrqNr+rVx3QLATz+B28dtHw3osQb5/+UUmQUIMuF9t0i8dTKOVL/2lyJSmLJVw2/SGDB4Ud6VlTPJ6oFw==',
releaseNotes:
'## Some markdown release notes\n\n- This is a list item\n- This is another list item\n\n```javascript\nconsole.log("Hello, world!")\n```\n',
releaseDate: '2024-10-09T11:57:59.133Z',
} as const
test('Happy path: renders the toast with good data', () => {
const onRestart = vi.fn()
const onDismiss = vi.fn()
render(
<ToastUpdate
onRestart={onRestart}
onDismiss={onDismiss}
version={testData.version}
releaseNotes={testData.releaseNotes}
/>
)
// Locators and other constants
const versionText = screen.getByTestId('update-version')
const restartButton = screen.getByRole('button', { name: /restart/i })
const dismissButton = screen.getByRole('button', { name: /got it/i })
const releaseNotes = screen.getByTestId('release-notes')
expect(versionText).toBeVisible()
expect(versionText).toHaveTextContent(testData.version)
expect(restartButton).toBeEnabled()
fireEvent.click(restartButton)
expect(onRestart.mock.calls).toHaveLength(1)
expect(dismissButton).toBeEnabled()
fireEvent.click(dismissButton)
expect(onDismiss.mock.calls).toHaveLength(1)
// I cannot for the life of me seem to get @testing-library/react
// to properly handle click events or visibility checks on the details element.
// So I'm only checking that the content is in the document.
expect(releaseNotes).toBeInTheDocument()
expect(releaseNotes).toHaveTextContent('Release notes')
const releaseNotesListItems = screen.getAllByRole('listitem')
expect(releaseNotesListItems.map((el) => el.textContent)).toEqual([
'This is a list item',
'This is another list item',
])
})
test('Happy path: renders the breaking changes notice', () => {
const releaseNotesWithBreakingChanges = `
## Some markdown release notes
- This is a list item
- This is another list item with a breaking change
- This is a list item
`
const onRestart = vi.fn()
const onDismiss = vi.fn()
render(
<ToastUpdate
onRestart={onRestart}
onDismiss={onDismiss}
version={testData.version}
releaseNotes={releaseNotesWithBreakingChanges}
/>
)
// Locators and other constants
const releaseNotes = screen.getByText('Release notes', {
selector: 'summary',
})
const listItemContents = screen
.getAllByRole('listitem')
.map((el) => el.textContent)
// I cannot for the life of me seem to get @testing-library/react
// to properly handle click events or visibility checks on the details element.
// So I'm only checking that the content is in the document.
expect(releaseNotes).toBeInTheDocument()
expect(listItemContents).toEqual([
'This is a list item',
'This is another list item with a breaking change',
'This is a list item',
])
})
test('Missing release notes: renders the toast without release notes', () => {
const onRestart = vi.fn()
const onDismiss = vi.fn()
render(
<ToastUpdate
onRestart={onRestart}
onDismiss={onDismiss}
version={testData.version}
releaseNotes={''}
/>
)
// Locators and other constants
const versionText = screen.getByTestId('update-version')
const restartButton = screen.getByRole('button', { name: /restart/i })
const dismissButton = screen.getByRole('button', { name: /got it/i })
const releaseNotes = screen.queryByText(/release notes/i, {
selector: 'details > summary',
})
const releaseNotesListItem = screen.queryByRole('listitem', {
name: /this is a list item/i,
})
expect(versionText).toBeVisible()
expect(versionText).toHaveTextContent(testData.version)
expect(releaseNotes).not.toBeInTheDocument()
expect(releaseNotesListItem).not.toBeInTheDocument()
expect(restartButton).toBeEnabled()
expect(dismissButton).toBeEnabled()
})
})

View File

@ -1,23 +1,14 @@
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { ActionButton } from './ActionButton' import { ActionButton } from './ActionButton'
import { openExternalBrowserIfDesktop } from 'lib/openWindow' import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { Marked } from '@ts-stack/markdown'
export function ToastUpdate({ export function ToastUpdate({
version, version,
releaseNotes,
onRestart, onRestart,
onDismiss,
}: { }: {
version: string version: string
releaseNotes?: string
onRestart: () => void onRestart: () => void
onDismiss: () => void
}) { }) {
const containsBreakingChanges = releaseNotes
?.toLocaleLowerCase()
.includes('breaking')
return ( return (
<div className="inset-0 z-50 grid place-content-center rounded bg-chalkboard-110/50 shadow-md"> <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"> <div className="max-w-3xl min-w-[35rem] p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
@ -28,7 +19,7 @@ export function ToastUpdate({
> >
v{version} v{version}
</span> </span>
<p className="ml-4 text-md text-bold"> <span className="ml-4 text-md text-bold">
A new update has downloaded and will be available next time you A new update has downloaded and will be available next time you
start the app. You can view the release notes{' '} start the app. You can view the release notes{' '}
<a <a
@ -41,39 +32,15 @@ export function ToastUpdate({
> >
here on GitHub. here on GitHub.
</a> </a>
</p> </span>
</div> </div>
{releaseNotes && (
<details
className="my-4 border border-chalkboard-30 dark:border-chalkboard-60 rounded"
open={containsBreakingChanges}
data-testid="release-notes"
>
<summary className="p-2 select-none cursor-pointer">
Release notes
{containsBreakingChanges && (
<strong className="text-destroy-50"> (Breaking changes)</strong>
)}
</summary>
<div
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,
}),
}}
></div>
</details>
)}
<div className="flex justify-between gap-8"> <div className="flex justify-between gap-8">
<ActionButton <ActionButton
Element="button" Element="button"
iconStart={{ iconStart={{
icon: 'arrowRotateRight', icon: 'arrowRotateRight',
}} }}
name="restart" name="Restart app now"
onClick={onRestart} onClick={onRestart}
> >
Restart app now Restart app now
@ -83,10 +50,9 @@ export function ToastUpdate({
iconStart={{ iconStart={{
icon: 'checkmark', icon: 'checkmark',
}} }}
name="dismiss" name="Got it"
onClick={() => { onClick={() => {
toast.dismiss() toast.dismiss()
onDismiss()
}} }}
> >
Got it Got it

View File

@ -1,9 +1,6 @@
import { styleTags, tags as t } from '@lezer/highlight' import { styleTags, tags as t } from '@lezer/highlight'
export const kclHighlight = styleTags({ export const kclHighlight = styleTags({
'import export': t.moduleKeyword,
ImportItemAs: t.definitionKeyword,
ImportFrom: t.moduleKeyword,
'fn var let const': t.definitionKeyword, 'fn var let const': t.definitionKeyword,
'if else': t.controlKeyword, 'if else': t.controlKeyword,
return: t.controlKeyword, return: t.controlKeyword,
@ -11,7 +8,7 @@ export const kclHighlight = styleTags({
nil: t.null, nil: t.null,
'AddOp MultOp ExpOp': t.arithmeticOperator, 'AddOp MultOp ExpOp': t.arithmeticOperator,
BangOp: t.logicOperator, BangOp: t.logicOperator,
CompOp: t.compareOperator, CompOp: t.logicOperator,
'Equals Arrow': t.definitionOperator, 'Equals Arrow': t.definitionOperator,
PipeOperator: t.controlOperator, PipeOperator: t.controlOperator,
String: t.string, String: t.string,

View File

@ -15,9 +15,8 @@
} }
statement[@isGroup=Statement] { statement[@isGroup=Statement] {
ImportStatement { kw<"import"> ImportItems ImportFrom String } | FunctionDeclaration { kw<"fn"> VariableDefinition Equals ParamList Arrow Body } |
FunctionDeclaration { kw<"export">? kw<"fn"> VariableDefinition Equals ParamList Arrow Body } | VariableDeclaration { (kw<"var"> | kw<"let"> | kw<"const">)? VariableDefinition Equals expression } |
VariableDeclaration { kw<"export">? (kw<"var"> | kw<"let"> | kw<"const">)? VariableDefinition Equals expression } |
ReturnStatement { kw<"return"> expression } | ReturnStatement { kw<"return"> expression } |
ExpressionStatement { expression } ExpressionStatement { expression }
} }
@ -26,9 +25,6 @@ ParamList { "(" commaSep<Parameter { VariableDefinition "?"? (":" type)? }> ")"
Body { "{" statement* "}" } Body { "{" statement* "}" }
ImportItems { commaSep1NoTrailingComma<ImportItem> }
ImportItem { identifier (ImportItemAs identifier)? }
expression[@isGroup=Expression] { expression[@isGroup=Expression] {
String | String |
Number | Number |
@ -78,8 +74,6 @@ kw<term> { @specialize[@name={term}]<identifier, term> }
commaSep<term> { (term ("," term)*)? ","? } commaSep<term> { (term ("," term)*)? ","? }
commaSep1NoTrailingComma<term> { term ("," term)* }
@tokens { @tokens {
String[isolate] { "'" ("\\" _ | !['\\])* "'" | '"' ("\\" _ | !["\\])* '"' } String[isolate] { "'" ("\\" _ | !['\\])* "'" | '"' ("\\" _ | !["\\])* '"' }
@ -90,7 +84,7 @@ commaSep1NoTrailingComma<term> { term ("," term)* }
MultOp { "/" | "*" | "\\" } MultOp { "/" | "*" | "\\" }
ExpOp { "^" } ExpOp { "^" }
BangOp { "!" } BangOp { "!" }
CompOp { "==" | "!=" | "<=" | ">=" | "<" | ">" } CompOp { $[<>] "="? | "!=" | "==" }
Equals { "=" } Equals { "=" }
Arrow { "=>" } Arrow { "=>" }
PipeOperator { "|>" } PipeOperator { "|>" }
@ -112,9 +106,6 @@ commaSep1NoTrailingComma<term> { term ("," term)* }
Shebang { "#!" ![\n]* } Shebang { "#!" ![\n]* }
ImportItemAs { "as" }
ImportFrom { "from" }
"(" ")" "(" ")"
"{" "}" "{" "}"
"[" "]" "[" "]"

View File

@ -293,24 +293,6 @@ code {
which lets you use them with @apply in your CSS, and get which lets you use them with @apply in your CSS, and get
autocomplete in classNames in your JSX. autocomplete in classNames in your JSX.
*/ */
.parsed-markdown ul,
.parsed-markdown ol {
@apply list-outside pl-4 lg:pl-8 my-2;
}
.parsed-markdown ul li {
@apply list-disc;
}
.parsed-markdown li p {
@apply inline;
}
.parsed-markdown code {
@apply px-1 py-0.5 rounded-sm;
@apply bg-chalkboard-20 text-chalkboard-80;
@apply dark:bg-chalkboard-80 dark:text-chalkboard-30;
}
} }
#code-mirror-override .cm-scroller, #code-mirror-override .cm-scroller,

View File

@ -70,17 +70,15 @@ if (isDesktop()) {
id: AUTO_UPDATER_TOAST_ID, id: AUTO_UPDATER_TOAST_ID,
}) })
}) })
window.electron.onUpdateDownloaded(({ version, releaseNotes }) => { window.electron.onUpdateDownloaded((version: string) => {
const message = `A new update (${version}) was downloaded and will be available next time you open the app.` const message = `A new update (${version}) was downloaded and will be available next time you open the app.`
console.log(message) console.log(message)
toast.custom( toast.custom(
ToastUpdate({ ToastUpdate({
version, version,
releaseNotes,
onRestart: () => { onRestart: () => {
window.electron.appRestart() window.electron.appRestart()
}, },
onDismiss: () => {},
}), }),
{ duration: 30000, id: AUTO_UPDATER_TOAST_ID } { duration: 30000, id: AUTO_UPDATER_TOAST_ID }
) )

View File

@ -40,7 +40,9 @@ export class KclManager {
nonCodeMeta: { nonCodeMeta: {
nonCodeNodes: {}, nonCodeNodes: {},
start: [], start: [],
digest: null,
}, },
digest: null,
} }
private _execState: ExecState = emptyExecState() private _execState: ExecState = emptyExecState()
private _programMemory: ProgramMemory = ProgramMemory.empty() private _programMemory: ProgramMemory = ProgramMemory.empty()
@ -206,7 +208,9 @@ export class KclManager {
nonCodeMeta: { nonCodeMeta: {
nonCodeNodes: {}, nonCodeNodes: {},
start: [], start: [],
digest: null,
}, },
digest: null,
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -32,7 +32,7 @@ const mySketch001 = startSketchOn('XY')
sourceRange: [46, 71], sourceRange: [46, 71],
}, },
}, },
paths: [ value: [
{ {
type: 'ToPoint', type: 'ToPoint',
tag: null, tag: null,
@ -96,7 +96,7 @@ const mySketch001 = startSketchOn('XY')
on: expect.any(Object), on: expect.any(Object),
start: expect.any(Object), start: expect.any(Object),
type: 'Sketch', type: 'Sketch',
paths: [ value: [
{ {
type: 'ToPoint', type: 'ToPoint',
from: [0, 0], from: [0, 0],
@ -172,6 +172,7 @@ const sk2 = startSketchOn('XY')
start: 114, start: 114,
type: 'TagDeclarator', type: 'TagDeclarator',
value: 'p', value: 'p',
digest: null,
}, },
id: expect.any(String), id: expect.any(String),
sourceRange: [95, 117], sourceRange: [95, 117],
@ -202,7 +203,7 @@ const sk2 = startSketchOn('XY')
info: expect.any(Object), info: expect.any(Object),
}, },
}, },
paths: [ value: [
{ {
type: 'ToPoint', type: 'ToPoint',
from: [0, 0], from: [0, 0],
@ -222,6 +223,7 @@ const sk2 = startSketchOn('XY')
start: 114, start: 114,
type: 'TagDeclarator', type: 'TagDeclarator',
value: 'p', value: 'p',
digest: null,
}, },
__geoMeta: { __geoMeta: {
id: expect.any(String), id: expect.any(String),
@ -264,6 +266,7 @@ const sk2 = startSketchOn('XY')
start: 417, start: 417,
type: 'TagDeclarator', type: 'TagDeclarator',
value: 'o', value: 'o',
digest: null,
}, },
id: expect.any(String), id: expect.any(String),
sourceRange: [399, 420], sourceRange: [399, 420],
@ -294,7 +297,7 @@ const sk2 = startSketchOn('XY')
info: expect.any(Object), info: expect.any(Object),
}, },
}, },
paths: [ value: [
{ {
type: 'ToPoint', type: 'ToPoint',
from: [0, 0], from: [0, 0],
@ -314,6 +317,7 @@ const sk2 = startSketchOn('XY')
start: 417, start: 417,
type: 'TagDeclarator', type: 'TagDeclarator',
value: 'o', value: 'o',
digest: null,
}, },
__geoMeta: { __geoMeta: {
id: expect.any(String), id: expect.any(String),

View File

@ -18,7 +18,6 @@ export default class CodeManager {
#updateState: (arg: string) => void = () => {} #updateState: (arg: string) => void = () => {}
private _currentFilePath: string | null = null private _currentFilePath: string | null = null
private _hotkeys: { [key: string]: () => void } = {} private _hotkeys: { [key: string]: () => void } = {}
private timeoutWriter: ReturnType<typeof setTimeout> | undefined = undefined
constructor() { constructor() {
if (isDesktop()) { if (isDesktop()) {
@ -116,11 +115,7 @@ export default class CodeManager {
async writeToFile() { async writeToFile() {
if (isDesktop()) { if (isDesktop()) {
// Only write our buffer contents to file once per second. Any faster setTimeout(() => {
// and file-system watchers which read, will receive empty data during
// writes.
clearTimeout(this.timeoutWriter)
this.timeoutWriter = setTimeout(() => {
// Wait one event loop to give a chance for params to be set // Wait one event loop to give a chance for params to be set
// Save the file to disk // Save the file to disk
this._currentFilePath && this._currentFilePath &&
@ -131,7 +126,7 @@ export default class CodeManager {
console.error('error saving file', err) console.error('error saving file', err)
toast.error('Error saving file, please check file permissions') toast.error('Error saving file, please check file permissions')
}) })
}, 1000) })
} else { } else {
safeLSSetItem(PERSIST_CODE_KEY, this.code) safeLSSetItem(PERSIST_CODE_KEY, this.code)
} }

View File

@ -58,7 +58,7 @@ const newVar = myVar + 1`
` `
const mem = await exe(code) const mem = await exe(code)
// geo is three js buffer geometry and is very bloated to have in tests // geo is three js buffer geometry and is very bloated to have in tests
const minusGeo = mem.get('mySketch')?.value?.paths const minusGeo = mem.get('mySketch')?.value?.value
expect(minusGeo).toEqual([ expect(minusGeo).toEqual([
{ {
type: 'ToPoint', type: 'ToPoint',
@ -73,6 +73,7 @@ const newVar = myVar + 1`
start: 89, start: 89,
type: 'TagDeclarator', type: 'TagDeclarator',
value: 'myPath', value: 'myPath',
digest: null,
}, },
}, },
{ {
@ -98,6 +99,7 @@ const newVar = myVar + 1`
start: 143, start: 143,
type: 'TagDeclarator', type: 'TagDeclarator',
value: 'rightPath', value: 'rightPath',
digest: null,
}, },
}, },
]) ])
@ -175,7 +177,7 @@ const newVar = myVar + 1`
info: expect.any(Object), info: expect.any(Object),
}, },
}, },
paths: [ value: [
{ {
type: 'ToPoint', type: 'ToPoint',
to: [1, 1], to: [1, 1],
@ -199,6 +201,7 @@ const newVar = myVar + 1`
start: 109, start: 109,
type: 'TagDeclarator', type: 'TagDeclarator',
value: 'myPath', value: 'myPath',
digest: null,
}, },
}, },
{ {
@ -367,7 +370,7 @@ describe('testing math operators', () => {
const mem = await exe(code) const mem = await exe(code)
const sketch = sketchFromKclValue(mem.get('part001'), 'part001') const sketch = sketchFromKclValue(mem.get('part001'), 'part001')
// result of `-legLen(5, min(3, 999))` should be -4 // result of `-legLen(5, min(3, 999))` should be -4
const yVal = (sketch as Sketch).paths?.[0]?.to?.[1] const yVal = (sketch as Sketch).value?.[0]?.to?.[1]
expect(yVal).toBe(-4) expect(yVal).toBe(-4)
}) })
it('test that % substitution feeds down CallExp->ArrExp->UnaryExp->CallExp', async () => { it('test that % substitution feeds down CallExp->ArrExp->UnaryExp->CallExp', async () => {
@ -385,8 +388,8 @@ describe('testing math operators', () => {
const mem = await exe(code) const mem = await exe(code)
const sketch = sketchFromKclValue(mem.get('part001'), 'part001') const sketch = sketchFromKclValue(mem.get('part001'), 'part001')
// expect -legLen(segLen('seg01'), myVar) to equal -4 setting the y value back to 0 // expect -legLen(segLen('seg01'), myVar) to equal -4 setting the y value back to 0
expect((sketch as Sketch).paths?.[1]?.from).toEqual([3, 4]) expect((sketch as Sketch).value?.[1]?.from).toEqual([3, 4])
expect((sketch as Sketch).paths?.[1]?.to).toEqual([6, 0]) expect((sketch as Sketch).value?.[1]?.to).toEqual([6, 0])
const removedUnaryExp = code.replace( const removedUnaryExp = code.replace(
`-legLen(segLen(seg01), myVar)`, `-legLen(segLen(seg01), myVar)`,
`legLen(segLen(seg01), myVar)` `legLen(segLen(seg01), myVar)`
@ -398,7 +401,7 @@ describe('testing math operators', () => {
) )
// without the minus sign, the y value should be 8 // without the minus sign, the y value should be 8
expect((removedUnaryExpMemSketch as Sketch).paths?.[1]?.to).toEqual([6, 8]) expect((removedUnaryExpMemSketch as Sketch).value?.[1]?.to).toEqual([6, 8])
}) })
it('with nested callExpression and binaryExpression', async () => { it('with nested callExpression and binaryExpression', async () => {
const code = 'const myVar = 2 + min(100, -1 + legLen(5, 3))' const code = 'const myVar = 2 + min(100, -1 + legLen(5, 3))'

View File

@ -100,15 +100,15 @@ describe('Testing findUniqueName', () => {
it('should find a unique name', () => { it('should find a unique name', () => {
const result = findUniqueName( const result = findUniqueName(
JSON.stringify([ JSON.stringify([
{ type: 'Identifier', name: 'yo01', start: 0, end: 0 }, { type: 'Identifier', name: 'yo01', start: 0, end: 0, digest: null },
{ type: 'Identifier', name: 'yo02', start: 0, end: 0 }, { type: 'Identifier', name: 'yo02', start: 0, end: 0, digest: null },
{ type: 'Identifier', name: 'yo03', start: 0, end: 0 }, { type: 'Identifier', name: 'yo03', start: 0, end: 0, digest: null },
{ type: 'Identifier', name: 'yo04', start: 0, end: 0 }, { type: 'Identifier', name: 'yo04', start: 0, end: 0, digest: null },
{ type: 'Identifier', name: 'yo05', start: 0, end: 0 }, { type: 'Identifier', name: 'yo05', start: 0, end: 0, digest: null },
{ type: 'Identifier', name: 'yo06', start: 0, end: 0 }, { type: 'Identifier', name: 'yo06', start: 0, end: 0, digest: null },
{ type: 'Identifier', name: 'yo07', start: 0, end: 0 }, { type: 'Identifier', name: 'yo07', start: 0, end: 0, digest: null },
{ type: 'Identifier', name: 'yo08', start: 0, end: 0 }, { type: 'Identifier', name: 'yo08', start: 0, end: 0, digest: null },
{ type: 'Identifier', name: 'yo09', start: 0, end: 0 }, { type: 'Identifier', name: 'yo09', start: 0, end: 0, digest: null },
] satisfies Identifier[]), ] satisfies Identifier[]),
'yo', 'yo',
2 2
@ -123,7 +123,8 @@ describe('Testing addSketchTo', () => {
body: [], body: [],
start: 0, start: 0,
end: 0, end: 0,
nonCodeMeta: { nonCodeNodes: {}, start: [] }, nonCodeMeta: { nonCodeNodes: {}, start: [], digest: null },
digest: null,
}, },
'yz' 'yz'
) )

View File

@ -241,6 +241,7 @@ export function mutateObjExpProp(
value: updateWith, value: updateWith,
start: 0, start: 0,
end: 0, end: 0,
digest: null,
}) })
} }
} }
@ -500,7 +501,6 @@ export function sketchOnExtrudedFace(
createIdentifier(extrudeName ? extrudeName : oldSketchName), createIdentifier(extrudeName ? extrudeName : oldSketchName),
_tag, _tag,
]), ]),
undefined,
'const' 'const'
) )
@ -578,6 +578,7 @@ export function createLiteral(value: string | number): Literal {
end: 0, end: 0,
value, value,
raw: `${value}`, raw: `${value}`,
digest: null,
} }
} }
@ -586,7 +587,7 @@ export function createTagDeclarator(value: string): TagDeclarator {
type: 'TagDeclarator', type: 'TagDeclarator',
start: 0, start: 0,
end: 0, end: 0,
digest: null,
value, value,
} }
} }
@ -596,7 +597,7 @@ export function createIdentifier(name: string): Identifier {
type: 'Identifier', type: 'Identifier',
start: 0, start: 0,
end: 0, end: 0,
digest: null,
name, name,
} }
} }
@ -606,6 +607,7 @@ export function createPipeSubstitution(): PipeSubstitution {
type: 'PipeSubstitution', type: 'PipeSubstitution',
start: 0, start: 0,
end: 0, end: 0,
digest: null,
} }
} }
@ -621,11 +623,12 @@ export function createCallExpressionStdLib(
type: 'Identifier', type: 'Identifier',
start: 0, start: 0,
end: 0, end: 0,
digest: null,
name, name,
}, },
optional: false, optional: false,
arguments: args, arguments: args,
digest: null,
} }
} }
@ -641,11 +644,12 @@ export function createCallExpression(
type: 'Identifier', type: 'Identifier',
start: 0, start: 0,
end: 0, end: 0,
digest: null,
name, name,
}, },
optional: false, optional: false,
arguments: args, arguments: args,
digest: null,
} }
} }
@ -656,7 +660,7 @@ export function createArrayExpression(
type: 'ArrayExpression', type: 'ArrayExpression',
start: 0, start: 0,
end: 0, end: 0,
digest: null,
nonCodeMeta: nonCodeMetaEmpty(), nonCodeMeta: nonCodeMetaEmpty(),
elements, elements,
} }
@ -669,7 +673,7 @@ export function createPipeExpression(
type: 'PipeExpression', type: 'PipeExpression',
start: 0, start: 0,
end: 0, end: 0,
digest: null,
body, body,
nonCodeMeta: nonCodeMetaEmpty(), nonCodeMeta: nonCodeMetaEmpty(),
} }
@ -678,25 +682,23 @@ export function createPipeExpression(
export function createVariableDeclaration( export function createVariableDeclaration(
varName: string, varName: string,
init: VariableDeclarator['init'], init: VariableDeclarator['init'],
visibility: VariableDeclaration['visibility'] = 'default',
kind: VariableDeclaration['kind'] = 'const' kind: VariableDeclaration['kind'] = 'const'
): VariableDeclaration { ): VariableDeclaration {
return { return {
type: 'VariableDeclaration', type: 'VariableDeclaration',
start: 0, start: 0,
end: 0, end: 0,
digest: null,
declarations: [ declarations: [
{ {
type: 'VariableDeclarator', type: 'VariableDeclarator',
start: 0, start: 0,
end: 0, end: 0,
digest: null,
id: createIdentifier(varName), id: createIdentifier(varName),
init, init,
}, },
], ],
visibility,
kind, kind,
} }
} }
@ -708,14 +710,14 @@ export function createObjectExpression(properties: {
type: 'ObjectExpression', type: 'ObjectExpression',
start: 0, start: 0,
end: 0, end: 0,
digest: null,
nonCodeMeta: nonCodeMetaEmpty(), nonCodeMeta: nonCodeMetaEmpty(),
properties: Object.entries(properties).map(([key, value]) => ({ properties: Object.entries(properties).map(([key, value]) => ({
type: 'ObjectProperty', type: 'ObjectProperty',
start: 0, start: 0,
end: 0, end: 0,
key: createIdentifier(key), key: createIdentifier(key),
digest: null,
value, value,
})), })),
} }
@ -729,7 +731,7 @@ export function createUnaryExpression(
type: 'UnaryExpression', type: 'UnaryExpression',
start: 0, start: 0,
end: 0, end: 0,
digest: null,
operator, operator,
argument, argument,
} }
@ -744,7 +746,7 @@ export function createBinaryExpression([left, operator, right]: [
type: 'BinaryExpression', type: 'BinaryExpression',
start: 0, start: 0,
end: 0, end: 0,
digest: null,
operator, operator,
left, left,
right, right,
@ -1134,5 +1136,5 @@ export async function deleteFromSelection(
} }
const nonCodeMetaEmpty = () => { const nonCodeMetaEmpty = () => {
return { nonCodeNodes: {}, start: [] } return { nonCodeNodes: {}, start: [], digest: null }
} }

View File

@ -41,7 +41,7 @@ beforeAll(async () => {
}, },
}) })
}) })
}, 30_000) }, 20_000)
afterAll(() => { afterAll(() => {
engineCommandManager.tearDown() engineCommandManager.tearDown()
@ -620,7 +620,7 @@ describe('Testing button states', () => {
it('should return true when body exists and segment is selected', async () => { it('should return true when body exists and segment is selected', async () => {
await runButtonStateTest(codeWithBody, `line([10, 0], %)`, true) await runButtonStateTest(codeWithBody, `line([10, 0], %)`, true)
}) })
it('should return false when body exists and not a segment is selected', async () => { it('hould return false when body exists and not a segment is selected', async () => {
await runButtonStateTest(codeWithBody, `close(%)`, false) await runButtonStateTest(codeWithBody, `close(%)`, false)
}) })
}) })

View File

@ -1,7 +1,5 @@
import { import {
CallExpression, CallExpression,
Expr,
Identifier,
ObjectExpression, ObjectExpression,
PathToNode, PathToNode,
Program, Program,
@ -29,7 +27,7 @@ import {
sketchLineHelperMap, sketchLineHelperMap,
} from '../std/sketch' } from '../std/sketch'
import { err, trap } from 'lib/trap' import { err, trap } from 'lib/trap'
import { Selections } from 'lib/selections' import { Selections, canFilletSelection } from 'lib/selections'
import { KclCommandValue } from 'lib/commandTypes' import { KclCommandValue } from 'lib/commandTypes'
import { import {
ArtifactGraph, ArtifactGraph,
@ -68,10 +66,7 @@ export function modifyAstCloneWithFilletAndTag(
const artifactGraph = engineCommandManager.artifactGraph const artifactGraph = engineCommandManager.artifactGraph
// Step 1: modify ast with tags and group them by extrude nodes (bodies) // Step 1: modify ast with tags and group them by extrude nodes (bodies)
const extrudeToTagsMap: Map< const extrudeToTagsMap: Map<PathToNode, string[]> = new Map()
PathToNode,
Array<{ tag: string; selectionType: string }>
> = new Map()
const lookupMap: Map<string, PathToNode> = new Map() // work around for Map key comparison const lookupMap: Map<string, PathToNode> = new Map() // work around for Map key comparison
for (const selectionRange of selection.codeBasedSelections) { for (const selectionRange of selection.codeBasedSelections) {
@ -79,7 +74,6 @@ export function modifyAstCloneWithFilletAndTag(
codeBasedSelections: [selectionRange], codeBasedSelections: [selectionRange],
otherSelections: [], otherSelections: [],
} }
const selectionType = singleSelection.codeBasedSelections[0].type
const result = getPathToExtrudeForSegmentSelection( const result = getPathToExtrudeForSegmentSelection(
clonedAstForGetExtrude, clonedAstForGetExtrude,
@ -95,7 +89,6 @@ export function modifyAstCloneWithFilletAndTag(
) )
if (err(tagResult)) return tagResult if (err(tagResult)) return tagResult
const { tag } = tagResult const { tag } = tagResult
const tagInfo = { tag, selectionType }
// Group tags by their corresponding extrude node // Group tags by their corresponding extrude node
const extrudeKey = JSON.stringify(pathToExtrudeNode) const extrudeKey = JSON.stringify(pathToExtrudeNode)
@ -103,29 +96,23 @@ export function modifyAstCloneWithFilletAndTag(
if (lookupMap.has(extrudeKey)) { if (lookupMap.has(extrudeKey)) {
const existingPath = lookupMap.get(extrudeKey) const existingPath = lookupMap.get(extrudeKey)
if (!existingPath) return new Error('Path to extrude node not found.') if (!existingPath) return new Error('Path to extrude node not found.')
extrudeToTagsMap.get(existingPath)?.push(tagInfo) extrudeToTagsMap.get(existingPath)?.push(tag)
} else { } else {
lookupMap.set(extrudeKey, pathToExtrudeNode) lookupMap.set(extrudeKey, pathToExtrudeNode)
extrudeToTagsMap.set(pathToExtrudeNode, [tagInfo]) extrudeToTagsMap.set(pathToExtrudeNode, [tag])
} }
} }
// Step 2: Apply fillet(s) for each extrude node (body) // Step 2: Apply fillet(s) for each extrude node (body)
let pathToFilletNodes: Array<PathToNode> = [] let pathToFilletNodes: Array<PathToNode> = []
for (const [pathToExtrudeNode, tagInfos] of extrudeToTagsMap.entries()) { for (const [pathToExtrudeNode, tags] of extrudeToTagsMap.entries()) {
// Create a fillet expression with multiple tags // Create a fillet expression with multiple tags
const radiusValue = const radiusValue =
'variableName' in radius ? radius.variableIdentifierAst : radius.valueAst 'variableName' in radius ? radius.variableIdentifierAst : radius.valueAst
const tagCalls = tagInfos.map(({ tag, selectionType }) => {
return getEdgeTagCall(tag, selectionType)
})
const firstTag = tagCalls[0] // can be Identifier or CallExpression (for opposite and adjacent edges)
const filletCall = createCallExpressionStdLib('fillet', [ const filletCall = createCallExpressionStdLib('fillet', [
createObjectExpression({ createObjectExpression({
radius: radiusValue, radius: radiusValue,
tags: createArrayExpression(tagCalls), tags: createArrayExpression(tags.map((tag) => createIdentifier(tag))),
}), }),
createPipeSubstitution(), createPipeSubstitution(),
]) ])
@ -157,7 +144,7 @@ export function modifyAstCloneWithFilletAndTag(
pathToFilletNode = getPathToNodeOfFilletLiteral( pathToFilletNode = getPathToNodeOfFilletLiteral(
pathToExtrudeNode, pathToExtrudeNode,
extrudeDeclarator, extrudeDeclarator,
firstTag tags[0]
) )
pathToFilletNodes.push(pathToFilletNode) pathToFilletNodes.push(pathToFilletNode)
} else if (extrudeDeclarator.init.type === 'PipeExpression') { } else if (extrudeDeclarator.init.type === 'PipeExpression') {
@ -178,7 +165,7 @@ export function modifyAstCloneWithFilletAndTag(
pathToFilletNode = getPathToNodeOfFilletLiteral( pathToFilletNode = getPathToNodeOfFilletLiteral(
pathToExtrudeNode, pathToExtrudeNode,
extrudeDeclarator, extrudeDeclarator,
firstTag tags[0]
) )
pathToFilletNodes.push(pathToFilletNode) pathToFilletNodes.push(pathToFilletNode)
} else { } else {
@ -289,21 +276,6 @@ function mutateAstWithTagForSketchSegment(
return { modifiedAst: astClone, tag } return { modifiedAst: astClone, tag }
} }
function getEdgeTagCall(
tag: string,
selectionType: string
): Identifier | CallExpression {
let tagCall: Expr = createIdentifier(tag)
// Modify the tag based on selectionType
if (selectionType === 'edge') {
tagCall = createCallExpressionStdLib('getOppositeEdge', [tagCall])
} else if (selectionType === 'adjacent-edge') {
tagCall = createCallExpressionStdLib('getNextAdjacentEdge', [tagCall])
}
return tagCall
}
function locateExtrudeDeclarator( function locateExtrudeDeclarator(
node: Program, node: Program,
pathToExtrudeNode: PathToNode pathToExtrudeNode: PathToNode
@ -339,7 +311,7 @@ function locateExtrudeDeclarator(
function getPathToNodeOfFilletLiteral( function getPathToNodeOfFilletLiteral(
pathToExtrudeNode: PathToNode, pathToExtrudeNode: PathToNode,
extrudeDeclarator: VariableDeclarator, extrudeDeclarator: VariableDeclarator,
tag: Identifier | CallExpression tag: string
): PathToNode { ): PathToNode {
let pathToFilletObj: PathToNode = [] let pathToFilletObj: PathToNode = []
let inFillet = false let inFillet = false
@ -375,31 +347,13 @@ function getPathToNodeOfFilletLiteral(
] ]
} }
function hasTag( function hasTag(node: ObjectExpression, tag: string): boolean {
node: ObjectExpression,
tag: Identifier | CallExpression
): boolean {
return node.properties.some((prop) => { return node.properties.some((prop) => {
if (prop.key.name === 'tags' && prop.value.type === 'ArrayExpression') { if (prop.key.name === 'tags' && prop.value.type === 'ArrayExpression') {
// if selection is a base edge:
if (tag.type === 'Identifier') {
return prop.value.elements.some( return prop.value.elements.some(
(element) => (element) => element.type === 'Identifier' && element.name === tag
element.type === 'Identifier' && element.name === tag.name
) )
} }
// if selection is an adjacent or opposite edge:
if (tag.type === 'CallExpression') {
return prop.value.elements.some(
(element) =>
element.type === 'CallExpression' &&
element.callee.name === tag.callee.name && // edge location
element.arguments[0].type === 'Identifier' &&
tag.arguments[0].type === 'Identifier' &&
element.arguments[0].name === tag.arguments[0].name // tag name
)
}
}
return false return false
}) })
} }
@ -429,7 +383,7 @@ export const hasValidFilletSelection = ({
ast: Program ast: Program
code: string code: string
}) => { }) => {
// check if there is anything filletable in the scene // case 0: check if there is anything filletable in the scene
let extrudeExists = false let extrudeExists = false
traverse(ast, { traverse(ast, {
enter(node) { enter(node) {
@ -440,88 +394,65 @@ export const hasValidFilletSelection = ({
}) })
if (!extrudeExists) return false if (!extrudeExists) return false
// check if nothing is selected // case 1: nothing selected, test whether the extrusion exists
if (selectionRanges) {
if (selectionRanges.codeBasedSelections.length === 0) { if (selectionRanges.codeBasedSelections.length === 0) {
return true return true
} }
const range0 = selectionRanges.codeBasedSelections[0].range[0]
// check if selection is last string in code const codeLength = code.length
if (selectionRanges.codeBasedSelections[0].range[0] === code.length) { if (range0 === codeLength) {
return true return true
} }
}
// selection exists: // case 2: sketch segment selected, test whether it is extruded
for (const selection of selectionRanges.codeBasedSelections) { // TODO: add loft / sweep check
// check if all selections are in sketchLineHelperMap if (selectionRanges.codeBasedSelections.length > 0) {
const path = getNodePathFromSourceRange(ast, selection.range) const isExtruded = hasSketchPipeBeenExtruded(
selectionRanges.codeBasedSelections[0],
ast
)
if (isExtruded) {
const pathToSelectedNode = getNodePathFromSourceRange(
ast,
selectionRanges.codeBasedSelections[0].range
)
const segmentNode = getNodeFromPath<CallExpression>( const segmentNode = getNodeFromPath<CallExpression>(
ast, ast,
path, pathToSelectedNode,
'CallExpression' 'CallExpression'
) )
if (err(segmentNode)) return false if (err(segmentNode)) return false
if (segmentNode.node.type !== 'CallExpression') { if (segmentNode.node.type === 'CallExpression') {
return false const segmentName = segmentNode.node.callee.name
} if (segmentName in sketchLineHelperMap) {
if (!(segmentNode.node.callee.name in sketchLineHelperMap)) { // Add check whether the tag exists at all:
return false if (!(segmentNode.node.arguments.length === 3)) return true
} // If the tag exists, check if it is already filleted
const edges = isTagUsedInFillet({
// check if selection is extruded ast,
// TODO: option 1 : extrude is in the sketch pipe callExp: segmentNode.node,
// option 2: extrude is outside the sketch pipe
const extrudeExists = hasSketchPipeBeenExtruded(selection, ast)
if (err(extrudeExists)) {
return false
}
if (!extrudeExists) {
return false
}
// check if tag exists for the selection
let tagExists = false
let tag = ''
traverse(segmentNode.node, {
enter(node) {
if (node.type === 'TagDeclarator') {
tagExists = true
tag = node.value
}
},
}) })
// edge has already been filleted
// check if tag is used in fillet if (
if (tagExists) { ['edge', 'default'].includes(
// create tag call selectionRanges.codeBasedSelections[0].type
let tagCall: Expr = getEdgeTagCall(tag, selection.type) ) &&
edges.includes('baseEdge')
// check if tag is used in fillet )
let inFillet = false
let tagUsedInFillet = false
traverse(ast, {
enter(node) {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
inFillet = true
}
if (inFillet && node.type === 'ObjectExpression') {
if (hasTag(node, tagCall)) {
tagUsedInFillet = true
}
}
},
leave(node) {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
inFillet = false
}
},
})
if (tagUsedInFillet) {
return false return false
}
}
}
return true return true
} else {
return false
}
}
} else {
return false
}
}
return canFilletSelection(selectionRanges)
} }
type EdgeTypes = type EdgeTypes =

View File

@ -28,7 +28,6 @@ import {
getConstraintType, getConstraintType,
} from './std/sketchcombos' } from './std/sketchcombos'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { ImportStatement } from 'wasm-lib/kcl/bindings/ImportStatement'
/** /**
* Retrieves a node from a given path within a Program node structure, optionally stopping at a specified node type. * Retrieves a node from a given path within a Program node structure, optionally stopping at a specified node type.
@ -121,12 +120,7 @@ export function getNodeFromPathCurry(
} }
function moreNodePathFromSourceRange( function moreNodePathFromSourceRange(
node: node: Expr | ExpressionStatement | VariableDeclaration | ReturnStatement,
| Expr
| ImportStatement
| ExpressionStatement
| VariableDeclaration
| ReturnStatement,
sourceRange: Selection['range'], sourceRange: Selection['range'],
previousPath: PathToNode = [['body', '']] previousPath: PathToNode = [['body', '']]
): PathToNode { ): PathToNode {
@ -717,7 +711,7 @@ export function isLinesParallelAndConstrained(
constraintType === 'angle' || constraintLevel === 'full' constraintType === 'angle' || constraintLevel === 'full'
// get the previous segment // get the previous segment
const prevSegment = sg.paths[secondaryIndex - 1] const prevSegment = sg.value[secondaryIndex - 1]
const prevSourceRange = prevSegment.__geoMeta.sourceRange const prevSourceRange = prevSegment.__geoMeta.sourceRange
const isParallelAndConstrained = const isParallelAndConstrained =

Binary file not shown.

Before

Width:  |  Height:  |  Size: 378 KiB

After

Width:  |  Height:  |  Size: 357 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 613 KiB

After

Width:  |  Height:  |  Size: 577 KiB

View File

@ -50,11 +50,6 @@ export enum ExportIntent {
Make = 'make', Make = 'make',
} }
export interface ExportInfo {
intent: ExportIntent
name: string
}
type ClientMetrics = Models['ClientMetrics_type'] type ClientMetrics = Models['ClientMetrics_type']
interface WebRTCClientMetrics extends ClientMetrics { interface WebRTCClientMetrics extends ClientMetrics {
@ -1359,7 +1354,7 @@ export class EngineCommandManager extends EventTarget {
* export in progress. Otherwise it is an enum value of the intent. * export in progress. Otherwise it is an enum value of the intent.
* Another export cannot be started if one is already in progress. * Another export cannot be started if one is already in progress.
*/ */
private _exportInfo: ExportInfo | null = null private _exportIntent: ExportIntent | null = null
_commandLogCallBack: (command: CommandLog[]) => void = () => {} _commandLogCallBack: (command: CommandLog[]) => void = () => {}
subscriptions: { subscriptions: {
@ -1415,12 +1410,12 @@ export class EngineCommandManager extends EventTarget {
(() => {}) as any (() => {}) as any
kclManager: null | KclManager = null kclManager: null | KclManager = null
set exportInfo(info: ExportInfo | null) { set exportIntent(intent: ExportIntent | null) {
this._exportInfo = info this._exportIntent = intent
} }
get exportInfo() { get exportIntent() {
return this._exportInfo return this._exportIntent
} }
start({ start({
@ -1612,7 +1607,7 @@ export class EngineCommandManager extends EventTarget {
// because in all other cases we send JSON strings. But in the case of // because in all other cases we send JSON strings. But in the case of
// export we send a binary blob. // export we send a binary blob.
// Pass this to our export function. // Pass this to our export function.
if (this.exportInfo === null || this.pendingExport === undefined) { if (this.exportIntent === null || this.pendingExport === undefined) {
toast.error( toast.error(
'Export intent was not set, but export data was received' 'Export intent was not set, but export data was received'
) )
@ -1622,7 +1617,7 @@ export class EngineCommandManager extends EventTarget {
return return
} }
switch (this.exportInfo.intent) { switch (this.exportIntent) {
case ExportIntent.Save: { case ExportIntent.Save: {
exportSave(event.data, this.pendingExport.toastId).then(() => { exportSave(event.data, this.pendingExport.toastId).then(() => {
this.pendingExport?.resolve(null) this.pendingExport?.resolve(null)
@ -1630,22 +1625,21 @@ export class EngineCommandManager extends EventTarget {
break break
} }
case ExportIntent.Make: { case ExportIntent.Make: {
exportMake( exportMake(event.data, this.pendingExport.toastId).then(
event.data, (result) => {
this.exportInfo.name,
this.pendingExport.toastId
).then((result) => {
if (result) { if (result) {
this.pendingExport?.resolve(null) this.pendingExport?.resolve(null)
} else { } else {
this.pendingExport?.reject('Failed to make export') this.pendingExport?.reject('Failed to make export')
} }
}, this.pendingExport?.reject) },
this.pendingExport?.reject
)
break break
} }
} }
// Set the export intent back to null. // Set the export intent back to null.
this.exportInfo = null this.exportIntent = null
return return
} }
@ -1959,15 +1953,15 @@ export class EngineCommandManager extends EventTarget {
return Promise.resolve(null) return Promise.resolve(null)
} else if (cmd.type === 'export') { } else if (cmd.type === 'export') {
const promise = new Promise<null>((resolve, reject) => { const promise = new Promise<null>((resolve, reject) => {
if (this.exportInfo === null) { if (this.exportIntent === null) {
if (this.exportInfo === null) { if (this.exportIntent === null) {
toast.error('Export intent was not set, but export is being sent') toast.error('Export intent was not set, but export is being sent')
console.error('Export intent was not set, but export is being sent') console.error('Export intent was not set, but export is being sent')
return return
} }
} }
const toastId = toast.loading( const toastId = toast.loading(
this.exportInfo.intent === ExportIntent.Save this.exportIntent === ExportIntent.Save
? EXPORT_TOAST_MESSAGES.START ? EXPORT_TOAST_MESSAGES.START
: MAKE_TOAST_MESSAGES.START : MAKE_TOAST_MESSAGES.START
) )
@ -1981,7 +1975,7 @@ export class EngineCommandManager extends EventTarget {
resolve(passThrough) resolve(passThrough)
}, },
reject: (reason: string) => { reject: (reason: string) => {
this.exportInfo = null this.exportIntent = null
reject(reason) reject(reason)
}, },
commandId: command.cmd_id, commandId: command.cmd_id,

View File

@ -18,7 +18,7 @@ class FileSystemManager {
return Promise.resolve(window.electron.path.join(dir, path)) return Promise.resolve(window.electron.path.join(dir, path))
} }
async readFile(path: string): Promise<Uint8Array> { async readFile(path: string): Promise<Uint8Array | void> {
// Using local file system only works from desktop. // Using local file system only works from desktop.
if (!isDesktop()) { if (!isDesktop()) {
return Promise.reject( return Promise.reject(
@ -28,8 +28,9 @@ class FileSystemManager {
) )
} }
return this.join(this.dir, path).then((filePath) => { return this.join(this.dir, path).then(async (filePath) => {
return window.electron.readFile(filePath) const content = await window.electron.readFile(filePath)
return content
}) })
} }

View File

@ -64,7 +64,7 @@ const ARC_SEGMENT_ERR = new Error('Invalid input, expected "arc-segment"')
export type Coords2d = [number, number] export type Coords2d = [number, number]
export function getCoordsFromPaths(skGroup: Sketch, index = 0): Coords2d { export function getCoordsFromPaths(skGroup: Sketch, index = 0): Coords2d {
const currentPath = skGroup?.paths?.[index] const currentPath = skGroup?.value?.[index]
if (!currentPath && skGroup?.start) { if (!currentPath && skGroup?.start) {
return skGroup.start.to return skGroup.start.to
} else if (!currentPath) { } else if (!currentPath) {
@ -1704,7 +1704,7 @@ export const angledLineThatIntersects: SketchLineHelper = {
varName varName
) )
if (err(sketch)) return sketch if (err(sketch)) return sketch
const intersectPath = sketch.paths.find( const intersectPath = sketch.value.find(
({ tag }: Path) => tag && tag.value === intersectTagName ({ tag }: Path) => tag && tag.value === intersectTagName
) )
let offset = 0 let offset = 0
@ -1823,10 +1823,11 @@ export const updateStartProfileAtArgs: SketchLineHelper['updateArgs'] = ({
start: 0, start: 0,
end: 0, end: 0,
body: [], body: [],
digest: null,
nonCodeMeta: { nonCodeMeta: {
start: [], start: [],
nonCodeNodes: [], nonCodeNodes: [],
digest: null,
}, },
}, },
pathToNode, pathToNode,

View File

@ -18,7 +18,7 @@ export function getSketchSegmentFromPathToNode(
pathToNode: PathToNode pathToNode: PathToNode
): ):
| { | {
segment: Sketch['paths'][number] segment: Sketch['value'][number]
index: number index: number
} }
| Error { | Error {
@ -39,15 +39,15 @@ export function getSketchSegmentFromSourceRange(
[rangeStart, rangeEnd]: SourceRange [rangeStart, rangeEnd]: SourceRange
): ):
| { | {
segment: Sketch['paths'][number] segment: Sketch['value'][number]
index: number index: number
} }
| Error { | Error {
const lineIndex = sketch.paths.findIndex( const lineIndex = sketch.value.findIndex(
({ __geoMeta: { sourceRange } }: Path) => ({ __geoMeta: { sourceRange } }: Path) =>
sourceRange[0] <= rangeStart && sourceRange[1] >= rangeEnd sourceRange[0] <= rangeStart && sourceRange[1] >= rangeEnd
) )
const line = sketch.paths[lineIndex] const line = sketch.value[lineIndex]
if (line) { if (line) {
return { return {
segment: line, segment: line,

View File

@ -1732,7 +1732,7 @@ export function transformAstSketchLines({
if (err(_segment)) return _segment if (err(_segment)) return _segment
referencedSegment = _segment.segment referencedSegment = _segment.segment
} else { } else {
referencedSegment = sketch.paths.find( referencedSegment = sketch.value.find(
(path) => path.tag?.value === _referencedSegmentName (path) => path.tag?.value === _referencedSegmentName
) )
} }

View File

@ -426,7 +426,6 @@ export const _executor = async (
baseUnit, baseUnit,
engineCommandManager, engineCommandManager,
fileSystemManager, fileSystemManager,
undefined,
isMock isMock
) )
return execStateFromRaw(execState) return execStateFromRaw(execState)

View File

@ -190,31 +190,10 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
options: () => { options: () => {
return Object.entries(machineManager.machines).map( return Object.entries(machineManager.machines).map(
([_, machine]) => ({ ([_, machine]) => ({
name: name: `${machine.id} (${
`${machine.id} (${
machine.make_model.model || machine.make_model.manufacturer machine.make_model.model || machine.make_model.manufacturer
}) (${machine.state.state})` + }) via ${machineManager.machineApiIp || 'the local network'}`,
(machine.hardware_configuration &&
machine.hardware_configuration.type !== 'none' &&
machine.hardware_configuration.config.nozzle_diameter
? ` - Nozzle Diameter: ${machine.hardware_configuration.config.nozzle_diameter}`
: '') +
(machine.hardware_configuration &&
machine.hardware_configuration.type !== 'none' &&
machine.hardware_configuration.config.filaments &&
machine.hardware_configuration.config.filaments[0]
? ` - ${
machine.hardware_configuration.config.filaments[0].name
} #${
machine.hardware_configuration.config &&
machine.hardware_configuration.config.filaments[0].color?.slice(
0,
6
)
}`
: ''),
isCurrent: false, isCurrent: false,
disabled: machine.state.state !== 'idle',
value: machine as components['schemas']['MachineInfoResponse'], value: machine as components['schemas']['MachineInfoResponse'],
}) })
) )

View File

@ -258,6 +258,5 @@ export type CommandArgumentWithName<
export type CommandArgumentOption<A> = { export type CommandArgumentOption<A> = {
name: string name: string
isCurrent?: boolean isCurrent?: boolean
disabled?: boolean
value: A value: A
} }

View File

@ -92,7 +92,6 @@ export const MAKE_TOAST_MESSAGES = {
NO_MACHINE_API_IP: 'No machine api ip available', NO_MACHINE_API_IP: 'No machine api ip available',
NO_CURRENT_MACHINE: 'No current machine available', NO_CURRENT_MACHINE: 'No current machine available',
NO_MACHINE_ID: 'No machine id available', NO_MACHINE_ID: 'No machine id available',
NO_NAME: 'No name provided',
ERROR_STARTING_PRINT: 'Error while starting print', ERROR_STARTING_PRINT: 'Error while starting print',
SUCCESS: 'Started print successfully', SUCCESS: 'Started print successfully',
} }

View File

@ -448,9 +448,7 @@ export const readProjectSettingsFile = async (
} }
} }
const configToml = await window.electron.readFile(settingsPath, { const configToml = await window.electron.readFile(settingsPath, 'utf-8')
encoding: 'utf-8',
})
const configObj = parseProjectSettings(configToml) const configObj = parseProjectSettings(configToml)
if (err(configObj)) { if (err(configObj)) {
return Promise.reject(configObj) return Promise.reject(configObj)
@ -469,9 +467,7 @@ export const readAppSettingsFile = async () => {
// The file exists, read it and parse it. // The file exists, read it and parse it.
if (window.electron.exists(settingsPath)) { if (window.electron.exists(settingsPath)) {
const configToml = await window.electron.readFile(settingsPath, { const configToml = await window.electron.readFile(settingsPath, 'utf-8')
encoding: 'utf-8',
})
const parsedAppConfig = parseAppSettings(configToml) const parsedAppConfig = parseAppSettings(configToml)
if (err(parsedAppConfig)) { if (err(parsedAppConfig)) {
return Promise.reject(parsedAppConfig) return Promise.reject(parsedAppConfig)
@ -531,9 +527,7 @@ export const readTokenFile = async () => {
let settingsPath = await getTokenFilePath() let settingsPath = await getTokenFilePath()
if (window.electron.exists(settingsPath)) { if (window.electron.exists(settingsPath)) {
const token: string = await window.electron.readFile(settingsPath, { const token: string = await window.electron.readFile(settingsPath, 'utf-8')
encoding: 'utf-8',
})
if (!token) return '' if (!token) return ''
return token return token

View File

@ -8,15 +8,8 @@ import { MAKE_TOAST_MESSAGES } from './constants'
// Make files locally from an export call. // Make files locally from an export call.
export async function exportMake( export async function exportMake(
data: ArrayBuffer, data: ArrayBuffer,
name: string,
toastId: string toastId: string
): Promise<Response | null> { ): Promise<Response | null> {
if (name === '') {
console.error(MAKE_TOAST_MESSAGES.NO_NAME)
toast.error(MAKE_TOAST_MESSAGES.NO_NAME, { id: toastId })
return null
}
if (machineManager.machineCount() === 0) { if (machineManager.machineCount() === 0) {
console.error(MAKE_TOAST_MESSAGES.NO_MACHINES) console.error(MAKE_TOAST_MESSAGES.NO_MACHINES)
toast.error(MAKE_TOAST_MESSAGES.NO_MACHINES, { id: toastId }) toast.error(MAKE_TOAST_MESSAGES.NO_MACHINES, { id: toastId })
@ -46,7 +39,7 @@ export async function exportMake(
const params: components['schemas']['PrintParameters'] = { const params: components['schemas']['PrintParameters'] = {
machine_id: machineId, machine_id: machineId,
job_name: name, job_name: 'Exported Job', // TODO: make this the project name.
} }
try { try {
console.log('params', params) console.log('params', params)

View File

@ -55,23 +55,6 @@ export interface paths {
patch?: never patch?: never
trace?: never trace?: never
} }
'/metrics': {
parameters: {
query?: never
header?: never
path?: never
cookie?: never
}
/** List available machines and their statuses */
get: operations['get_metrics']
put?: never
post?: never
delete?: never
options?: never
head?: never
patch?: never
trace?: never
}
'/ping': { '/ping': {
parameters: { parameters: {
query?: never query?: never
@ -119,105 +102,18 @@ export interface components {
/** @description Extra machine-specific information regarding a connected machine. */ /** @description Extra machine-specific information regarding a connected machine. */
ExtraMachineInfoResponse: ExtraMachineInfoResponse:
| { | {
/** @enum {string} */ Moonraker: Record<string, never>
type: 'moonraker'
} }
| { | {
/** @enum {string} */ Usb: Record<string, never>
type: 'usb'
} }
| { | {
/** @description The current stage of the machine as defined by Bambu which can include errors, etc. */ Bambu: Record<string, never>
current_stage?: components['schemas']['Stage'] | null
/** @description The nozzle diameter of the machine. */
nozzle_diameter: components['schemas']['NozzleDiameter']
/** @enum {string} */
type: 'bambu'
}
/** @description Configuration for a FDM-based printer. */
FdmHardwareConfiguration: {
/** @description The filaments the printer has access to. */
filaments: components['schemas']['Filament'][]
/**
* Format: uint
* @description The currently loaded filament index.
*/
loaded_filament_idx?: number | null
/**
* Format: double
* @description Diameter of the extrusion nozzle, in mm.
*/
nozzle_diameter: number
}
/** @description Information about the filament being used in a FDM printer. */
Filament: {
/** @description The color (as hex without the `#`) of the filament, this is likely specific to the manufacturer. */
color?: string | null
/** @description The material that the filament is made of. */
material: components['schemas']['FilamentMaterial']
/** @description The name of the filament, this is likely specfic to the manufacturer. */
name?: string | null
}
/** @description The material that the filament is made of. */
FilamentMaterial:
| {
/** @enum {string} */
type: 'pla'
}
| {
/** @enum {string} */
type: 'pla_support'
}
| {
/** @enum {string} */
type: 'abs'
}
| {
/** @enum {string} */
type: 'petg'
}
| {
/** @enum {string} */
type: 'nylon'
}
| {
/** @enum {string} */
type: 'tpu'
}
| {
/** @enum {string} */
type: 'pva'
}
| {
/** @enum {string} */
type: 'hips'
}
| {
/** @enum {string} */
type: 'composite'
}
| {
/** @enum {string} */
type: 'unknown'
}
/** @description The hardware configuration of a machine. */
HardwareConfiguration:
| {
/** @enum {string} */
type: 'none'
}
| {
/** @description The configuration for the FDM printer. */
config: components['schemas']['FdmHardwareConfiguration']
/** @enum {string} */
type: 'fdm'
} }
/** @description Information regarding a connected machine. */ /** @description Information regarding a connected machine. */
MachineInfoResponse: { MachineInfoResponse: {
/** @description Additional, per-machine information which is specific to the underlying machine type. */ /** @description Additional, per-machine information which is specific to the underlying machine type. */
extra?: components['schemas']['ExtraMachineInfoResponse'] | null extra?: components['schemas']['ExtraMachineInfoResponse'] | null
/** @description Information about how the Machine is currently configured. */
hardware_configuration: components['schemas']['HardwareConfiguration']
/** @description Machine Identifier (ID) for the specific Machine. */ /** @description Machine Identifier (ID) for the specific Machine. */
id: string id: string
/** @description Information regarding the method of manufacture. */ /** @description Information regarding the method of manufacture. */
@ -230,11 +126,6 @@ export interface components {
* *
* What "close" means is up to you! */ * What "close" means is up to you! */
max_part_volume?: components['schemas']['Volume'] | null max_part_volume?: components['schemas']['Volume'] | null
/**
* Format: double
* @description Progress of the current print, if printing.
*/
progress?: number | null
/** @description Status of the printer -- be it printing, idle, or unreachable. This may dictate if a machine is capable of taking a new job. */ /** @description Status of the printer -- be it printing, idle, or unreachable. This may dictate if a machine is capable of taking a new job. */
state: components['schemas']['MachineState'] state: components['schemas']['MachineState']
} }
@ -249,40 +140,17 @@ export interface components {
} }
/** @description Current state of the machine -- be it printing, idle or offline. This can be used to determine if a printer is in the correct state to take a new job. */ /** @description Current state of the machine -- be it printing, idle or offline. This can be used to determine if a printer is in the correct state to take a new job. */
MachineState: MachineState:
| 'Unknown'
| 'Idle'
| 'Running'
| 'Offline'
| 'Paused'
| 'Complete'
| { | {
/** @enum {string} */ Failed: string | null
state: 'unknown'
}
| {
/** @enum {string} */
state: 'idle'
}
| {
/** @enum {string} */
state: 'running'
}
| {
/** @enum {string} */
state: 'offline'
}
| {
/** @enum {string} */
state: 'paused'
}
| {
/** @enum {string} */
state: 'complete'
}
| {
/** @description A human-readable message describing the failure. */
message?: string | null
/** @enum {string} */
state: 'failed'
} }
/** @description Specific technique by which this Machine takes a design, and produces a real-world 3D object. */ /** @description Specific technique by which this Machine takes a design, and produces a real-world 3D object. */
MachineType: 'stereolithography' | 'fused_deposition' | 'cnc' MachineType: 'Stereolithography' | 'FusedDeposition' | 'Cnc'
/** @description A nozzle diameter. */
NozzleDiameter: '0.2' | '0.4' | '0.6' | '0.8'
/** @description The response from the `/ping` endpoint. */ /** @description The response from the `/ping` endpoint. */
Pong: { Pong: {
/** @description The pong response. */ /** @description The pong response. */
@ -301,56 +169,7 @@ export interface components {
job_name: string job_name: string
/** @description The machine id to print to. */ /** @description The machine id to print to. */
machine_id: string machine_id: string
/** @description Requested design-specific slicer configurations. */
slicer_configuration?: components['schemas']['SlicerConfiguration'] | null
} }
/** @description The slicer configuration is a set of parameters that are passed to the slicer to control how the gcode is generated. */
SlicerConfiguration: {
/**
* Format: uint
* @description The filament to use for the print.
*/
filament_idx?: number | null
}
/** @description The print stage. These come from: https://github.com/SoftFever/OrcaSlicer/blob/431978baf17961df90f0d01871b0ad1d839d7f5d/src/slic3r/GUI/DeviceManager.cpp#L78 */
Stage:
| 'nothing'
| 'empty'
| 'auto_bed_leveling'
| 'heatbed_preheating'
| 'sweeping_xy_mech_mode'
| 'changing_filament'
| 'm400_pause'
| 'paused_due_to_filament_runout'
| 'heating_hotend'
| 'calibrating_extrusion'
| 'scanning_bed_surface'
| 'inspecting_first_layer'
| 'identifying_build_plate_type'
| 'calibrating_micro_lidar'
| 'homing_toolhead'
| 'cleaning_nozzle_tip'
| 'checking_extruder_temperature'
| 'printing_was_paused_by_the_user'
| 'pause_of_front_cover_falling'
| 'calibrating_micro_lidar2'
| 'calibrating_extrusion_flow'
| 'paused_due_to_nozzle_temperature_malfunction'
| 'paused_due_to_heat_bed_temperature_malfunction'
| 'filament_unloading'
| 'skip_step_pause'
| 'filament_loading'
| 'motor_noise_calibration'
| 'paused_due_to_ams_lost'
| 'paused_due_to_low_speed_of_the_heat_break_fan'
| 'paused_due_to_chamber_temperature_control_error'
| 'cooling_chamber'
| 'paused_by_the_gcode_inserted_by_the_user'
| 'motor_noise_showoff'
| 'nozzle_filament_covered_detected_pause'
| 'cutter_error_pause'
| 'first_layer_error_pause'
| 'nozzle_clog_pause'
/** @description Set of three values to represent the extent of a 3-D Volume. This contains the width, depth, and height values, generally used to represent some maximum or minimum. /** @description Set of three values to represent the extent of a 3-D Volume. This contains the width, depth, and height values, generally used to represent some maximum or minimum.
* *
* All measurements are in millimeters. */ * All measurements are in millimeters. */
@ -459,28 +278,6 @@ export interface operations {
'5XX': components['responses']['Error'] '5XX': components['responses']['Error']
} }
} }
get_metrics: {
parameters: {
query?: never
header?: never
path?: never
cookie?: never
}
requestBody?: never
responses: {
/** @description successful operation */
200: {
headers: {
[name: string]: unknown
}
content: {
'application/json': string
}
}
'4XX': components['responses']['Error']
'5XX': components['responses']['Error']
}
}
ping: { ping: {
parameters: { parameters: {
query?: never query?: never

View File

@ -85,11 +85,7 @@ export class MachineManager {
return return
} }
if (this._machineApiIp === null) { this._machines = await window.electron.listMachines()
return
}
this._machines = await window.electron.listMachines(this._machineApiIp)
} }
private async updateMachineApiIp(): Promise<void> { private async updateMachineApiIp(): Promise<void> {

View File

@ -109,9 +109,7 @@ export const fileLoader: LoaderFunction = async (
) )
} }
code = await window.electron.readFile(currentFilePath, { code = await window.electron.readFile(currentFilePath, 'utf-8')
encoding: 'utf-8',
})
code = normalizeLineEndings(code) code = normalizeLineEndings(code)
// Update both the state and the editor's code. // Update both the state and the editor's code.

View File

@ -236,7 +236,6 @@ ipcMain.handle('find_machine_api', () => {
const ip = service.addresses[0] const ip = service.addresses[0]
const port = service.port const port = service.port
// We want to return the ip address of the machine API. // We want to return the ip address of the machine API.
console.log(`Machine API found at ${ip}:${port}`)
resolve(`${ip}:${port}`) resolve(`${ip}:${port}`)
} }
) )
@ -286,10 +285,7 @@ app.on('ready', () => {
autoUpdater.on('update-downloaded', (info) => { autoUpdater.on('update-downloaded', (info) => {
console.log('update-downloaded', info) console.log('update-downloaded', info)
mainWindow?.webContents.send('update-downloaded', { mainWindow?.webContents.send('update-downloaded', info.version)
version: info.version,
releaseNotes: info.releaseNotes,
})
}) })
ipcMain.handle('app.restart', () => { ipcMain.handle('app.restart', () => {

View File

@ -16,12 +16,11 @@ const startDeviceFlow = (host: string): Promise<string> =>
ipcRenderer.invoke('startDeviceFlow', host) ipcRenderer.invoke('startDeviceFlow', host)
const loginWithDeviceFlow = (): Promise<string> => const loginWithDeviceFlow = (): Promise<string> =>
ipcRenderer.invoke('loginWithDeviceFlow') ipcRenderer.invoke('loginWithDeviceFlow')
const onUpdateDownloaded = (
callback: (value: { version: string; releaseNotes: string }) => void
) => ipcRenderer.on('update-downloaded', (_event, value) => callback(value))
const onUpdateDownloadStart = ( const onUpdateDownloadStart = (
callback: (value: { version: string }) => void callback: (value: { version: string }) => void
) => ipcRenderer.on('update-download-start', (_event, value) => callback(value)) ) => ipcRenderer.on('update-download-start', (_event, value) => callback(value))
const onUpdateDownloaded = (callback: (value: string) => void) =>
ipcRenderer.on('update-downloaded', (_event, value) => callback(value))
const onUpdateError = (callback: (value: Error) => void) => const onUpdateError = (callback: (value: Error) => void) =>
ipcRenderer.on('update-error', (_event, value) => callback(value)) ipcRenderer.on('update-error', (_event, value) => callback(value))
const appRestart = () => ipcRenderer.invoke('app.restart') const appRestart = () => ipcRenderer.invoke('app.restart')
@ -74,7 +73,7 @@ const watchFileOff = (path: string, key: string) => {
fsWatchListeners.set(path, watchers) fsWatchListeners.set(path, watchers)
} }
} }
const readFile = fs.readFile const readFile = (path: string, as?: string) => fs.readFile(path, as)
// It seems like from the node source code this does not actually block but also // It seems like from the node source code this does not actually block but also
// don't trust me on that (jess). // don't trust me on that (jess).
const exists = (path: string) => fsSync.existsSync(path) const exists = (path: string) => fsSync.existsSync(path)
@ -106,12 +105,11 @@ const kittycad = (access: string, args: any) =>
// We could probably do this from the renderer side, but I fear CORS will // We could probably do this from the renderer side, but I fear CORS will
// bite our butts. // bite our butts.
const listMachines = async ( const listMachines = async (): Promise<MachinesListing> => {
machineApiAddr: string const machineApi = await ipcRenderer.invoke('find_machine_api')
): Promise<MachinesListing> => { if (!machineApi) return []
return fetch(`http://${machineApiAddr}/machines`).then((resp) => {
return resp.json() return fetch(`http://${machineApi}/machines`).then((resp) => resp.json())
})
} }
const getMachineApiIp = async (): Promise<String | null> => const getMachineApiIp = async (): Promise<String | null> =>

View File

@ -23,9 +23,6 @@ import { codeManager, editorManager, kclManager } from 'lib/singletons'
import { bracket } from 'lib/exampleKcl' import { bracket } from 'lib/exampleKcl'
import { toSync } from 'lib/utils' import { toSync } from 'lib/utils'
import { reportRejection } from 'lib/trap' import { reportRejection } from 'lib/trap'
import { useNetworkContext } from 'hooks/useNetworkContext'
import { NetworkHealthState } from 'hooks/useNetworkStatus'
import { EngineConnectionStateType } from 'lang/std/engineConnection'
export const kbdClasses = export const kbdClasses =
'py-0.5 px-1 text-sm rounded bg-chalkboard-10 dark:bg-chalkboard-100 border border-chalkboard-50 border-b-2' 'py-0.5 px-1 text-sm rounded bg-chalkboard-10 dark:bg-chalkboard-100 border border-chalkboard-50 border-b-2'
@ -83,20 +80,8 @@ export const onboardingRoutes = [
] ]
export function useDemoCode() { export function useDemoCode() {
const { overallState, immediateState } = useNetworkContext()
useEffect(() => { useEffect(() => {
// Don't run if the editor isn't loaded or the code is already the bracket if (!editorManager.editorView || codeManager.code === bracket) return
if (!editorManager.editorView || codeManager.code === bracket) {
return
}
// Don't run if the network isn't healthy or the connection isn't established
if (
overallState !== NetworkHealthState.Ok ||
immediateState.type !== EngineConnectionStateType.ConnectionEstablished
) {
return
}
setTimeout( setTimeout(
toSync(async () => { toSync(async () => {
codeManager.updateCodeStateEditor(bracket) codeManager.updateCodeStateEditor(bracket)
@ -104,7 +89,7 @@ export function useDemoCode() {
await codeManager.writeToFile() await codeManager.writeToFile()
}, reportRejection) }, reportRejection)
) )
}, [editorManager.editorView, immediateState, overallState]) }, [editorManager.editorView])
} }
export function useNextClick(newStatus: string) { export function useNextClick(newStatus: string) {

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