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
284 changed files with 12985 additions and 32823 deletions

View File

@ -1,3 +1,3 @@
[codespell]
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:
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')) }}
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Non-release build, commit {0}', github.sha) }}
concurrency:
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)
outputs:
version: ${{ steps.export_version.outputs.version }}
notes: ${{ steps.export_version.outputs.notes }}
steps:
- 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
- name: Generate release notes
env:
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Non-release build, commit {0}', github.sha) }}
run: |
echo "$NOTES" > release-notes.md
cat release-notes.md
@ -70,17 +72,8 @@ jobs:
- id: export_version
run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
- name: Prepare electron-builder.yml file for nightly
if: ${{ github.event_name == 'schedule' }}
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
- id: export_notes
run: echo "notes=`cat release-notes.md'`" >> "$GITHUB_OUTPUT"
- name: Prepare electron-builder.yml file for updater test
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
- uses: actions/upload-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' }}
with:
name: prepared-files-updater-test
path: |
@ -100,13 +92,7 @@ jobs:
strategy:
fail-fast: false
matrix:
include:
- os: macos-14
platform: mac
- os: windows-2022
platform: win
- os: ubuntu-22.04
platform: linux
os: [macos-14, windows-2022, ubuntu-22.04]
runs-on: ${{ matrix.os }}
env:
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/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
uses: actions/setup-node@v4
with:
@ -189,27 +165,9 @@ jobs:
- uses: actions/upload-artifact@v3
with:
name: out-arm64-${{ matrix.platform }}
# 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
name: out-${{ matrix.os }}
path: |
out/Zoo*.*
out/latest*.yml
# TODO: add the 'Build for Mac TestFlight (nightly)' stage back
@ -231,20 +189,10 @@ jobs:
- uses: actions/upload-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' }}
with:
name: updater-test-arm64-${{ matrix.platform }}
name: updater-test-${{ matrix.os }}
path: |
out/*-arm64-win.exe
out/*-arm64-mac.dmg
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
out/Zoo*.*
out/latest*.yml
publish-apps-release:
@ -257,6 +205,7 @@ jobs:
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) }}
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' }}
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' }}
@ -265,37 +214,17 @@ jobs:
- uses: actions/download-artifact@v3
with:
name: out-arm64-win
name: out-windows-2022
path: out
- uses: actions/download-artifact@v3
with:
name: out-x64-win
name: out-macos-14
path: out
- uses: actions/download-artifact@v3
with:
name: out-arm64-mac
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
name: out-ubuntu-22.04
path: out
- name: Generate the download static endpoint

View File

@ -62,7 +62,7 @@ jobs:
shell: bash
run: |-
cd "${{ matrix.dir }}"
cargo llvm-cov nextest --workspace --lcov --output-path lcov.info --test-threads=1 --no-fail-fast -P ci 2>&1 | tee /tmp/github-actions.log
cargo llvm-cov nextest --all --lcov --output-path lcov.info --test-threads=1 --no-fail-fast -P ci 2>&1 | tee /tmp/github-actions.log
env:
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}
RUST_MIN_STACK: 10485760000

View File

@ -37,6 +37,10 @@ jobs:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- run: yarn build:wasm
yarn-tsc:
@ -66,6 +70,10 @@ jobs:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- run: yarn lint
python-codespell:
@ -93,6 +101,11 @@ jobs:
cache: 'yarn'
- run: yarn install
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- run: yarn build:wasm
- 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
##### Release builds
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.
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.
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 `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
```
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).
#### 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(%)
|> patternCircular2d({
center: [0, 0],
instances: 13,
repetitions: 12,
arcDegrees: 360,
rotateDuplicates: true
}, %)

View File

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

View File

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

View File

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

View File

@ -32,7 +32,7 @@ reduce(array: [KclValue], start: KclValue, reduce_fn: FunctionParam) -> KclValue
fn decagon = (radius) => {
step = 1 / 10 * tau()
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
y = sin(step * i) * radius
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
| Property | Type | Description | Required |
@ -66,7 +45,6 @@ layout: manual
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `declarations` |`[` [`VariableDeclarator`](/docs/kcl/types/VariableDeclarator) `]`| | No |
| `visibility` |[`ItemVisibility`](/docs/kcl/types/ItemVisibility)| | 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 |

View File

@ -16,7 +16,7 @@ Data for a circular pattern on a 2D sketch.
| 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 |
| `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 |

View File

@ -16,7 +16,7 @@ Data for a circular pattern on a 3D model.
| 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 |
| `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 |

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
| 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 |
|----------|------|-------------|----------|
| `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 |
| `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 |
|----------|------|-------------|----------|
| `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 |
| `axis` |`[number, number, number]`| The axis of the pattern. | No |

View File

@ -162,28 +162,6 @@ A base path.
----
A circular arc, not necessarily tangential to the current point.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Arc`| | No |
| `center` |`[number, number]`| Center of the circle that this arc is drawn on. | No |
| `radius` |`number`| Radius of the circle that this arc is drawn on. | No |
| `from` |`[number, number]`| The from point. | No |
| `to` |`[number, number]`| The to point. | No |
| `tag` |[`TagDeclarator`](/docs/kcl/types#tag-declaration)| The tag of the path. | No |
| `__geoMeta` |[`GeoMeta`](/docs/kcl/types/GeoMeta)| Metadata. | No |
----

View File

@ -16,8 +16,8 @@ A sketch is a collection of paths.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `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 |
| `id` |`string`| The id of the sketch (this will change when the engine's reference to it changes. | 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 |
| `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | 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 |
|----------|------|-------------|----------|
| `type` |enum: `sketch`| | 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 |
| `id` |`string`| The id of the sketch (this will change when the engine's reference to it changes. | 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 |
| `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No |
| `tags` |`object`| Tag identifiers that have been declared in this sketch. | No |

View File

@ -18,7 +18,7 @@ Engine information for a tag.
|----------|------|-------------|----------|
| `id` |`string`| The id of the tagged object. | No |
| `sketch` |`string`| The sketch the tag is on. | No |
| `path` |[`Path`](/docs/kcl/types/Path)| The path the tag is on. | No |
| `path` |[`BasePath`](/docs/kcl/types/BasePath)| The path the tag is on. | No |
| `surface` |[`ExtrudeSurface`](/docs/kcl/types/ExtrudeSurface)| The surface information for the tag. | No |

View File

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

View File

@ -1,16 +1,6 @@
import { test, expect } from '@playwright/test'
import fsp from 'fs/promises'
import { uuidv4 } from 'lib/utils'
import {
darkModeBgColor,
darkModePlaneColorXZ,
executorInputPath,
getUtils,
setup,
setupElectron,
tearDown,
} from './test-utils'
import { join } from 'path'
import { getUtils, setup, tearDown } from './test-utils'
test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo)
@ -984,84 +974,4 @@ test.describe('Editor tests', () => {
|> close(%)
|> 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)
// 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 tronApp.page.reload()
@ -225,11 +222,9 @@ test.describe('when using the file tree to', () => {
)
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 kcl2 = '2.kcl'
await createNewFileAndSelect(kcl2)
const kclCylinder = await fsp.readFile(
'src/wasm-lib/tests/executor/inputs/cylinder.kcl',
@ -237,9 +232,6 @@ test.describe('when using the file tree to', () => {
)
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 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')
})
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 }) => {
const initialCode = `sketch001 = startSketchOn('XZ')`

View File

@ -255,7 +255,7 @@ test.describe('Can export from electron app', () => {
},
{ timeout: 15_000 }
)
.toBeGreaterThan(300_000)
.toBe(431341)
// clean up 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.',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
@ -861,12 +861,6 @@ test.fixme(
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"
await expect(page.getByText('No Projects found')).toBeVisible()
@ -879,7 +873,16 @@ test.fixme(
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')
|> startProfileAt([-87.4, 282.92], %)
@ -889,28 +892,8 @@ test.fixme(
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(200, sketch001)`)
await page.waitForTimeout(800)
async function getCameraZValue() {
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)
})
const pointOnModel = { x: 660, y: 250 }
// gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color)
@ -918,7 +901,7 @@ extrude001 = extrude(200, sketch001)`)
.poll(() => u.getGreatestPixDiff(pointOnModel, [143, 143, 143]), {
timeout: 10_000,
})
.toBeLessThan(30)
.toBeLessThan(15)
await expect(async () => {
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.waitForTimeout(1000)
await page.waitForTimeout(300)
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
@ -528,7 +528,6 @@ test(
// Draw the rectangle
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 30)
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
await expect(page).toHaveScreenshot({
@ -670,7 +669,6 @@ test.describe(
// screen shot should show the sketch
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
})
// exit sketch
@ -688,7 +686,6 @@ test.describe(
// second screen shot should look almost identical, i.e. scale should be the same.
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
})
})
@ -896,7 +893,7 @@ test(
// Wait for the second extrusion to appear
// TODO: Find a way to truly know that the objects have finished
// rendering, because an execution-done message is not sufficient.
await page.waitForTimeout(2000)
await page.waitForTimeout(1000)
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
@ -940,7 +937,7 @@ test(
// Wait for the second extrusion to appear
// TODO: Find a way to truly know that the objects have finished
// rendering, because an execution-done message is not sufficient.
await page.waitForTimeout(2000)
await page.waitForTimeout(1000)
await expect(page).toHaveScreenshot({
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: 46 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 41 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,
}
/** 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"]'
type PaneId = 'variables' | 'code' | 'files' | 'logs'
@ -471,9 +463,6 @@ export async function getUtils(page: Page, test_?: typeof test) {
return test_?.step(
`Create and select project with text "${hasText}"`,
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()
const projectLinksPost = page.getByTestId('project-link')
await projectLinksPost.filter({ hasText }).click()
@ -503,11 +492,6 @@ export async function getUtils(page: Page, test_?: typeof test) {
createNewFile: async (name: string) => {
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('file-rename-field').fill(name)
await page.keyboard.press('Enter')
@ -888,20 +872,10 @@ export async function setupElectron({
const tempSettingsFilePath = join(projectDirName, SETTINGS_FILE_NAME)
const settingsOverrides = TOML.stringify(
appSettings
? {
settings: {
...TEST_SETTINGS,
...appSettings,
app: {
...TEST_SETTINGS.app,
projectDirectory: projectDirName,
...appSettings.app,
},
},
}
? { settings: appSettings }
: {
...TEST_SETTINGS,
settings: {
...TEST_SETTINGS,
app: {
...TEST_SETTINGS.app,
projectDirectory: projectDirName,

View File

@ -292,7 +292,7 @@ test.describe(`Testing gizmo, fixture-based`, () => {
await test.step(`Verify the camera moved`, async () => {
await scene.expectState({
camera: {
position: [0, -23865.37, 11073.53],
position: [0, -23865.37, 11073.54],
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 page.getByRole('button', { name: 'New project' }).click()
await page.getByTestId('project-link').first().click()
await page.getByRole('button', { name: 'Dismiss' }).click()
await changeColor('58')
await expect(logoLink).toHaveCSS('--primary-hue', '58')
})

8
interface.d.ts vendored
View File

@ -2,7 +2,7 @@ import fs from 'node:fs/promises'
import fsSync from 'node:fs'
import path from 'path'
import { dialog, shell } from 'electron'
import { MachinesListing } from 'components/MachineManagerProvider'
import { MachinesListing } from 'lib/machineManager'
type EnvFn = (value?: string) => string
@ -23,8 +23,8 @@ export interface IElectronAPI {
key: string,
callback: (eventType: string, path: string) => void
) => void
readFile: typeof fs.readFile
watchFileOff: (path: string, key: string) => void
readFile: (path: string) => ReturnType<fs.readFile>
writeFile: (
path: string,
data: string | Uint8Array
@ -68,13 +68,13 @@ export interface IElectronAPI {
}
}
kittycad: (access: string, args: any) => any
listMachines: (machineApiIp: string) => Promise<MachinesListing>
listMachines: () => Promise<MachinesListing>
getMachineApiIp: () => Promise<string | null>
onUpdateDownloadStart: (
callback: (value: { version: string }) => void
) => Electron.IpcRenderer
onUpdateDownloaded: (
callback: (value: { version: string; releaseNotes: string }) => void
callback: (value: string) => void
) => Electron.IpcRenderer
onUpdateError: (callback: (value: { error: Error }) => void) => Electron
appRestart: () => void

View File

@ -70,7 +70,7 @@ echo ""
echo "Suggested changelog:"
echo "\`\`\`"
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 "**Full Changelog**: https://github.com/KittyCAD/modeling-app/compare/${latest_tag}...${new_version}"
echo "\`\`\`"

View File

@ -36,319 +36,38 @@
"description": "Extra machine-specific information regarding a connected machine.",
"oneOf": [
{
"additionalProperties": false,
"properties": {
"type": {
"enum": [
"moonraker"
],
"type": "string"
"Moonraker": {
"type": "object"
}
},
"required": [
"type"
"Moonraker"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"type": {
"enum": [
"usb"
],
"type": "string"
"Usb": {
"type": "object"
}
},
"required": [
"type"
"Usb"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"current_stage": {
"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"
"Bambu": {
"type": "object"
}
},
"required": [
"nozzle_diameter",
"type"
],
"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": [
"filaments",
"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"
"Bambu"
],
"type": "object"
}
@ -366,14 +85,6 @@
"description": "Additional, per-machine information which is specific to the underlying machine type.",
"nullable": true
},
"hardware_configuration": {
"allOf": [
{
"$ref": "#/components/schemas/HardwareConfiguration"
}
],
"description": "Information about how the Machine is currently configured."
},
"id": {
"description": "Machine Identifier (ID) for the specific Machine.",
"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!",
"nullable": true
},
"progress": {
"description": "Progress of the current print, if printing.",
"format": "double",
"nullable": true,
"type": "number"
},
"state": {
"allOf": [
{
@ -419,7 +124,6 @@
}
},
"required": [
"hardware_configuration",
"id",
"machine_type",
"make_model",
@ -453,111 +157,57 @@
"oneOf": [
{
"description": "If a print state can not be resolved at this time, an Unknown may be returned.",
"properties": {
"state": {
"enum": [
"unknown"
],
"type": "string"
}
},
"required": [
"state"
"enum": [
"Unknown"
],
"type": "object"
"type": "string"
},
{
"description": "Idle, and ready for another job.",
"properties": {
"state": {
"enum": [
"idle"
],
"type": "string"
}
},
"required": [
"state"
"enum": [
"Idle"
],
"type": "object"
"type": "string"
},
{
"description": "Running a job -- 3D printing or CNC-ing a part.",
"properties": {
"state": {
"enum": [
"running"
],
"type": "string"
}
},
"required": [
"state"
"enum": [
"Running"
],
"type": "object"
"type": "string"
},
{
"description": "Machine is currently offline or unreachable.",
"properties": {
"state": {
"enum": [
"offline"
],
"type": "string"
}
},
"required": [
"state"
"enum": [
"Offline"
],
"type": "object"
"type": "string"
},
{
"description": "Job is underway but halted, waiting for some action to take place.",
"properties": {
"state": {
"enum": [
"paused"
],
"type": "string"
}
},
"required": [
"state"
"enum": [
"Paused"
],
"type": "object"
"type": "string"
},
{
"description": "Job is finished, but waiting manual action to move back to Idle.",
"properties": {
"state": {
"enum": [
"complete"
],
"type": "string"
}
},
"required": [
"state"
"enum": [
"Complete"
],
"type": "object"
"type": "string"
},
{
"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.",
"properties": {
"message": {
"description": "A human-readable message describing the failure.",
"Failed": {
"nullable": true,
"type": "string"
},
"state": {
"enum": [
"failed"
],
"type": "string"
}
},
"required": [
"state"
"Failed"
],
"type": "object"
}
@ -569,54 +219,21 @@
{
"description": "Use light to cure a resin to build up layers.",
"enum": [
"stereolithography"
"Stereolithography"
],
"type": "string"
},
{
"description": "Fused Deposition Modeling, layers of melted plastic.",
"enum": [
"fused_deposition"
"FusedDeposition"
],
"type": "string"
},
{
"description": "\"Computer numerical control\" - machine that grinds away material from a hunk of material to construct a part.",
"enum": [
"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"
"Cnc"
],
"type": "string"
}
@ -667,15 +284,6 @@
"machine_id": {
"description": "The machine id to print to.",
"type": "string"
},
"slicer_configuration": {
"allOf": [
{
"$ref": "#/components/schemas/SlicerConfiguration"
}
],
"description": "Requested design-specific slicer configurations.",
"nullable": true
}
},
"required": [
@ -684,283 +292,6 @@
],
"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": {
"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": {
@ -996,7 +327,7 @@
},
"description": "",
"title": "machine-api",
"version": "0.1.1"
"version": "0.1.0"
},
"openapi": "3.0.3",
"paths": {
@ -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": {
"get": {
"operationId": "ping",
@ -1189,13 +492,6 @@
}
},
"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.",
"externalDocs": {

View File

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

View File

@ -21,7 +21,6 @@ import { WasmErrBanner } from 'components/WasmErrBanner'
import { CommandBar } from 'components/CommandBar/CommandBar'
import ModelingMachineProvider from 'components/ModelingMachineProvider'
import FileMachineProvider from 'components/FileMachineProvider'
import { MachineManagerProvider } from 'components/MachineManagerProvider'
import { PATHS } from 'lib/paths'
import {
fileLoader,
@ -50,7 +49,6 @@ const router = createRouter([
{
loader: settingsLoader,
id: PATHS.INDEX,
// TODO: Re-evaluate if this is true
/* Make sure auth is the outermost provider or else we will have
* inefficient re-renders, use the react profiler to see. */
element: (
@ -59,9 +57,7 @@ const router = createRouter([
<LspProvider>
<KclContextProvider>
<AppStateProvider>
<MachineManagerProvider>
<Outlet />
</MachineManagerProvider>
<Outlet />
</AppStateProvider>
</KclContextProvider>
</LspProvider>

View File

@ -64,27 +64,6 @@ export type ReactCameraProperties =
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 {
engineCommandManager: EngineCommandManager
syncDirection: 'clientToEngine' | 'engineToClient' = 'engineToClient'
@ -92,15 +71,15 @@ export class CameraControls {
target: Vector3
domElement: HTMLCanvasElement
isDragging: boolean
wasDragging: boolean
mouseDownPosition: Vector2
mouseNewPosition: Vector2
rotationSpeed = 0.3
enableRotate = true
enablePan = true
enableZoom = true
moveSender: CameraRateLimiter = new CameraRateLimiter()
zoomSender: CameraRateLimiter = new CameraRateLimiter()
zoomDataFromLastFrame?: number = undefined
// holds coordinates, and interaction
moveDataFromLastFrame?: [number, number, string] = undefined
lastPerspectiveFov: number = 45
pendingZoom: number | 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(
isOrtho = false,
domElement: HTMLCanvasElement,
@ -234,7 +183,6 @@ export class CameraControls {
this.target = new Vector3()
this.domElement = domElement
this.isDragging = false
this.wasDragging = false
this.mouseDownPosition = new Vector2()
this.mouseNewPosition = new Vector2()
@ -310,6 +258,49 @@ export class CameraControls {
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(() => {
this.engineCommandManager.subscribeTo({
event: 'camera_drag_end',
@ -365,8 +356,6 @@ export class CameraControls {
onMouseDown = (event: PointerEvent) => {
this.domElement.setPointerCapture(event.pointerId)
this.isDragging = true
// Reset the wasDragging flag to false when starting a new drag
this.wasDragging = false
this.mouseDownPosition.set(event.clientX, event.clientY)
let interaction = this.getInteractionType(event)
if (interaction === 'none') return
@ -396,18 +385,11 @@ export class CameraControls {
const interaction = this.getInteractionType(event)
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') {
this.moveSender.send(() => {
this.doMove(interaction, [event.clientX, event.clientY])
})
this.moveDataFromLastFrame = [event.clientX, event.clientY, interaction]
return
}
// else "clientToEngine" (Sketch Mode) or forceUpdate
// Implement camera movement logic here based on deltaMove
// For example, for rotating the camera around the target:
if (interaction === 'rotate') {
@ -436,9 +418,6 @@ export class CameraControls {
* under the cursor. This recently moved from being handled in App.tsx.
* This might not be the right spot, but it is more consolidated.
*/
// Clear any previous drag state
this.wasDragging = false
if (this.syncDirection === 'engineToClient') {
const newCmdId = uuidv4()
@ -480,9 +459,7 @@ export class CameraControls {
if (this.syncDirection === 'engineToClient') {
if (interaction === 'zoom') {
this.zoomSender.send(() => {
this.doZoom(event.deltaY)
})
this.zoomDataFromLastFrame = event.deltaY
} else {
// This case will get handled when we add pan and rotate using Apple trackpad.
console.error(

View File

@ -338,11 +338,6 @@ export class SceneEntities {
sceneInfra.setCallbacks({
onClick: async (args) => {
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
const { intersectionPoint } = args
if (!intersectionPoint?.twoD || !sketchDetails?.sketchPathToNode) return
@ -412,7 +407,7 @@ export class SceneEntities {
if (err(sketch)) return Promise.reject(sketch)
if (!sketch) return Promise.reject('sketch not found')
if (!isArray(sketch?.paths))
if (!isArray(sketch?.value))
return {
truncatedAst,
programMemoryOverride,
@ -440,7 +435,7 @@ export class SceneEntities {
maybeModdedAst,
sketch.start.__geoMeta.sourceRange
)
if (sketch?.paths?.[0]?.type !== 'Circle') {
if (sketch?.value?.[0]?.type !== 'Circle') {
const _profileStart = createProfileStartHandle({
from: sketch.start.from,
id: sketch.start.__geoMeta.id,
@ -456,16 +451,16 @@ export class SceneEntities {
this.activeSegments[JSON.stringify(segPathToNode)] = _profileStart
}
const callbacks: (() => SegmentOverlayPayload | null)[] = []
sketch.paths.forEach((segment, index) => {
sketch.value.forEach((segment, index) => {
let segPathToNode = getNodePathFromSourceRange(
maybeModdedAst,
segment.__geoMeta.sourceRange
)
if (
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(
maybeModdedAst,
previousSegment.__geoMeta.sourceRange
@ -516,7 +511,7 @@ export class SceneEntities {
to: segment.to,
}
const result = initSegment({
prevSegment: sketch.paths[index - 1],
prevSegment: sketch.value[index - 1],
callExpName,
input,
id: segment.__geoMeta.id,
@ -615,9 +610,9 @@ export class SceneEntities {
variableDeclarationName
)
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({
node: _ast,
programMemory: kclManager.programMemory,
@ -650,13 +645,7 @@ export class SceneEntities {
sceneInfra.setCallbacks({
onClick: async (args) => {
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
const { intersectionPoint } = args
let intersection2d = intersectionPoint?.twoD
const profileStart = args.intersects
@ -665,7 +654,7 @@ export class SceneEntities {
let modifiedAst
if (profileStart) {
const lastSegment = sketch.paths.slice(-1)[0]
const lastSegment = sketch.value.slice(-1)[0]
modifiedAst = addCallExpressionsToPipe({
node: kclManager.ast,
programMemory: kclManager.programMemory,
@ -697,7 +686,7 @@ export class SceneEntities {
})
if (trap(modifiedAst)) return Promise.reject(modifiedAst)
} else if (intersection2d) {
const lastSegment = sketch.paths.slice(-1)[0]
const lastSegment = sketch.value.slice(-1)[0]
const tmp = addNewSketchLn({
node: kclManager.ast,
programMemory: kclManager.programMemory,
@ -746,6 +735,7 @@ export class SceneEntities {
},
})
},
...this.mouseEnterLeaveCallbacks(),
})
}
setupDraftRectangle = async (
@ -827,7 +817,7 @@ export class SceneEntities {
variableDeclarationName
)
if (err(sketch)) return Promise.reject(sketch)
const sgPaths = sketch.paths
const sgPaths = sketch.value
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
this.updateSegment(sketch.start, 0, 0, _ast, orthoFactor, sketch)
@ -836,11 +826,6 @@ export class SceneEntities {
)
},
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
const cornerPoint = args.intersectionPoint?.twoD
if (!cornerPoint || args.mouseEvent.button !== 0) return
@ -883,7 +868,7 @@ export class SceneEntities {
variableDeclarationName
)
if (err(sketch)) return
const sgPaths = sketch.paths
const sgPaths = sketch.value
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
// Update the starting segment of the THREEjs scene
@ -1000,7 +985,7 @@ export class SceneEntities {
variableDeclarationName
)
if (err(sketch)) return
const sgPaths = sketch.paths
const sgPaths = sketch.value
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
this.updateSegment(sketch.start, 0, 0, _ast, orthoFactor, sketch)
@ -1009,11 +994,6 @@ export class SceneEntities {
)
},
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
const cornerPoint = args.intersectionPoint?.twoD
if (!cornerPoint || args.mouseEvent.button !== 0) return
@ -1125,7 +1105,7 @@ export class SceneEntities {
const pipeIndex = pathToNode[pathToNodeIndex + 1][0] as number
if (addingNewSegmentStatus === 'nothing') {
const prevSegment = sketch.paths[pipeIndex - 2]
const prevSegment = sketch.value[pipeIndex - 2]
const mod = addNewSketchLn({
node: kclManager.ast,
programMemory: kclManager.programMemory,
@ -1177,11 +1157,6 @@ export class SceneEntities {
},
onMove: () => {},
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 || !args.selected) {
sceneInfra.modelingSend({
@ -1370,7 +1345,7 @@ export class SceneEntities {
}
if (!sketch) return
const sgPaths = sketch.paths
const sgPaths = sketch.value
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
this.updateSegment(
@ -1418,7 +1393,7 @@ export class SceneEntities {
modifiedAst,
segment.__geoMeta.sourceRange
)
const sgPaths = sketch.paths
const sgPaths = sketch.value
const originalPathToNodeStr = JSON.stringify(segPathToNode)
segPathToNode[1][0] = varDecIndex
const pathToNodeStr = JSON.stringify(segPathToNode)
@ -1726,7 +1701,7 @@ function prepareTruncatedMemoryAndAst(
variableDeclarationName
)
if (err(sg)) return sg
const lastSeg = sg?.paths.slice(-1)[0]
const lastSeg = sg?.value.slice(-1)[0]
if (draftSegment) {
// truncatedAst needs to setup with another segment at the end
let newSegment

View File

@ -213,7 +213,7 @@ export class SceneInfra {
to: Coords2d
angle?: number
}): SegmentOverlayPayload | null {
if (!group.userData.draft && group.userData.pathToNode && arrowGroup) {
if (group.userData.pathToNode && arrowGroup) {
const vector = new Vector3(0, 0, 0)
// Get the position of the object3D in world space

View File

@ -58,7 +58,7 @@ import { err } from 'lib/trap'
interface CreateSegmentArgs {
input: SegmentInputs
prevSegment: Sketch['paths'][number]
prevSegment: Sketch['value'][number]
id: string
pathToNode: PathToNode
isDraftSegment?: boolean
@ -72,7 +72,7 @@ interface CreateSegmentArgs {
interface UpdateSegmentArgs {
input: SegmentInputs
prevSegment: Sketch['paths'][number]
prevSegment: Sketch['value'][number]
group: Group
sceneInfra: SceneInfra
scale?: number
@ -147,7 +147,6 @@ class StraightSegment implements SegmentUtils {
segmentGroup.name = STRAIGHT_SEGMENT
segmentGroup.userData = {
type: STRAIGHT_SEGMENT,
draft: isDraftSegment,
id,
from,
to,
@ -348,7 +347,6 @@ class TangentialArcToSegment implements SegmentUtils {
mesh.name = meshName
group.userData = {
type: TANGENTIAL_ARC_TO_SEGMENT,
draft: isDraftSegment,
id,
from,
to,
@ -517,18 +515,11 @@ class CircleSegment implements SegmentUtils {
const meshType = isDraftSegment ? CIRCLE_SEGMENT_DASH : CIRCLE_SEGMENT_BODY
const arrowGroup = createArrowhead(scale, theme, color)
const circleCenterGroup = createCircleCenterHandle(scale, theme, color)
// A radius indicator that appears from the center to the perimeter
const radiusIndicatorGroup = createLengthIndicator({
from: center,
to: [center[0] + radius, center[1]],
scale,
})
arcMesh.userData.type = meshType
arcMesh.name = meshType
group.userData = {
type: CIRCLE_SEGMENT,
draft: isDraftSegment,
id,
from,
radius,
@ -541,7 +532,7 @@ class CircleSegment implements SegmentUtils {
}
group.name = CIRCLE_SEGMENT
group.add(arcMesh, arrowGroup, circleCenterGroup, radiusIndicatorGroup)
group.add(arcMesh, arrowGroup, circleCenterGroup)
const updateOverlaysCallback = this.update({
prevSegment,
input,
@ -573,9 +564,6 @@ class CircleSegment implements SegmentUtils {
group.userData.radius = radius
group.userData.prevSegment = prevSegment
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
const radiusLengthIndicator = group.getObjectByName(
SEGMENT_LENGTH_LABEL
) as Group
const circleCenterHandle = group.getObjectByName(
CIRCLE_CENTER_HANDLE
) as Group
@ -593,14 +581,11 @@ class CircleSegment implements SegmentUtils {
}
if (arrowGroup) {
// The arrowhead is placed at the perimeter of the circle,
// pointing up and to the right
const arrowPoint = {
x: center[0] + Math.cos(Math.PI / 4) * radius,
y: center[1] + Math.sin(Math.PI / 4) * radius,
}
arrowGroup.position.set(arrowPoint.x, arrowPoint.y, 0)
arrowGroup.position.set(
center[0] + Math.cos(Math.PI / 4) * radius,
center[1] + Math.sin(Math.PI / 4) * radius,
0
)
const arrowheadAngle = Math.PI / 4
arrowGroup.quaternion.setFromUnitVectors(
@ -611,31 +596,6 @@ class CircleSegment implements SegmentUtils {
arrowGroup.visible = isHandlesVisible
}
if (radiusLengthIndicator) {
// The radius indicator is placed at the midpoint of the radius,
// at a 45 degree CCW angle from the positive X-axis
const indicatorPoint = {
x: center[0] + (Math.cos(Math.PI / 4) * radius) / 2,
y: center[1] + (Math.sin(Math.PI / 4) * radius) / 2,
}
const labelWrapper = radiusLengthIndicator.getObjectByName(
SEGMENT_LENGTH_LABEL_TEXT
) as CSS2DObject
const labelWrapperElem = labelWrapper.element as HTMLDivElement
const label = labelWrapperElem.children[0] as HTMLParagraphElement
label.innerText = `${roundOff(radius)}`
label.classList.add(SEGMENT_LENGTH_LABEL_TEXT)
const isPlaneBackFace = center[0] > indicatorPoint.x
label.style.setProperty(
'--degree',
`${isPlaneBackFace ? '45' : '-45'}deg`
)
label.style.setProperty('--x', `0px`)
label.style.setProperty('--y', `0px`)
labelWrapper.position.set(indicatorPoint.x, indicatorPoint.y, 0)
radiusLengthIndicator.visible = isHandlesVisible
}
if (circleCenterHandle) {
circleCenterHandle.position.set(center[0], center[1], 0)
circleCenterHandle.scale.set(scale, scale, scale)

View File

@ -135,9 +135,7 @@ function CommandArgOptionInput({
<Combobox.Input
id="option-input"
ref={inputRef}
onChange={(event) =>
!event.target.disabled && setQuery(event.target.value)
}
onChange={(event) => 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"
onKeyDown={(event) => {
if (event.metaKey && event.key === 'k')
@ -177,18 +175,9 @@ function CommandArgOptionInput({
<Combobox.Option
key={option.name}
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"
>
<p
className={`flex-grow ${
(option.disabled &&
'text-chalkboard-70 dark:text-chalkboard-50 cursor-not-allowed') ||
''
}`}
>
{option.name}
</p>
<p className="flex-grow">{option.name} </p>
{option.value === currentOption?.value && (
<small className="text-chalkboard-70 dark:text-chalkboard-50">
current

View File

@ -140,14 +140,7 @@ const FileTreeItem = ({
async (eventType, path) => {
// Don't try to read a file that was removed.
if (isCurrentFile && eventType !== 'unlink') {
// Prevents a cyclic read / write causing editor problems such as
// misplaced cursor positions.
if (codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher) {
codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher = false
return
}
let code = await window.electron.readFile(path, { encoding: 'utf-8' })
let code = await window.electron.readFile(path, 'utf-8')
code = normalizeLineEndings(code)
codeManager.updateCodeStateEditor(code)
}
@ -495,12 +488,6 @@ export const FileTreeInner = ({
// Refresh the file tree when there are changes.
useFileSystemWatcher(
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' })
},
[loaderData?.project?.path, fileContext.selectedDirectory.path].filter(

View File

@ -23,7 +23,6 @@ export function LowerRightControls({
}) {
const location = useLocation()
const filePath = useAbsoluteFilePath()
const linkOverrideClassName =
'!text-chalkboard-70 hover:!text-chalkboard-80 dark:!text-chalkboard-40 dark:hover:!text-chalkboard-30'

View File

@ -1,123 +0,0 @@
import { createContext, useEffect, useState } from 'react'
import { engineCommandManager } from 'lib/singletons'
import { CommandsContext } from 'components/CommandBar/CommandBarProvider'
import { isDesktop } from 'lib/isDesktop'
import { components } from 'lib/machine-api'
import { reportRejection } from 'lib/trap'
import { toSync } from 'lib/utils'
export type MachinesListing = Array<
components['schemas']['MachineInfoResponse']
>
export interface MachineManager {
machines: MachinesListing
machineApiIp: string | null
currentMachine: components['schemas']['MachineInfoResponse'] | null
noMachinesReason: () => string | undefined
setCurrentMachine: (
m: components['schemas']['MachineInfoResponse'] | null
) => void
}
export const MachineManagerContext = createContext<MachineManager>({
machines: [],
machineApiIp: null,
currentMachine: null,
setCurrentMachine: (
_: components['schemas']['MachineInfoResponse'] | null
) => {},
noMachinesReason: () => undefined,
})
export const MachineManagerProvider = ({
children,
}: {
children: React.ReactNode
}) => {
const [machines, setMachines] = useState<MachinesListing>([])
const [machineApiIp, setMachineApiIp] = useState<string | null>(null)
const [currentMachine, setCurrentMachine] = useState<
components['schemas']['MachineInfoResponse'] | null
>(null)
const commandBarActor = CommandsContext.useActorRef()
// Get the reason message for why there are no machines.
const noMachinesReason = (): string | undefined => {
if (machines.length > 0) {
return undefined
}
if (machineApiIp === null) {
return 'Machine API server was not discovered'
}
return 'Machine API server was discovered, but no machines are available'
}
useEffect(() => {
if (!isDesktop()) return
const update = async () => {
const _machineApiIp = await window.electron.getMachineApiIp()
if (_machineApiIp === null) return
setMachineApiIp(_machineApiIp)
const _machines = await window.electron.listMachines(_machineApiIp)
setMachines(_machines)
}
// Start a background job to update the machines every ten seconds.
// If MDNS is already watching, this timeout will wait until it's done to trigger the
// finding again.
let timeoutId: ReturnType<typeof setTimeout> | undefined = undefined
const timeoutLoop = () => {
clearTimeout(timeoutId)
timeoutId = setTimeout(
toSync(async () => {
await update()
timeoutLoop()
}, reportRejection),
1000
)
}
timeoutLoop()
update().catch(reportRejection)
}, [])
// Update engineCommandManager's copy of this data.
useEffect(() => {
const machineManagerNext = {
machines,
machineApiIp,
currentMachine,
noMachinesReason,
setCurrentMachine,
}
engineCommandManager.machineManager = machineManagerNext
commandBarActor.send({
type: 'Set machine manager',
data: machineManagerNext,
})
}, [machines, machineApiIp, currentMachine])
return (
<MachineManagerContext.Provider
value={{
machines,
machineApiIp,
currentMachine,
setCurrentMachine,
noMachinesReason,
}}
>
{' '}
{children}{' '}
</MachineManagerContext.Provider>
)
}

View File

@ -1,11 +1,5 @@
import { useMachine } from '@xstate/react'
import React, {
createContext,
useEffect,
useMemo,
useRef,
useContext,
} from 'react'
import React, { createContext, useEffect, useMemo, useRef } from 'react'
import {
Actor,
AnyStateMachine,
@ -34,7 +28,7 @@ import {
editorManager,
sceneEntitiesManager,
} from 'lib/singletons'
import { MachineManagerContext } from 'components/MachineManagerProvider'
import { machineManager } from 'lib/machineManager'
import { useHotkeys } from 'react-hotkeys-hook'
import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance'
import {
@ -75,7 +69,7 @@ import { exportFromEngine } from 'lib/exportFromEngine'
import { Models } from '@kittycad/lib/dist/types/src'
import toast from 'react-hot-toast'
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 { getVarNameModal } from 'hooks/useToolbarGuards'
import { err, reportRejection, trap } from 'lib/trap'
@ -90,7 +84,6 @@ import {
import { submitAndAwaitTextToKcl } from 'lib/textToCad'
import { useFileContext } from 'hooks/useFileContext'
import { uuidv4 } from 'lib/utils'
import { IndexLoaderData } from 'lib/types'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
@ -123,7 +116,6 @@ export const ModelingMachineProvider = ({
} = useSettingsAuthContext()
const navigate = useNavigate()
const { context, send: fileMachineSend } = useFileContext()
const { file } = useLoaderData() as IndexLoaderData
const token = auth?.context?.token
const streamRef = useRef<HTMLDivElement>(null)
const persistedContext = useMemo(() => getPersistedContext(), [])
@ -146,8 +138,6 @@ export const ModelingMachineProvider = ({
// >
// )
const machineManager = useContext(MachineManagerContext)
const [modelingState, modelingSend, modelingActor] = useMachine(
modelingMachine.provide({
actions: {
@ -416,35 +406,18 @@ export const ModelingMachineProvider = ({
return {}
}
),
Make: ({ context, event }) => {
Make: ({ event }) => {
if (event.type !== 'Make') return
// Check if we already have an export intent.
if (engineCommandManager.exportInfo) {
if (engineCommandManager.exportIntent) {
toast.error('Already exporting')
return
}
// Set the export intent.
engineCommandManager.exportInfo = {
intent: ExportIntent.Make,
name: file?.name || '',
}
engineCommandManager.exportIntent = ExportIntent.Make
// Set the current machine.
// Due to our use of singeton pattern, we need to do this to reliably
// update this object across React and non-React boundary.
// We need to do this eagerly because of the exportToEngine call below.
if (engineCommandManager.machineManager === null) {
console.warn(
"engineCommandManager.machineManager is null. It shouldn't be at this point. Aborting operation."
)
return
} else {
engineCommandManager.machineManager.currentMachine =
event.data.machine
}
// Update the rest of the UI that needs to know the current machine
context.machineManager.setCurrentMachine(event.data.machine)
machineManager.currentMachine = event.data.machine
const format: Models['OutputFormat_type'] = {
type: 'stl',
@ -470,16 +443,12 @@ export const ModelingMachineProvider = ({
},
'Engine export': ({ event }) => {
if (event.type !== 'Export') return
if (engineCommandManager.exportInfo) {
if (engineCommandManager.exportIntent) {
toast.error('Already exporting')
return
}
// Set the export intent.
engineCommandManager.exportInfo = {
intent: ExportIntent.Save,
// This never gets used its only for make.
name: '',
}
engineCommandManager.exportIntent = ExportIntent.Save
const format = {
...event.data,
@ -666,7 +635,6 @@ export const ModelingMachineProvider = ({
input.plane
)
await kclManager.updateAst(modifiedAst, false)
sceneInfra.camControls.enableRotate = false
sceneInfra.camControls.syncDirection = 'clientToEngine'
await letEngineAnimateAndSyncCamAfter(
@ -1017,7 +985,6 @@ export const ModelingMachineProvider = ({
...modelingMachineDefaultContext.store,
...persistedContext,
},
machineManager,
},
// devTools: true,
}

View File

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

View File

@ -1,12 +1,6 @@
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { Resizable } from 're-resizable'
import {
MouseEventHandler,
useCallback,
useEffect,
useMemo,
useContext,
} from 'react'
import { MouseEventHandler, useCallback, useEffect, useMemo } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { SidebarAction, SidebarType, sidebarPanes } from './ModelingPanes'
import Tooltip from 'components/Tooltip'
@ -19,7 +13,7 @@ import { CustomIconName } from 'components/CustomIcon'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
import { useKclContext } from 'lang/KclProvider'
import { MachineManagerContext } from 'components/MachineManagerProvider'
import { machineManager } from 'lib/machineManager'
interface ModelingSidebarProps {
paneOpacity: '' | 'opacity-20' | 'opacity-40'
@ -35,7 +29,6 @@ function getPlatformString(): 'web' | 'desktop' {
}
export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
const machineManager = useContext(MachineManagerContext)
const { commandBarSend } = useCommandsContext()
const kclContext = useKclContext()
const { settings } = useSettingsAuthContext()

View File

@ -1,9 +1,7 @@
import { Popover } from '@headlessui/react'
import { useContext } from 'react'
import Tooltip from './Tooltip'
import { machineManager } from 'lib/machineManager'
import { isDesktop } from 'lib/isDesktop'
import { components } from 'lib/machine-api'
import { MachineManagerContext } from 'components/MachineManagerProvider'
import { CustomIcon } from './CustomIcon'
export const NetworkMachineIndicator = ({
@ -11,12 +9,8 @@ export const NetworkMachineIndicator = ({
}: {
className?: string
}) => {
const {
noMachinesReason,
machines,
machines: { length: machineCount },
} = useContext(MachineManagerContext)
const reason = noMachinesReason()
const machineCount = machineManager.machineCount()
const reason = machineManager.noMachinesReason()
return isDesktop() ? (
<Popover className="relative">
@ -52,35 +46,19 @@ export const NetworkMachineIndicator = ({
</div>
{machineCount > 0 && (
<ul className="divide-y divide-chalkboard-20 dark:divide-chalkboard-80">
{machines.map(
(machine: components['schemas']['MachineInfoResponse']) => {
return (
<li key={machine.id} className={'px-2 py-4 gap-1 last:mb-0 '}>
<p className="">{machine.id.toUpperCase()}</p>
<p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs">
{machine.make_model.model}
</p>
{machine.extra &&
machine.extra.type === 'bambu' &&
machine.extra.nozzle_diameter && (
<p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs">
Nozzle Diameter: {machine.extra.nozzle_diameter}
</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>
</li>
)
}
{Object.entries(machineManager.machines).map(
([hostname, machine]) => (
<li key={hostname} className={'px-2 py-4 gap-1 last:mb-0 '}>
<p className="">
{machine.make_model.model ||
machine.make_model.manufacturer ||
'Unknown Machine'}
</p>
<p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs">
Hostname {hostname}
</p>
</li>
)
)}
</ul>
)}

View File

@ -4,14 +4,14 @@ import { type IndexLoaderData } from 'lib/types'
import { PATHS } from 'lib/paths'
import { isDesktop } from '../lib/isDesktop'
import { Link, useLocation, useNavigate } from 'react-router-dom'
import { Fragment, useMemo, useContext } from 'react'
import { Fragment, useMemo } from 'react'
import { Logo } from './Logo'
import { APP_NAME } from 'lib/constants'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { CustomIcon } from './CustomIcon'
import { useLspContext } from './LspProvider'
import { engineCommandManager } from 'lib/singletons'
import { MachineManagerContext } from 'components/MachineManagerProvider'
import { machineManager } from 'lib/machineManager'
import usePlatform from 'hooks/usePlatform'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import Tooltip from './Tooltip'
@ -96,8 +96,6 @@ function ProjectMenuPopover({
const location = useLocation()
const navigate = useNavigate()
const filePath = useAbsoluteFilePath()
const machineManager = useContext(MachineManagerContext)
const { commandBarState, commandBarSend } = useCommandsContext()
const { onProjectClose } = useLspContext()
const exportCommandInfo = { name: 'Export', groupId: 'modeling' }
@ -108,7 +106,7 @@ function ProjectMenuPopover({
(c) => c.name === obj.name && c.groupId === obj.groupId
)
)
const machineCount = machineManager.machines.length
const machineCount = machineManager.machineCount()
// We filter this memoized list so that no orphan "break" elements are rendered.
const projectMenuItems = useMemo<(ActionButtonProps | 'break')[]>(

View File

@ -255,14 +255,10 @@ export const Stream = () => {
}, [mediaStream])
const handleMouseUp: MouseEventHandler<HTMLDivElement> = (e) => {
// If we've got no stream or connection, don't do anything
if (!isNetworkOkay) 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({ 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) {
// 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 { ActionButton } from './ActionButton'
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { Marked } from '@ts-stack/markdown'
export function ToastUpdate({
version,
releaseNotes,
onRestart,
onDismiss,
}: {
version: string
releaseNotes?: string
onRestart: () => void
onDismiss: () => void
}) {
const containsBreakingChanges = releaseNotes
?.toLocaleLowerCase()
.includes('breaking')
return (
<div className="inset-0 z-50 grid place-content-center rounded bg-chalkboard-110/50 shadow-md">
<div className="max-w-3xl min-w-[35rem] p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
@ -28,7 +19,7 @@ export function ToastUpdate({
>
v{version}
</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
start the app. You can view the release notes{' '}
<a
@ -41,39 +32,15 @@ export function ToastUpdate({
>
here on GitHub.
</a>
</p>
</span>
</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">
<ActionButton
Element="button"
iconStart={{
icon: 'arrowRotateRight',
}}
name="restart"
name="Restart app now"
onClick={onRestart}
>
Restart app now
@ -83,10 +50,9 @@ export function ToastUpdate({
iconStart={{
icon: 'checkmark',
}}
name="dismiss"
name="Got it"
onClick={() => {
toast.dismiss()
onDismiss()
}}
>
Got it

View File

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

View File

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

View File

@ -70,17 +70,15 @@ if (isDesktop()) {
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.`
console.log(message)
toast.custom(
ToastUpdate({
version,
releaseNotes,
onRestart: () => {
window.electron.appRestart()
},
onDismiss: () => {},
}),
{ duration: 30000, id: AUTO_UPDATER_TOAST_ID }
)

View File

@ -40,7 +40,9 @@ export class KclManager {
nonCodeMeta: {
nonCodeNodes: {},
start: [],
digest: null,
},
digest: null,
}
private _execState: ExecState = emptyExecState()
private _programMemory: ProgramMemory = ProgramMemory.empty()
@ -206,7 +208,9 @@ export class KclManager {
nonCodeMeta: {
nonCodeNodes: {},
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],
},
},
paths: [
value: [
{
type: 'ToPoint',
tag: null,
@ -96,7 +96,7 @@ const mySketch001 = startSketchOn('XY')
on: expect.any(Object),
start: expect.any(Object),
type: 'Sketch',
paths: [
value: [
{
type: 'ToPoint',
from: [0, 0],
@ -172,6 +172,7 @@ const sk2 = startSketchOn('XY')
start: 114,
type: 'TagDeclarator',
value: 'p',
digest: null,
},
id: expect.any(String),
sourceRange: [95, 117],
@ -202,7 +203,7 @@ const sk2 = startSketchOn('XY')
info: expect.any(Object),
},
},
paths: [
value: [
{
type: 'ToPoint',
from: [0, 0],
@ -222,6 +223,7 @@ const sk2 = startSketchOn('XY')
start: 114,
type: 'TagDeclarator',
value: 'p',
digest: null,
},
__geoMeta: {
id: expect.any(String),
@ -264,6 +266,7 @@ const sk2 = startSketchOn('XY')
start: 417,
type: 'TagDeclarator',
value: 'o',
digest: null,
},
id: expect.any(String),
sourceRange: [399, 420],
@ -294,7 +297,7 @@ const sk2 = startSketchOn('XY')
info: expect.any(Object),
},
},
paths: [
value: [
{
type: 'ToPoint',
from: [0, 0],
@ -314,6 +317,7 @@ const sk2 = startSketchOn('XY')
start: 417,
type: 'TagDeclarator',
value: 'o',
digest: null,
},
__geoMeta: {
id: expect.any(String),

View File

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

View File

@ -58,7 +58,7 @@ const newVar = myVar + 1`
`
const mem = await exe(code)
// 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([
{
type: 'ToPoint',
@ -73,6 +73,7 @@ const newVar = myVar + 1`
start: 89,
type: 'TagDeclarator',
value: 'myPath',
digest: null,
},
},
{
@ -98,6 +99,7 @@ const newVar = myVar + 1`
start: 143,
type: 'TagDeclarator',
value: 'rightPath',
digest: null,
},
},
])
@ -175,7 +177,7 @@ const newVar = myVar + 1`
info: expect.any(Object),
},
},
paths: [
value: [
{
type: 'ToPoint',
to: [1, 1],
@ -199,6 +201,7 @@ const newVar = myVar + 1`
start: 109,
type: 'TagDeclarator',
value: 'myPath',
digest: null,
},
},
{
@ -367,7 +370,7 @@ describe('testing math operators', () => {
const mem = await exe(code)
const sketch = sketchFromKclValue(mem.get('part001'), 'part001')
// 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)
})
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 sketch = sketchFromKclValue(mem.get('part001'), 'part001')
// 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).paths?.[1]?.to).toEqual([6, 0])
expect((sketch as Sketch).value?.[1]?.from).toEqual([3, 4])
expect((sketch as Sketch).value?.[1]?.to).toEqual([6, 0])
const removedUnaryExp = code.replace(
`-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
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 () => {
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', () => {
const result = findUniqueName(
JSON.stringify([
{ type: 'Identifier', name: 'yo01', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo02', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo03', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo04', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo05', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo06', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo07', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo08', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo09', start: 0, end: 0 },
{ type: 'Identifier', name: 'yo01', start: 0, end: 0, digest: null },
{ type: 'Identifier', name: 'yo02', start: 0, end: 0, digest: null },
{ type: 'Identifier', name: 'yo03', start: 0, end: 0, digest: null },
{ type: 'Identifier', name: 'yo04', start: 0, end: 0, digest: null },
{ type: 'Identifier', name: 'yo05', start: 0, end: 0, digest: null },
{ type: 'Identifier', name: 'yo06', start: 0, end: 0, digest: null },
{ type: 'Identifier', name: 'yo07', start: 0, end: 0, digest: null },
{ type: 'Identifier', name: 'yo08', start: 0, end: 0, digest: null },
{ type: 'Identifier', name: 'yo09', start: 0, end: 0, digest: null },
] satisfies Identifier[]),
'yo',
2
@ -123,7 +123,8 @@ describe('Testing addSketchTo', () => {
body: [],
start: 0,
end: 0,
nonCodeMeta: { nonCodeNodes: {}, start: [] },
nonCodeMeta: { nonCodeNodes: {}, start: [], digest: null },
digest: null,
},
'yz'
)

View File

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

View File

@ -41,7 +41,7 @@ beforeAll(async () => {
},
})
})
}, 30_000)
}, 20_000)
afterAll(() => {
engineCommandManager.tearDown()
@ -620,7 +620,7 @@ describe('Testing button states', () => {
it('should return true when body exists and segment is selected', async () => {
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)
})
})

View File

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

View File

@ -28,7 +28,6 @@ import {
getConstraintType,
} from './std/sketchcombos'
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.
@ -121,12 +120,7 @@ export function getNodeFromPathCurry(
}
function moreNodePathFromSourceRange(
node:
| Expr
| ImportStatement
| ExpressionStatement
| VariableDeclaration
| ReturnStatement,
node: Expr | ExpressionStatement | VariableDeclaration | ReturnStatement,
sourceRange: Selection['range'],
previousPath: PathToNode = [['body', '']]
): PathToNode {
@ -717,7 +711,7 @@ export function isLinesParallelAndConstrained(
constraintType === 'angle' || constraintLevel === 'full'
// get the previous segment
const prevSegment = sg.paths[secondaryIndex - 1]
const prevSegment = sg.value[secondaryIndex - 1]
const prevSourceRange = prevSegment.__geoMeta.sourceRange
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

@ -28,7 +28,6 @@ import {
} from 'lib/constants'
import { KclManager } from 'lang/KclSingleton'
import { reportRejection } from 'lib/trap'
import { MachineManager } from 'components/MachineManagerProvider'
// TODO(paultag): This ought to be tweakable.
const pingIntervalMs = 5_000
@ -51,11 +50,6 @@ export enum ExportIntent {
Make = 'make',
}
export interface ExportInfo {
intent: ExportIntent
name: string
}
type ClientMetrics = Models['ClientMetrics_type']
interface WebRTCClientMetrics extends ClientMetrics {
@ -1360,7 +1354,7 @@ export class EngineCommandManager extends EventTarget {
* export in progress. Otherwise it is an enum value of the intent.
* 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 = () => {}
subscriptions: {
@ -1416,15 +1410,12 @@ export class EngineCommandManager extends EventTarget {
(() => {}) as any
kclManager: null | KclManager = null
// The current "manufacturing machine" aka 3D printer, CNC, etc.
public machineManager: MachineManager | null = null
set exportInfo(info: ExportInfo | null) {
this._exportInfo = info
set exportIntent(intent: ExportIntent | null) {
this._exportIntent = intent
}
get exportInfo() {
return this._exportInfo
get exportIntent() {
return this._exportIntent
}
start({
@ -1616,7 +1607,7 @@ export class EngineCommandManager extends EventTarget {
// because in all other cases we send JSON strings. But in the case of
// export we send a binary blob.
// Pass this to our export function.
if (this.exportInfo === null || this.pendingExport === undefined) {
if (this.exportIntent === null || this.pendingExport === undefined) {
toast.error(
'Export intent was not set, but export data was received'
)
@ -1626,7 +1617,7 @@ export class EngineCommandManager extends EventTarget {
return
}
switch (this.exportInfo.intent) {
switch (this.exportIntent) {
case ExportIntent.Save: {
exportSave(event.data, this.pendingExport.toastId).then(() => {
this.pendingExport?.resolve(null)
@ -1634,28 +1625,21 @@ export class EngineCommandManager extends EventTarget {
break
}
case ExportIntent.Make: {
if (!this.machineManager) {
console.warn('Some how, no manufacturing machine is selected.')
break
}
exportMake(
event.data,
this.exportInfo.name,
this.pendingExport.toastId,
this.machineManager
).then((result) => {
if (result) {
this.pendingExport?.resolve(null)
} else {
this.pendingExport?.reject('Failed to make export')
}
}, this.pendingExport?.reject)
exportMake(event.data, this.pendingExport.toastId).then(
(result) => {
if (result) {
this.pendingExport?.resolve(null)
} else {
this.pendingExport?.reject('Failed to make export')
}
},
this.pendingExport?.reject
)
break
}
}
// Set the export intent back to null.
this.exportInfo = null
this.exportIntent = null
return
}
@ -1969,15 +1953,15 @@ export class EngineCommandManager extends EventTarget {
return Promise.resolve(null)
} else if (cmd.type === 'export') {
const promise = new Promise<null>((resolve, reject) => {
if (this.exportInfo === null) {
if (this.exportInfo === null) {
if (this.exportIntent === null) {
if (this.exportIntent === null) {
toast.error('Export intent was not set, but export is being sent')
console.error('Export intent was not set, but export is being sent')
return
}
}
const toastId = toast.loading(
this.exportInfo.intent === ExportIntent.Save
this.exportIntent === ExportIntent.Save
? EXPORT_TOAST_MESSAGES.START
: MAKE_TOAST_MESSAGES.START
)
@ -1991,7 +1975,7 @@ export class EngineCommandManager extends EventTarget {
resolve(passThrough)
},
reject: (reason: string) => {
this.exportInfo = null
this.exportIntent = null
reject(reason)
},
commandId: command.cmd_id,

View File

@ -18,7 +18,7 @@ class FileSystemManager {
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.
if (!isDesktop()) {
return Promise.reject(
@ -28,8 +28,9 @@ class FileSystemManager {
)
}
return this.join(this.dir, path).then((filePath) => {
return window.electron.readFile(filePath)
return this.join(this.dir, path).then(async (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 function getCoordsFromPaths(skGroup: Sketch, index = 0): Coords2d {
const currentPath = skGroup?.paths?.[index]
const currentPath = skGroup?.value?.[index]
if (!currentPath && skGroup?.start) {
return skGroup.start.to
} else if (!currentPath) {
@ -1704,7 +1704,7 @@ export const angledLineThatIntersects: SketchLineHelper = {
varName
)
if (err(sketch)) return sketch
const intersectPath = sketch.paths.find(
const intersectPath = sketch.value.find(
({ tag }: Path) => tag && tag.value === intersectTagName
)
let offset = 0
@ -1823,10 +1823,11 @@ export const updateStartProfileAtArgs: SketchLineHelper['updateArgs'] = ({
start: 0,
end: 0,
body: [],
digest: null,
nonCodeMeta: {
start: [],
nonCodeNodes: [],
digest: null,
},
},
pathToNode,

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@ import { StateMachineCommandSetConfig, KclCommandValue } from 'lib/commandTypes'
import { KCL_DEFAULT_LENGTH, KCL_DEFAULT_DEGREE } from 'lib/constants'
import { components } from 'lib/machine-api'
import { Selections } from 'lib/selections'
import { machineManager } from 'lib/machineManager'
import { modelingMachine, SketchTool } from 'machines/modelingMachine'
type OutputFormat = Models['OutputFormat_type']
@ -186,41 +187,20 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
machine.make_model.model ||
machine.make_model.manufacturer ||
'Unknown Machine',
options: (commandBarContext) => {
return Object.values(
commandBarContext.machineManager?.machines || []
).map((machine: components['schemas']['MachineInfoResponse']) => ({
name:
`${machine.id} (${
options: () => {
return Object.entries(machineManager.machines).map(
([_, machine]) => ({
name: `${machine.id} (${
machine.make_model.model || machine.make_model.manufacturer
}) (${machine.state.state})` +
(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,
disabled: machine.state.state !== 'idle',
value: machine,
}))
}) via ${machineManager.machineApiIp || 'the local network'}`,
isCurrent: false,
value: machine as components['schemas']['MachineInfoResponse'],
})
)
},
defaultValue: (commandBarContext) => {
defaultValue: () => {
return Object.values(
commandBarContext.machineManager.machines || []
machineManager.machines
)[0] as components['schemas']['MachineInfoResponse']
},
},

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