Compare commits

...

42 Commits

Author SHA1 Message Date
4b8ce34b31 Fix release notes (#4295) 2024-10-24 17:07:12 -04:00
6617f72373 Fix docs for how to patch kcmc (#4294) 2024-10-24 20:14:38 +00:00
e9033e1754 Cut release v0.26.1 (#4289) 2024-10-24 14:08:29 -04:00
9b697e30cf Revert "Separate debug/release electron-builder to help mac job (#4270)" (#4293)
This reverts commit 19d01c563e.
2024-10-24 13:07:43 -04:00
a70facdab4 Disable rotate on start new sketch (#4287)
disable rotate on start new sketch

Co-authored-by: Frank Noirot <frank@zoo.dev>
2024-10-24 10:18:41 -04:00
4083f9f3dd Fix release notes in last_download.json (#4247)
* Fix release notes in last_download.json
Fixes #4246

* Fix job output

* Clean up, ready for merge
2024-10-24 09:47:44 -04:00
7ead2bb875 Stop propagation of camera clicks after drags (#4257)
* Update CameraControls.ts

* fix static analyzer error

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)

* Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)"

This reverts commit 0b63016217.

* Don't perform sketch click operations if a camera movement interaction also matches

* Don't `stopPropogation`, make selection listener early return if `wasDragging`
+ consolidate `wasDragging` set statements, add comments

* Codespell

---------

Co-authored-by: 49fl <ircsurfer33@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Frank Noirot <frank@kittycad.io>
Co-authored-by: Frank Noirot <frank@zoo.dev>
2024-10-24 09:26:33 -04:00
19d01c563e Separate debug/release electron-builder to help mac job (#4270)
* Separate debug/release electron-builder to help mac job
Will attempt to fix #4199

* Test BUILD_RELEASE: true

* Revert "Test BUILD_RELEASE: true"

This reverts commit f2c0c24432.
2024-10-24 08:10:16 -04:00
dfe7cfc91c Bump syn from 2.0.82 to 2.0.85 in /src/wasm-lib (#4283)
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.82 to 2.0.85.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.82...2.0.85)

---
updated-dependencies:
- dependency-name: syn
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-24 05:52:18 +00:00
01443e445d Bump kittycad-modeling-cmds from 0.2.68 to 0.2.70 in /src/wasm-lib (#4284)
Bumps [kittycad-modeling-cmds](https://github.com/KittyCAD/modeling-api) from 0.2.68 to 0.2.70.
- [Commits](https://github.com/KittyCAD/modeling-api/compare/kittycad-modeling-cmds-0.2.68...kittycad-modeling-cmds-0.2.70)

---
updated-dependencies:
- dependency-name: kittycad-modeling-cmds
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-24 03:53:07 +00:00
e16eb49f51 Bump serde from 1.0.210 to 1.0.213 in /src/wasm-lib (#4285)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.210 to 1.0.213.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.210...v1.0.213)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-24 03:50:38 +00:00
5d5138e8e6 Bump syn from 2.0.79 to 2.0.82 in /src/wasm-lib (#4252)
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.79 to 2.0.82.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.79...2.0.82)

---
updated-dependencies:
- dependency-name: syn
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-10-23 23:48:18 +00:00
e1d6e29523 Update machine-api spec (#4241)
* YOYO NEW API SPEC!

* New machine-api types

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-10-23 23:41:48 +00:00
49657ad2e5 Bump @types/node from 22.5.0 to 22.7.8 (#4258)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.5.0 to 22.7.8.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-23 15:12:40 -07:00
b40d353994 Bump thiserror from 1.0.64 to 1.0.65 in /src/wasm-lib (#4268)
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.64 to 1.0.65.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.64...1.0.65)

---
updated-dependencies:
- dependency-name: thiserror
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-23 21:47:08 +00:00
62ffa53add Bump proc-macro2 from 1.0.88 to 1.0.89 in /src/wasm-lib (#4266)
Bumps [proc-macro2](https://github.com/dtolnay/proc-macro2) from 1.0.88 to 1.0.89.
- [Release notes](https://github.com/dtolnay/proc-macro2/releases)
- [Commits](https://github.com/dtolnay/proc-macro2/compare/1.0.88...1.0.89)

---
updated-dependencies:
- dependency-name: proc-macro2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-23 21:46:23 +00:00
64dce4d8b1 Bump anyhow from 1.0.89 to 1.0.91 in /src/wasm-lib (#4269)
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.89 to 1.0.91.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.89...1.0.91)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-23 21:41:00 +00:00
02588b2672 Rename Sketch.value to Sketch.paths (#4272)
'value' is not a useful name
2024-10-23 17:42:54 +00:00
3d1ac2ac0b Fix engine connection break when starting onboarding from a fresh install (#4263)
* Make electron test setting overrides not entirely replace default settings

* Add failing test

* Fix test by checking for healthy engine connection before executing demo code

* Fix one electron test that assumed all settings got wiped if you override any.

* 🤷🏻‍♂️ an engine-side camera position in one of the E2E tests changed by 0.01 randomly
2024-10-22 20:22:52 -04:00
ff5ce29fd7 Delete ..env.development.local.swp (#4259) 2024-10-22 18:33:28 -04:00
4bd7e02271 Support negative start and end in ranges (#4249)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-10-22 14:28:48 +13:00
26042790b6 Remove setInterval implementations from camera controls (#4255)
* remove setInterval implementations from camera controls

* fmt
2024-10-21 16:46:47 -07:00
af74f3bb05 Upgrade to rust toolchain 1.82.0 (#4245)
* Upgrade to rust toolchain 1.82.0

* Fix lint about variant being too large

* Fix lint about Err variant being too large
2024-10-21 16:35:04 -05:00
0bdedf5854 fix job name for printers (#4234)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Paul Tagliamonte <paul@zoo.dev>
2024-10-21 21:27:32 +00:00
d2c6b5cf3a Buffer file writes, because writing to file after every keypress has bad consequences (#4242)
* Buffer file writes, because writing to file after every keypress does bad things

* fix: kevin -- added timeouts for the time being since the workflow for saving data to disk is now changed

---------

Co-authored-by: Kevin Nadro <kevin@zoo.dev>
2024-10-21 15:07:20 -05:00
c42967d0e7 Fix auto changelog in make-release.sh (#4197) 2024-10-18 11:57:21 -07:00
cb8fc33adb Change CUT_RELEASE_PR to BUILD_RELEASE for out-yml (#4214)
Change CUT_RELEASE_PR to BUILD_RELEASE
2024-10-18 12:46:12 -04:00
2dc8b429ff Cut release v0.26.0 (#4196) 2024-10-18 09:17:13 -07:00
19ffa220e8 Fix reading files from WebAssembly (#4183) 2024-10-18 14:43:01 +00:00
5332ddd88e Add more Machine API capabilities (#4203) 2024-10-18 10:25:54 -04:00
11d9a2ee00 Fix test settings to actually get used (#4191)
* Fix test settings to actually get used

Co-authored-by: Frank Noirot <frank@zoo.dev>

* Export file size changed out from under us again, relax this test to just be above a reasonable size

* Missed on updated export expectation

* Wrong check, should just be greater than

* Fix E2E test, remove console log

* fmt

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest)

* Sketchy rectangle commit fix

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest)

* Re-run CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)

* Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)"

This reverts commit 2ace7a3b0e.

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)

* Bump timeouts for snapshots

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)

* Re-run CI

---------

Co-authored-by: Frank Noirot <frank@zoo.dev>
Co-authored-by: Frank Noirot <frank@kittycad.io>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-18 08:31:00 +00:00
bfebc41a5c Franknoirot/adhoc/revert-dedupe-commits (#4213)
* Revert "KCL: Fix duplicate 'type' key"

This reverts commit f650281855.

* Revert "Remove duplicate "type" field in the snapshots (#4211)"

This reverts commit 824b4c823e.
2024-10-18 02:16:44 -04:00
824b4c823e Remove duplicate "type" field in the snapshots (#4211)
* Remove duplicate "type" field in the snapshots

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest)

* Confirm

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2024-10-18 01:32:03 -04:00
785002fa4e Live reload on file tree changes and project settings changes (#4142)
* Reload FileTree and File when changed externally

* Added tests

* Make file and project creation a bit more reliable
2024-10-17 23:42:24 -04:00
f650281855 KCL: Fix duplicate 'type' key 2024-10-17 20:37:05 -07:00
9f6999829a Fix broken digest code (#4206)
I broke the typescript bindings in https://github.com/KittyCAD/modeling-app/pull/4193 -- basically the `digest: Option<Digest>` fields previously allowed Typescript to not set a value for the `digest` key at all, but somehow my change made it required in the Typescript.

Fix is to apply `#[ts(optional)]`, see docs at https://docs.rs/ts-rs/latest/ts_rs/trait.TS.html#struct-field-attributes
2024-10-17 20:36:40 -07:00
a14bbaa237 Update machine-api spec (#4205)
* YOYO NEW API SPEC!

* New machine-api types

* empty

* Update .codespellrc

* Update .codespellrc

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-10-17 20:01:34 -07:00
0706624381 KCL: Refactor array-indexing code (#4173)
Neater code, and better error messages.
2024-10-17 16:29:27 -07:00
ef0ae5e06e Add syntax highlighting for comparison operators (#4182)
* Add syntax highlighting for comparison operators

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest)

* Confirm

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-17 23:29:13 +00:00
a010743abb KCL: Skip serializing default values for AST nodes (#4193)
This will make our snapshots and JSON representations easier to read (making our tests less verbose).
2024-10-17 16:22:40 -07:00
057ee479c3 Update machine-api spec (#4201)
* YOYO NEW API SPEC!

* New machine-api types

* empt

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jess Frazelle <github@jessfraz.com>
2024-10-17 16:03:55 -07:00
7218efc489 Fix weird machine api behavior/add status (#4186)
* YOYO NEW API SPEC!

* fies

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add status

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* pass disabled

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* disabled

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add nozzle diameter

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* ypdates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* update types

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* update types

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* update types

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Paul Tagliamonte <paul@zoo.dev>
2024-10-17 15:30:46 -07:00
159 changed files with 3759 additions and 3508 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
skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md,.yarn.lock,**/yarn.lock,./openapi/*.json,./src/lib/machine-api.d.ts

View File

@ -15,6 +15,7 @@ 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 }}
@ -25,7 +26,6 @@ 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,8 +55,6 @@ 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
@ -84,9 +82,6 @@ jobs:
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' }}
run: |
@ -211,7 +206,7 @@ jobs:
out/*-x86_64-linux.*
- uses: actions/upload-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' }}
if: ${{ env.BUILD_RELEASE == 'true' }}
with:
name: out-yml
path: |
@ -262,7 +257,6 @@ 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' }}

File diff suppressed because it is too large Load Diff

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 |
| `value` |`[` [`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 |
| `paths` |`[` [`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 |
| `value` |`[` [`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 |
| `paths` |`[` [`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

@ -313,3 +313,45 @@ test(
await electronApp.close()
}
)
test(
'external change of file contents are reflected in editor',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const PROJECT_DIR_NAME = 'lee-was-here'
const {
electronApp,
page,
dir: projectsDir,
} = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
const aProjectDir = join(dir, PROJECT_DIR_NAME)
await fsp.mkdir(aProjectDir, { recursive: true })
},
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await test.step('Open the project', async () => {
await expect(page.getByText(PROJECT_DIR_NAME)).toBeVisible()
await page.getByText(PROJECT_DIR_NAME).click()
await u.waitForPageLoad()
})
await u.openFilePanel()
await u.openKclCodePanel()
await test.step('Write to file externally and check for changed content', async () => {
const content = 'ha he ho ho ha blap scap be dap'
await fsp.writeFile(
join(projectsDir, PROJECT_DIR_NAME, 'main.kcl'),
content
)
await u.editorTextMatches(content)
})
await electronApp.close()
}
)

View File

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

View File

@ -1,6 +1,16 @@
import { test, expect } from '@playwright/test'
import fsp from 'fs/promises'
import { uuidv4 } from 'lib/utils'
import { getUtils, setup, tearDown } from './test-utils'
import {
darkModeBgColor,
darkModePlaneColorXZ,
executorInputPath,
getUtils,
setup,
setupElectron,
tearDown,
} from './test-utils'
import { join } from 'path'
test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo)
@ -974,4 +984,84 @@ 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,6 +136,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)
await renameFile(fromFile, toFile)
await tronApp.page.reload()
@ -222,9 +225,11 @@ 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',
@ -232,6 +237,9 @@ 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 () => {
@ -960,4 +968,171 @@ _test.describe('Deleting items from the file pane', () => {
'TODO - delete folder we are in, with no main.kcl',
async () => {}
)
// Copied from tests above.
_test(
`external deletion of project navigates back home`,
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const TEST_PROJECT_NAME = 'Test Project'
const {
electronApp,
page,
dir: projectsDirName,
} = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(join(dir, TEST_PROJECT_NAME), { recursive: true })
await fsp.mkdir(join(dir, TEST_PROJECT_NAME, 'folderToDelete'), {
recursive: true,
})
await fsp.copyFile(
executorInputPath('basic_fillet_cube_end.kcl'),
join(dir, TEST_PROJECT_NAME, 'main.kcl')
)
await fsp.copyFile(
executorInputPath('cylinder.kcl'),
join(dir, TEST_PROJECT_NAME, 'folderToDelete', 'someFileWithin.kcl')
)
},
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
// Constants and locators
const projectCard = page.getByText(TEST_PROJECT_NAME)
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
const folderToDelete = page.getByRole('button', {
name: 'folderToDelete',
})
const fileWithinFolder = page.getByRole('listitem').filter({
has: page.getByRole('button', { name: 'someFileWithin.kcl' }),
})
await _test.step(
'Open project and navigate into folderToDelete',
async () => {
await projectCard.click()
await u.waitForPageLoad()
await _expect(projectMenuButton).toContainText('main.kcl')
await u.closeKclCodePanel()
await u.openFilePanel()
await folderToDelete.click()
await _expect(fileWithinFolder).toBeVisible()
await fileWithinFolder.click()
await _expect(projectMenuButton).toContainText('someFileWithin.kcl')
}
)
// Point of divergence. Delete the project folder and see if it goes back
// to the home view.
await _test.step(
'Delete projectsDirName/<project-name> externally',
async () => {
await fsp.rm(join(projectsDirName, TEST_PROJECT_NAME), {
recursive: true,
force: true,
})
}
)
await _test.step('Check the app is back on the home view', async () => {
const projectsDirLink = page.getByText('Loaded from')
await _expect(projectsDirLink).toBeVisible()
})
await electronApp.close()
}
)
// Similar to the above
_test(
`external deletion of file in sub-directory updates the file tree and recreates it on code editor typing`,
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const TEST_PROJECT_NAME = 'Test Project'
const {
electronApp,
page,
dir: projectsDirName,
} = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(join(dir, TEST_PROJECT_NAME), { recursive: true })
await fsp.mkdir(join(dir, TEST_PROJECT_NAME, 'folderToDelete'), {
recursive: true,
})
await fsp.copyFile(
executorInputPath('basic_fillet_cube_end.kcl'),
join(dir, TEST_PROJECT_NAME, 'main.kcl')
)
await fsp.copyFile(
executorInputPath('cylinder.kcl'),
join(dir, TEST_PROJECT_NAME, 'folderToDelete', 'someFileWithin.kcl')
)
},
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
// Constants and locators
const projectCard = page.getByText(TEST_PROJECT_NAME)
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
const folderToDelete = page.getByRole('button', {
name: 'folderToDelete',
})
const fileWithinFolder = page.getByRole('listitem').filter({
has: page.getByRole('button', { name: 'someFileWithin.kcl' }),
})
await _test.step(
'Open project and navigate into folderToDelete',
async () => {
await projectCard.click()
await u.waitForPageLoad()
await _expect(projectMenuButton).toContainText('main.kcl')
await u.openFilePanel()
await folderToDelete.click()
await _expect(fileWithinFolder).toBeVisible()
await fileWithinFolder.click()
await _expect(projectMenuButton).toContainText('someFileWithin.kcl')
}
)
await _test.step(
'Delete projectsDirName/<project-name> externally',
async () => {
await fsp.rm(
join(
projectsDirName,
TEST_PROJECT_NAME,
'folderToDelete',
'someFileWithin.kcl'
)
)
}
)
await _test.step('Check the file is gone in the file tree', async () => {
await _expect(
page.getByTestId('file-pane-scroll-container')
).not.toContainText('someFileWithin.kcl')
})
await _test.step(
'Check the file is back in the file tree after typing in code editor',
async () => {
await u.pasteCodeInEditor('hello = 1')
await _expect(
page.getByTestId('file-pane-scroll-container')
).toContainText('someFileWithin.kcl')
}
)
await electronApp.close()
}
)
})

View File

@ -55,6 +55,53 @@ 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 }
)
.toBe(431341)
.toBeGreaterThan(300_000)
// clean up output.gltf
await fsp.rm('output.gltf')
@ -851,7 +851,7 @@ test(
}
)
test(
test.fixme(
'When the project folder is empty, user can create new project and open it.',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
@ -861,6 +861,12 @@ test(
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()
@ -873,16 +879,7 @@ test(
await page.getByText('project-000').click()
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 u.waitForPageLoad()
await page.locator('.cm-content').fill(`sketch001 = startSketchOn('XZ')
|> startProfileAt([-87.4, 282.92], %)
@ -892,8 +889,28 @@ test(
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(200, sketch001)`)
await page.waitForTimeout(800)
const pointOnModel = { x: 660, y: 250 }
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)
})
// gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color)
@ -901,7 +918,7 @@ extrude001 = extrude(200, sketch001)`)
.poll(() => u.getGreatestPixDiff(pointOnModel, [143, 143, 143]), {
timeout: 10_000,
})
.toBeLessThan(15)
.toBeLessThan(30)
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(300)
await page.waitForTimeout(1000)
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
@ -528,6 +528,7 @@ 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({
@ -895,7 +896,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(1000)
await page.waitForTimeout(2000)
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
@ -939,7 +940,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(1000)
await page.waitForTimeout(2000)
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 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: 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: 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,6 +47,14 @@ 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'
@ -463,6 +471,9 @@ 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()
@ -492,6 +503,11 @@ 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')
@ -872,10 +888,20 @@ export async function setupElectron({
const tempSettingsFilePath = join(projectDirName, SETTINGS_FILE_NAME)
const settingsOverrides = TOML.stringify(
appSettings
? { settings: appSettings }
: {
...TEST_SETTINGS,
? {
settings: {
...TEST_SETTINGS,
...appSettings,
app: {
...TEST_SETTINGS.app,
projectDirectory: projectDirName,
...appSettings.app,
},
},
}
: {
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.54],
position: [0, -23865.37, 11073.53],
target: [0, 0, 0],
},
})

View File

@ -9,7 +9,7 @@ import {
executorInputPath,
} from './test-utils'
import { SaveSettingsPayload, SettingsLevel } from 'lib/settings/settingsTypes'
import { SETTINGS_FILE_NAME } from 'lib/constants'
import { SETTINGS_FILE_NAME, PROJECT_SETTINGS_FILE_NAME } from 'lib/constants'
import {
TEST_SETTINGS_KEY,
TEST_SETTINGS_CORRUPTED,
@ -430,7 +430,6 @@ 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')
})
@ -445,6 +444,58 @@ test.describe('Testing settings', () => {
}
)
test(
'project settings reload on external change',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const {
electronApp,
page,
dir: projectDirName,
} = await setupElectron({
testInfo,
})
await page.setViewportSize({ width: 1200, height: 500 })
const logoLink = page.getByTestId('app-logo')
const projectDirLink = page.getByText('Loaded from')
await test.step('Wait for project view', async () => {
await expect(projectDirLink).toBeVisible()
})
const projectLinks = page.getByTestId('project-link')
const oldCount = await projectLinks.count()
await page.getByRole('button', { name: 'New project' }).click()
await expect(projectLinks).toHaveCount(oldCount + 1)
await projectLinks.filter({ hasText: 'project-000' }).first().click()
const changeColorFs = async (color: string) => {
const tempSettingsFilePath = join(
projectDirName,
'project-000',
PROJECT_SETTINGS_FILE_NAME
)
await fsp.writeFile(
tempSettingsFilePath,
`[settings.app]\nthemeColor = "${color}"`
)
}
await test.step('Check the color is first starting as we expect', async () => {
await expect(logoLink).toHaveCSS('--primary-hue', '264.5')
})
await test.step('Check color of logo changed', async () => {
await changeColorFs('99')
await expect(logoLink).toHaveCSS('--primary-hue', '99')
})
await electronApp.close()
}
)
test(
`Closing settings modal should go back to the original file being viewed`,
{ tag: '@electron' },

7
interface.d.ts vendored
View File

@ -20,10 +20,11 @@ export interface IElectronAPI {
version: typeof process.env.version
watchFileOn: (
path: string,
key: string,
callback: (eventType: string, path: string) => void
) => void
watchFileOff: (path: string) => void
readFile: (path: string) => ReturnType<fs.readFile>
readFile: typeof fs.readFile
watchFileOff: (path: string, key: string) => void
writeFile: (
path: string,
data: string | Uint8Array
@ -67,7 +68,7 @@ export interface IElectronAPI {
}
}
kittycad: (access: string, args: any) => any
listMachines: () => Promise<MachinesListing>
listMachines: (machineApiIp: string) => Promise<MachinesListing>
getMachineApiIp: () => Promise<string | null>
onUpdateDownloadStart: (
callback: (value: { version: string }) => void

View File

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

View File

@ -36,38 +36,319 @@
"description": "Extra machine-specific information regarding a connected machine.",
"oneOf": [
{
"additionalProperties": false,
"properties": {
"Moonraker": {
"type": "object"
"type": {
"enum": [
"moonraker"
],
"type": "string"
}
},
"required": [
"Moonraker"
"type"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"Usb": {
"type": "object"
"type": {
"enum": [
"usb"
],
"type": "string"
}
},
"required": [
"Usb"
"type"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"Bambu": {
"type": "object"
"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"
}
},
"required": [
"Bambu"
"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"
],
"type": "object"
}
@ -85,6 +366,14 @@
"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"
@ -114,6 +403,12 @@
"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": [
{
@ -124,6 +419,7 @@
}
},
"required": [
"hardware_configuration",
"id",
"machine_type",
"make_model",
@ -157,57 +453,111 @@
"oneOf": [
{
"description": "If a print state can not be resolved at this time, an Unknown may be returned.",
"enum": [
"Unknown"
],
"type": "string"
},
{
"description": "Idle, and ready for another job.",
"enum": [
"Idle"
],
"type": "string"
},
{
"description": "Running a job -- 3D printing or CNC-ing a part.",
"enum": [
"Running"
],
"type": "string"
},
{
"description": "Machine is currently offline or unreachable.",
"enum": [
"Offline"
],
"type": "string"
},
{
"description": "Job is underway but halted, waiting for some action to take place.",
"enum": [
"Paused"
],
"type": "string"
},
{
"description": "Job is finished, but waiting manual action to move back to Idle.",
"enum": [
"Complete"
],
"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": {
"Failed": {
"nullable": true,
"state": {
"enum": [
"unknown"
],
"type": "string"
}
},
"required": [
"Failed"
"state"
],
"type": "object"
},
{
"description": "Idle, and ready for another job.",
"properties": {
"state": {
"enum": [
"idle"
],
"type": "string"
}
},
"required": [
"state"
],
"type": "object"
},
{
"description": "Running a job -- 3D printing or CNC-ing a part.",
"properties": {
"state": {
"enum": [
"running"
],
"type": "string"
}
},
"required": [
"state"
],
"type": "object"
},
{
"description": "Machine is currently offline or unreachable.",
"properties": {
"state": {
"enum": [
"offline"
],
"type": "string"
}
},
"required": [
"state"
],
"type": "object"
},
{
"description": "Job is underway but halted, waiting for some action to take place.",
"properties": {
"state": {
"enum": [
"paused"
],
"type": "string"
}
},
"required": [
"state"
],
"type": "object"
},
{
"description": "Job is finished, but waiting manual action to move back to Idle.",
"properties": {
"state": {
"enum": [
"complete"
],
"type": "string"
}
},
"required": [
"state"
],
"type": "object"
},
{
"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.",
"nullable": true,
"type": "string"
},
"state": {
"enum": [
"failed"
],
"type": "string"
}
},
"required": [
"state"
],
"type": "object"
}
@ -219,21 +569,54 @@
{
"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": [
"FusedDeposition"
"fused_deposition"
],
"type": "string"
},
{
"description": "\"Computer numerical control\" - machine that grinds away material from a hunk of material to construct a part.",
"enum": [
"Cnc"
"cnc"
],
"type": "string"
}
]
},
"NozzleDiameter": {
"description": "A nozzle diameter.",
"oneOf": [
{
"description": "0.2mm.",
"enum": [
"0.2"
],
"type": "string"
},
{
"description": "0.4mm.",
"enum": [
"0.4"
],
"type": "string"
},
{
"description": "0.6mm.",
"enum": [
"0.6"
],
"type": "string"
},
{
"description": "0.8mm.",
"enum": [
"0.8"
],
"type": "string"
}
@ -284,6 +667,15 @@
"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": [
@ -292,6 +684,283 @@
],
"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": {

View File

@ -1,6 +1,6 @@
{
"name": "zoo-modeling-app",
"version": "0.25.6",
"version": "0.26.1",
"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.5.0",
"@types/node": "^22.7.8",
"@types/pixelmatch": "^5.2.6",
"@types/pngjs": "^6.0.4",
"@types/react": "^18.3.4",

View File

@ -64,6 +64,27 @@ 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'
@ -71,15 +92,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
zoomDataFromLastFrame?: number = undefined
// holds coordinates, and interaction
moveDataFromLastFrame?: [number, number, string] = undefined
moveSender: CameraRateLimiter = new CameraRateLimiter()
zoomSender: CameraRateLimiter = new CameraRateLimiter()
lastPerspectiveFov: number = 45
pendingZoom: number | null = null
pendingRotation: Vector2 | null = null
@ -171,6 +192,36 @@ 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,
@ -183,6 +234,7 @@ export class CameraControls {
this.target = new Vector3()
this.domElement = domElement
this.isDragging = false
this.wasDragging = false
this.mouseDownPosition = new Vector2()
this.mouseNewPosition = new Vector2()
@ -258,49 +310,6 @@ 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',
@ -356,6 +365,8 @@ 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
@ -385,11 +396,18 @@ 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.moveDataFromLastFrame = [event.clientX, event.clientY, interaction]
this.moveSender.send(() => {
this.doMove(interaction, [event.clientX, event.clientY])
})
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') {
@ -418,6 +436,9 @@ 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()
@ -459,7 +480,9 @@ export class CameraControls {
if (this.syncDirection === 'engineToClient') {
if (interaction === 'zoom') {
this.zoomDataFromLastFrame = event.deltaY
this.zoomSender.send(() => {
this.doZoom(event.deltaY)
})
} else {
// This case will get handled when we add pan and rotate using Apple trackpad.
console.error(

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@ import type { IndexLoaderData } from 'lib/types'
import { PATHS } from 'lib/paths'
import { ActionButton } from './ActionButton'
import Tooltip from './Tooltip'
import { Dispatch, useCallback, useEffect, useRef, useState } from 'react'
import { Dispatch, useCallback, useRef, useState } from 'react'
import { useNavigate, useRouteLoaderData } from 'react-router-dom'
import { Disclosure } from '@headlessui/react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
@ -13,7 +13,6 @@ import { sortProject } from 'lib/desktopFS'
import { FILE_EXT } from 'lib/constants'
import { CustomIcon } from './CustomIcon'
import { codeManager, kclManager } from 'lib/singletons'
import { useDocumentHasFocus } from 'hooks/useDocumentHasFocus'
import { useLspContext } from './LspProvider'
import useHotkeyWrapper from 'lib/hotkeyWrapper'
import { useModelingContext } from 'hooks/useModelingContext'
@ -21,6 +20,8 @@ import { DeleteConfirmationDialog } from './ProjectCard/DeleteProjectDialog'
import { ContextMenu, ContextMenuItem } from './ContextMenu'
import usePlatform from 'hooks/usePlatform'
import { FileEntry } from 'lib/project'
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
import { normalizeLineEndings } from 'lib/codeEditor'
function getIndentationCSS(level: number) {
return `calc(1rem * ${level + 1})`
@ -131,6 +132,23 @@ const FileTreeItem = ({
const isCurrentFile = fileOrDir.path === currentFile?.path
const itemRef = useRef(null)
// Since every file or directory gets its own FileTreeItem, we can do this.
// Because subtrees only render when they are opened, that means this
// only listens when they open. Because this acts like a useEffect, when
// the ReactNodes are destroyed, so is this listener :)
useFileSystemWatcher(
async (eventType, path) => {
// Don't try to read a file that was removed.
if (isCurrentFile && eventType !== 'unlink') {
let code = await window.electron.readFile(path, { encoding: 'utf-8' })
code = normalizeLineEndings(code)
codeManager.updateCodeStateEditor(code)
}
fileSend({ type: 'Refresh' })
},
[fileOrDir.path]
)
const isRenaming = fileContext.itemsBeingRenamed.includes(fileOrDir.path)
const removeCurrentItemFromRenaming = useCallback(
() =>
@ -154,6 +172,13 @@ const FileTreeItem = ({
})
}, [fileContext.itemsBeingRenamed, fileOrDir.path, fileSend])
const clickDirectory = () => {
fileSend({
type: 'Set selected directory',
directory: fileOrDir,
})
}
function handleKeyUp(e: React.KeyboardEvent<HTMLButtonElement>) {
if (e.metaKey && e.key === 'Backspace') {
// Open confirmation dialog
@ -242,18 +267,8 @@ const FileTreeItem = ({
}
style={{ paddingInlineStart: getIndentationCSS(level) }}
onClick={(e) => e.currentTarget.focus()}
onClickCapture={(e) =>
fileSend({
type: 'Set selected directory',
directory: fileOrDir,
})
}
onFocusCapture={(e) =>
fileSend({
type: 'Set selected directory',
directory: fileOrDir,
})
}
onClickCapture={clickDirectory}
onFocusCapture={clickDirectory}
onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()}
onKeyUp={handleKeyUp}
>
@ -469,27 +484,36 @@ export const FileTreeInner = ({
const loaderData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const { send: fileSend, context: fileContext } = useFileContext()
const { send: modelingSend } = useModelingContext()
const documentHasFocus = useDocumentHasFocus()
// Refresh the file tree when the document gets focus
useEffect(() => {
fileSend({ type: 'Refresh' })
}, [documentHasFocus])
// 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(
(x: string | undefined) => x !== undefined
)
)
const clickDirectory = () => {
fileSend({
type: 'Set selected directory',
directory: fileContext.project,
})
}
return (
<div
className="overflow-auto pb-12 absolute inset-0"
data-testid="file-pane-scroll-container"
>
<ul
className="m-0 p-0 text-sm"
onClickCapture={(e) => {
fileSend({
type: 'Set selected directory',
directory: fileContext.project,
})
}}
>
<ul className="m-0 p-0 text-sm" onClickCapture={clickDirectory}>
{sortProject(fileContext.project?.children || []).map((fileOrDir) => (
<FileTreeItem
project={fileContext.project}

View File

@ -69,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 { useNavigate, useSearchParams } from 'react-router-dom'
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
import { getVarNameModal } from 'hooks/useToolbarGuards'
import { err, reportRejection, trap } from 'lib/trap'
@ -84,6 +84,7 @@ 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>
@ -116,6 +117,7 @@ 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(), [])
@ -409,12 +411,15 @@ export const ModelingMachineProvider = ({
Make: ({ event }) => {
if (event.type !== 'Make') return
// Check if we already have an export intent.
if (engineCommandManager.exportIntent) {
if (engineCommandManager.exportInfo) {
toast.error('Already exporting')
return
}
// Set the export intent.
engineCommandManager.exportIntent = ExportIntent.Make
engineCommandManager.exportInfo = {
intent: ExportIntent.Make,
name: file?.name || '',
}
// Set the current machine.
machineManager.currentMachine = event.data.machine
@ -443,12 +448,16 @@ export const ModelingMachineProvider = ({
},
'Engine export': ({ event }) => {
if (event.type !== 'Export') return
if (engineCommandManager.exportIntent) {
if (engineCommandManager.exportInfo) {
toast.error('Already exporting')
return
}
// Set the export intent.
engineCommandManager.exportIntent = ExportIntent.Save
engineCommandManager.exportInfo = {
intent: ExportIntent.Save,
// This never gets used its only for make.
name: '',
}
const format = {
...event.data,
@ -635,6 +644,7 @@ export const ModelingMachineProvider = ({
input.plane
)
await kclManager.updateAst(modifiedAst, false)
sceneInfra.camControls.enableRotate = false
sceneInfra.camControls.syncDirection = 'clientToEngine'
await letEngineAnimateAndSyncCamAfter(

View File

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

View File

@ -11,6 +11,7 @@ export const NetworkMachineIndicator = ({
}) => {
const machineCount = machineManager.machineCount()
const reason = machineManager.noMachinesReason()
const machines = machineManager.machines
return isDesktop() ? (
<Popover className="relative">
@ -46,20 +47,34 @@ export const NetworkMachineIndicator = ({
</div>
{machineCount > 0 && (
<ul className="divide-y divide-chalkboard-20 dark:divide-chalkboard-80">
{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>
{machines.map((machine) => {
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">
Hostname {hostname}
{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>
)
)}
})}
</ul>
)}
</Popover.Panel>

View File

@ -221,6 +221,19 @@ export const SettingsAuthProviderBase = ({
useFileSystemWatcher(
async () => {
// If there is a projectPath but it no longer exists it means
// it was exterally removed. If we let the code past this condition
// execute it will recreate the directory due to code in
// loadAndValidateSettings trying to recreate files. I do not
// wish to change the behavior in case anything else uses it.
// Go home.
if (loadedProject?.project?.path) {
if (!window.electron.exists(loadedProject?.project?.path)) {
navigate(PATHS.HOME)
return
}
}
const data = await loadAndValidateSettings(loadedProject?.project?.path)
settingsSend({
type: 'Set all settings',
@ -228,7 +241,9 @@ export const SettingsAuthProviderBase = ({
doNotPersist: true,
})
},
settingsPath ? [settingsPath] : []
[settingsPath, loadedProject?.project?.path].filter(
(x: string | undefined) => x !== undefined
)
)
// Add settings commands to the command bar

View File

@ -255,10 +255,14 @@ 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

@ -11,7 +11,7 @@ export const kclHighlight = styleTags({
nil: t.null,
'AddOp MultOp ExpOp': t.arithmeticOperator,
BangOp: t.logicOperator,
CompOp: t.logicOperator,
CompOp: t.compareOperator,
'Equals Arrow': t.definitionOperator,
PipeOperator: t.controlOperator,
String: t.string,

View File

@ -90,7 +90,7 @@ commaSep1NoTrailingComma<term> { term ("," term)* }
MultOp { "/" | "*" | "\\" }
ExpOp { "^" }
BangOp { "!" }
CompOp { $[<>] "="? | "!=" | "==" }
CompOp { "==" | "!=" | "<=" | ">=" | "<" | ">" }
Equals { "=" }
Arrow { "=>" }
PipeOperator { "|>" }

View File

@ -12,35 +12,51 @@ type Path = string
// watcher.addListener(() => { ... }).
export const useFileSystemWatcher = (
callback: (path: Path) => Promise<void>,
dependencyArray: Path[]
callback: (eventType: string, path: Path) => Promise<void>,
paths: Path[]
): void => {
// Track a ref to the callback. This is how we get the callback updated
// across the NodeJS<->Browser boundary.
const callbackRef = useRef<{ fn: (path: Path) => Promise<void> }>({
fn: async (_path) => {},
})
// Used to track this instance of useFileSystemWatcher.
// Assign to ref so it doesn't change between renders.
const key = useRef(Math.random().toString())
const [output, setOutput] = useState<
{ eventType: string; path: string } | undefined
>(undefined)
// Used to track if paths list changes.
const [pathsTracked, setPathsTracked] = useState<Path[]>([])
useEffect(() => {
callbackRef.current.fn = callback
}, [callback])
// Used to track if dependencyArrray changes.
const [dependencyArrayTracked, setDependencyArrayTracked] = useState<Path[]>(
[]
)
if (!output) return
callback(output.eventType, output.path).catch(reportRejection)
}, [output])
// On component teardown obliterate all watchers.
useEffect(() => {
// The hook is useless on web.
if (!isDesktop()) return
const cbWatcher = (eventType: string, path: string) => {
setOutput({ eventType, path })
}
for (let path of pathsTracked) {
// Because functions don't retain refs between NodeJS-Browser I need to
// pass an identifying key so we can later remove it.
// A way to think of the function call is:
// "For this path, add a new handler with this key"
// "There can be many keys (functions) per path"
// Again if refs were preserved, we wouldn't need to do this. Keys
// gives us uniqueness.
window.electron.watchFileOn(path, key.current, cbWatcher)
}
return () => {
for (let path of dependencyArray) {
window.electron.watchFileOff(path)
for (let path of pathsTracked) {
window.electron.watchFileOff(path, key.current)
}
}
}, [])
}, [pathsTracked])
function difference<T>(l1: T[], l2: T[]): [T[], T[]] {
return [
@ -49,8 +65,7 @@ export const useFileSystemWatcher = (
]
}
const hasDiff =
difference(dependencyArray, dependencyArrayTracked)[0].length !== 0
const hasDiff = difference(paths, pathsTracked)[0].length !== 0
// Removing 1 watcher at a time is only possible because in a filesystem,
// a path is unique (there can never be two paths with the same name).
@ -61,19 +76,8 @@ export const useFileSystemWatcher = (
if (!hasDiff) return
const [pathsRemoved, pathsRemaining] = difference(
dependencyArrayTracked,
dependencyArray
)
for (let path of pathsRemoved) {
window.electron.watchFileOff(path)
}
const [pathsAdded] = difference(dependencyArray, dependencyArrayTracked)
for (let path of pathsAdded) {
window.electron.watchFileOn(path, (_eventType: string, path: Path) => {
callbackRef.current.fn(path).catch(reportRejection)
})
}
setDependencyArrayTracked(pathsRemaining.concat(pathsAdded))
const [, pathsRemaining] = difference(pathsTracked, paths)
const [pathsAdded] = difference(paths, pathsTracked)
setPathsTracked(pathsRemaining.concat(pathsAdded))
}, [hasDiff])
}

View File

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

View File

@ -18,6 +18,7 @@ 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
constructor() {
if (isDesktop()) {
@ -115,7 +116,11 @@ export default class CodeManager {
async writeToFile() {
if (isDesktop()) {
setTimeout(() => {
// 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.timeoutWriter = setTimeout(() => {
// Wait one event loop to give a chance for params to be set
// Save the file to disk
this._currentFilePath &&
@ -126,7 +131,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?.value
const minusGeo = mem.get('mySketch')?.value?.paths
expect(minusGeo).toEqual([
{
type: 'ToPoint',
@ -73,7 +73,6 @@ const newVar = myVar + 1`
start: 89,
type: 'TagDeclarator',
value: 'myPath',
digest: null,
},
},
{
@ -99,7 +98,6 @@ const newVar = myVar + 1`
start: 143,
type: 'TagDeclarator',
value: 'rightPath',
digest: null,
},
},
])
@ -177,7 +175,7 @@ const newVar = myVar + 1`
info: expect.any(Object),
},
},
value: [
paths: [
{
type: 'ToPoint',
to: [1, 1],
@ -201,7 +199,6 @@ const newVar = myVar + 1`
start: 109,
type: 'TagDeclarator',
value: 'myPath',
digest: null,
},
},
{
@ -370,7 +367,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).value?.[0]?.to?.[1]
const yVal = (sketch as Sketch).paths?.[0]?.to?.[1]
expect(yVal).toBe(-4)
})
it('test that % substitution feeds down CallExp->ArrExp->UnaryExp->CallExp', async () => {
@ -388,8 +385,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).value?.[1]?.from).toEqual([3, 4])
expect((sketch as Sketch).value?.[1]?.to).toEqual([6, 0])
expect((sketch as Sketch).paths?.[1]?.from).toEqual([3, 4])
expect((sketch as Sketch).paths?.[1]?.to).toEqual([6, 0])
const removedUnaryExp = code.replace(
`-legLen(segLen(seg01), myVar)`,
`legLen(segLen(seg01), myVar)`
@ -401,7 +398,7 @@ describe('testing math operators', () => {
)
// without the minus sign, the y value should be 8
expect((removedUnaryExpMemSketch as Sketch).value?.[1]?.to).toEqual([6, 8])
expect((removedUnaryExpMemSketch as Sketch).paths?.[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, 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 },
{ 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 },
] satisfies Identifier[]),
'yo',
2
@ -123,8 +123,7 @@ describe('Testing addSketchTo', () => {
body: [],
start: 0,
end: 0,
nonCodeMeta: { nonCodeNodes: {}, start: [], digest: null },
digest: null,
nonCodeMeta: { nonCodeNodes: {}, start: [] },
},
'yz'
)

View File

@ -241,7 +241,6 @@ export function mutateObjExpProp(
value: updateWith,
start: 0,
end: 0,
digest: null,
})
}
}
@ -579,7 +578,6 @@ export function createLiteral(value: string | number): Literal {
end: 0,
value,
raw: `${value}`,
digest: null,
}
}
@ -588,7 +586,7 @@ export function createTagDeclarator(value: string): TagDeclarator {
type: 'TagDeclarator',
start: 0,
end: 0,
digest: null,
value,
}
}
@ -598,7 +596,7 @@ export function createIdentifier(name: string): Identifier {
type: 'Identifier',
start: 0,
end: 0,
digest: null,
name,
}
}
@ -608,7 +606,6 @@ export function createPipeSubstitution(): PipeSubstitution {
type: 'PipeSubstitution',
start: 0,
end: 0,
digest: null,
}
}
@ -624,12 +621,11 @@ export function createCallExpressionStdLib(
type: 'Identifier',
start: 0,
end: 0,
digest: null,
name,
},
optional: false,
arguments: args,
digest: null,
}
}
@ -645,12 +641,11 @@ export function createCallExpression(
type: 'Identifier',
start: 0,
end: 0,
digest: null,
name,
},
optional: false,
arguments: args,
digest: null,
}
}
@ -661,7 +656,7 @@ export function createArrayExpression(
type: 'ArrayExpression',
start: 0,
end: 0,
digest: null,
nonCodeMeta: nonCodeMetaEmpty(),
elements,
}
@ -674,7 +669,7 @@ export function createPipeExpression(
type: 'PipeExpression',
start: 0,
end: 0,
digest: null,
body,
nonCodeMeta: nonCodeMetaEmpty(),
}
@ -690,13 +685,13 @@ export function createVariableDeclaration(
type: 'VariableDeclaration',
start: 0,
end: 0,
digest: null,
declarations: [
{
type: 'VariableDeclarator',
start: 0,
end: 0,
digest: null,
id: createIdentifier(varName),
init,
},
@ -713,14 +708,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,
})),
}
@ -734,7 +729,7 @@ export function createUnaryExpression(
type: 'UnaryExpression',
start: 0,
end: 0,
digest: null,
operator,
argument,
}
@ -749,7 +744,7 @@ export function createBinaryExpression([left, operator, right]: [
type: 'BinaryExpression',
start: 0,
end: 0,
digest: null,
operator,
left,
right,
@ -1139,5 +1134,5 @@ export async function deleteFromSelection(
}
const nonCodeMetaEmpty = () => {
return { nonCodeNodes: {}, start: [], digest: null }
return { nonCodeNodes: {}, start: [] }
}

View File

@ -41,7 +41,7 @@ beforeAll(async () => {
},
})
})
}, 20_000)
}, 30_000)
afterAll(() => {
engineCommandManager.tearDown()

View File

@ -717,7 +717,7 @@ export function isLinesParallelAndConstrained(
constraintType === 'angle' || constraintLevel === 'full'
// get the previous segment
const prevSegment = sg.value[secondaryIndex - 1]
const prevSegment = sg.paths[secondaryIndex - 1]
const prevSourceRange = prevSegment.__geoMeta.sourceRange
const isParallelAndConstrained =

View File

@ -50,6 +50,11 @@ export enum ExportIntent {
Make = 'make',
}
export interface ExportInfo {
intent: ExportIntent
name: string
}
type ClientMetrics = Models['ClientMetrics_type']
interface WebRTCClientMetrics extends ClientMetrics {
@ -1354,7 +1359,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 _exportIntent: ExportIntent | null = null
private _exportInfo: ExportInfo | null = null
_commandLogCallBack: (command: CommandLog[]) => void = () => {}
subscriptions: {
@ -1410,12 +1415,12 @@ export class EngineCommandManager extends EventTarget {
(() => {}) as any
kclManager: null | KclManager = null
set exportIntent(intent: ExportIntent | null) {
this._exportIntent = intent
set exportInfo(info: ExportInfo | null) {
this._exportInfo = info
}
get exportIntent() {
return this._exportIntent
get exportInfo() {
return this._exportInfo
}
start({
@ -1607,7 +1612,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.exportIntent === null || this.pendingExport === undefined) {
if (this.exportInfo === null || this.pendingExport === undefined) {
toast.error(
'Export intent was not set, but export data was received'
)
@ -1617,7 +1622,7 @@ export class EngineCommandManager extends EventTarget {
return
}
switch (this.exportIntent) {
switch (this.exportInfo.intent) {
case ExportIntent.Save: {
exportSave(event.data, this.pendingExport.toastId).then(() => {
this.pendingExport?.resolve(null)
@ -1625,21 +1630,22 @@ export class EngineCommandManager extends EventTarget {
break
}
case ExportIntent.Make: {
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
)
exportMake(
event.data,
this.exportInfo.name,
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.exportIntent = null
this.exportInfo = null
return
}
@ -1953,15 +1959,15 @@ export class EngineCommandManager extends EventTarget {
return Promise.resolve(null)
} else if (cmd.type === 'export') {
const promise = new Promise<null>((resolve, reject) => {
if (this.exportIntent === null) {
if (this.exportIntent === null) {
if (this.exportInfo === null) {
if (this.exportInfo === 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.exportIntent === ExportIntent.Save
this.exportInfo.intent === ExportIntent.Save
? EXPORT_TOAST_MESSAGES.START
: MAKE_TOAST_MESSAGES.START
)
@ -1975,7 +1981,7 @@ export class EngineCommandManager extends EventTarget {
resolve(passThrough)
},
reject: (reason: string) => {
this.exportIntent = null
this.exportInfo = 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 | void> {
async readFile(path: string): Promise<Uint8Array> {
// Using local file system only works from desktop.
if (!isDesktop()) {
return Promise.reject(

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?.value?.[index]
const currentPath = skGroup?.paths?.[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.value.find(
const intersectPath = sketch.paths.find(
({ tag }: Path) => tag && tag.value === intersectTagName
)
let offset = 0
@ -1823,11 +1823,10 @@ 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['value'][number]
segment: Sketch['paths'][number]
index: number
}
| Error {
@ -39,15 +39,15 @@ export function getSketchSegmentFromSourceRange(
[rangeStart, rangeEnd]: SourceRange
):
| {
segment: Sketch['value'][number]
segment: Sketch['paths'][number]
index: number
}
| Error {
const lineIndex = sketch.value.findIndex(
const lineIndex = sketch.paths.findIndex(
({ __geoMeta: { sourceRange } }: Path) =>
sourceRange[0] <= rangeStart && sourceRange[1] >= rangeEnd
)
const line = sketch.value[lineIndex]
const line = sketch.paths[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.value.find(
referencedSegment = sketch.paths.find(
(path) => path.tag?.value === _referencedSegmentName
)
}

3
src/lib/codeEditor.ts Normal file
View File

@ -0,0 +1,3 @@
export const normalizeLineEndings = (str: string, normalized = '\n') => {
return str.replace(/\r?\n/g, normalized)
}

View File

@ -190,10 +190,31 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
options: () => {
return Object.entries(machineManager.machines).map(
([_, machine]) => ({
name: `${machine.id} (${
machine.make_model.model || machine.make_model.manufacturer
}) via ${machineManager.machineApiIp || 'the local network'}`,
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 as components['schemas']['MachineInfoResponse'],
})
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,6 +14,7 @@ import { codeManager } from 'lib/singletons'
import { fileSystemManager } from 'lang/std/fileSystemManager'
import { getProjectInfo } from './desktop'
import { createSettings } from './settings/initialSettings'
import { normalizeLineEndings } from 'lib/codeEditor'
// The root loader simply resolves the settings and any errors that
// occurred during the settings load
@ -108,7 +109,9 @@ export const fileLoader: LoaderFunction = async (
)
}
code = await window.electron.readFile(currentFilePath)
code = await window.electron.readFile(currentFilePath, {
encoding: 'utf-8',
})
code = normalizeLineEndings(code)
// Update both the state and the editor's code.
@ -182,7 +185,3 @@ export const homeLoader: LoaderFunction = async (): Promise<
}
return {}
}
const normalizeLineEndings = (str: string, normalized = '\n') => {
return str.replace(/\r?\n/g, normalized)
}

View File

@ -37,8 +37,6 @@ if (!process.env.NODE_ENV)
// dotenv override when present
dotenv.config({ path: [`.env.${NODE_ENV}.local`, `.env.${NODE_ENV}`] })
console.log(process.env)
process.env.VITE_KC_API_WS_MODELING_URL ??=
'wss://api.zoo.dev/ws/modeling/commands'
process.env.VITE_KC_API_BASE_URL ??= 'https://api.zoo.dev'
@ -238,6 +236,7 @@ ipcMain.handle('find_machine_api', () => {
const ip = service.addresses[0]
const port = service.port
// We want to return the ip address of the machine API.
console.log(`Machine API found at ${ip}:${port}`)
resolve(`${ip}:${port}`)
}
)

View File

@ -30,22 +30,51 @@ const isMac = os.platform() === 'darwin'
const isWindows = os.platform() === 'win32'
const isLinux = os.platform() === 'linux'
let fsWatchListeners = new Map<string, ReturnType<typeof chokidar.watch>>()
let fsWatchListeners = new Map<
string,
Map<
string,
{
watcher: ReturnType<typeof chokidar.watch>
callback: (eventType: string, path: string) => void
}
>
>()
const watchFileOn = (path: string, callback: (path: string) => void) => {
const watcherMaybe = fsWatchListeners.get(path)
if (watcherMaybe) return
const watcher = chokidar.watch(path)
const watchFileOn = (
path: string,
key: string,
callback: (eventType: string, path: string) => void
) => {
let watchers = fsWatchListeners.get(path)
if (!watchers) {
watchers = new Map()
}
const watcher = chokidar.watch(path, { depth: 1 })
watcher.on('all', callback)
fsWatchListeners.set(path, watcher)
watchers.set(key, { watcher, callback })
fsWatchListeners.set(path, watchers)
}
const watchFileOff = (path: string) => {
const watcher = fsWatchListeners.get(path)
if (!watcher) return
watcher.unwatch(path)
fsWatchListeners.delete(path)
const watchFileOff = (path: string, key: string) => {
const watchers = fsWatchListeners.get(path)
if (!watchers) return
const data = watchers.get(key)
if (!data) {
console.warn(
"Trying to remove a watcher, callback that doesn't exist anymore. Suspicious."
)
return
}
const { watcher, callback } = data
watcher.off('all', callback)
watchers.delete(key)
if (watchers.size === 0) {
fsWatchListeners.delete(path)
} else {
fsWatchListeners.set(path, watchers)
}
}
const readFile = (path: string) => fs.readFile(path, 'utf-8')
const readFile = fs.readFile
// It seems like from the node source code this does not actually block but also
// don't trust me on that (jess).
const exists = (path: string) => fsSync.existsSync(path)
@ -77,11 +106,12 @@ const kittycad = (access: string, args: any) =>
// We could probably do this from the renderer side, but I fear CORS will
// bite our butts.
const listMachines = async (): Promise<MachinesListing> => {
const machineApi = await ipcRenderer.invoke('find_machine_api')
if (!machineApi) return []
return fetch(`http://${machineApi}/machines`).then((resp) => resp.json())
const listMachines = async (
machineApiAddr: string
): Promise<MachinesListing> => {
return fetch(`http://${machineApiAddr}/machines`).then((resp) => {
return resp.json()
})
}
const getMachineApiIp = async (): Promise<String | null> =>

View File

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

116
src/wasm-lib/Cargo.lock generated
View File

@ -121,9 +121,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.89"
version = "1.0.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6"
checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8"
dependencies = [
"backtrace",
]
@ -176,7 +176,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -187,7 +187,7 @@ checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -204,7 +204,7 @@ checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -465,7 +465,7 @@ dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -656,7 +656,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -667,7 +667,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
dependencies = [
"darling_core",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -722,7 +722,7 @@ checksum = "4078275de501a61ceb9e759d37bdd3d7210e654dbc167ac1a3678ef4435ed57b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
"synstructure",
]
@ -751,7 +751,7 @@ dependencies = [
"rustfmt-wrapper",
"serde",
"serde_tokenstream",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -762,7 +762,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -789,7 +789,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -827,7 +827,7 @@ checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -988,7 +988,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -1084,7 +1084,7 @@ dependencies = [
"inflections",
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -1612,7 +1612,7 @@ dependencies = [
"pretty_assertions",
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -1684,9 +1684,9 @@ dependencies = [
[[package]]
name = "kittycad-modeling-cmds"
version = "0.2.68"
version = "0.2.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3aedfcc1d8ea9995ec3eb78a6743c585c9380475c48701797f107489b696aa"
checksum = "b135696d07a4fab928e5abace4dd05f4976eafab5d73e5747a85dc5a684b936c"
dependencies = [
"anyhow",
"chrono",
@ -1716,7 +1716,7 @@ dependencies = [
"kittycad-modeling-cmds-macros-impl",
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -1727,7 +1727,7 @@ checksum = "6607507a8a0e4273b943179f0a3ef8e90712308d1d3095246040c29cfdbf985b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -2112,7 +2112,7 @@ dependencies = [
"regex",
"regex-syntax 0.8.5",
"structmeta",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -2126,7 +2126,7 @@ dependencies = [
"regex",
"regex-syntax 0.8.5",
"structmeta",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -2166,7 +2166,7 @@ dependencies = [
"pest_meta",
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -2224,7 +2224,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -2337,9 +2337,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.88"
version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9"
checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
dependencies = [
"unicode-ident",
]
@ -2391,7 +2391,7 @@ dependencies = [
"proc-macro2",
"pyo3-macros-backend",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -2404,7 +2404,7 @@ dependencies = [
"proc-macro2",
"pyo3-build-config",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -2925,7 +2925,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -2965,9 +2965,9 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
[[package]]
name = "serde"
version = "1.0.210"
version = "1.0.213"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1"
dependencies = [
"serde_derive",
]
@ -2983,13 +2983,13 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.210"
version = "1.0.213"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -3000,14 +3000,14 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
name = "serde_json"
version = "1.0.128"
version = "1.0.132"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
dependencies = [
"indexmap 2.6.0",
"itoa",
@ -3024,7 +3024,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -3045,7 +3045,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -3182,7 +3182,7 @@ dependencies = [
"proc-macro2",
"quote",
"structmeta-derive",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -3193,7 +3193,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -3215,7 +3215,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -3237,9 +3237,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.79"
version = "2.0.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56"
dependencies = [
"proc-macro2",
"quote",
@ -3263,7 +3263,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -3326,22 +3326,22 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.64"
version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.64"
version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -3437,7 +3437,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -3579,7 +3579,7 @@ checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -3607,7 +3607,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -3689,7 +3689,7 @@ checksum = "0ea0b99e8ec44abd6f94a18f28f7934437809dd062820797c52401298116f70e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
"termcolor",
]
@ -3865,7 +3865,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -3927,7 +3927,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
"wasm-bindgen-shared",
]
@ -3962,7 +3962,7 @@ checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@ -4328,7 +4328,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]

View File

@ -72,7 +72,7 @@ members = [
[workspace.dependencies]
http = "1"
kittycad = { version = "0.3.23", default-features = false, features = ["js", "requests"] }
kittycad-modeling-cmds = { version = "0.2.68", features = ["websocket"] }
kittycad-modeling-cmds = { version = "0.2.70", features = ["websocket"] }
[[test]]
name = "executor"
@ -83,6 +83,6 @@ name = "modify"
path = "tests/modify/main.rs"
# Example: how to point modeling-api at a different repo (e.g. a branch or a local clone)
#[patch."https://github.com/KittyCAD/modeling-api"]
#[patch.crates-io]
#kittycad-modeling-cmds = { path = "../../../modeling-api/modeling-cmds" }
#kittycad-modeling-session = { path = "../../../modeling-api/modeling-session" }

View File

@ -18,12 +18,12 @@ once_cell = "1.20.2"
proc-macro2 = "1"
quote = "1"
regex = "1.10"
serde = { version = "1.0.210", features = ["derive"] }
serde = { version = "1.0.213", features = ["derive"] }
serde_tokenstream = "0.2"
syn = { version = "2.0.79", features = ["full"] }
syn = { version = "2.0.85", features = ["full"] }
[dev-dependencies]
anyhow = "1.0.89"
anyhow = "1.0.91"
expectorate = "1.1.0"
pretty_assertions = "1.4.1"
rustfmt-wrapper = "0.2.1"

View File

@ -15,7 +15,7 @@ databake = "0.1.8"
kcl-lib = { path = "../kcl" }
proc-macro2 = "1"
quote = "1"
syn = { version = "2.0.79", features = ["full"] }
syn = { version = "2.0.85", features = ["full"] }
[dev-dependencies]
pretty_assertions = "1.4.1"

View File

@ -12,7 +12,7 @@ fn basic() {
let expected = Program {
start: 0,
end: 11,
body: vec![BodyItem::VariableDeclaration(VariableDeclaration {
body: vec![BodyItem::VariableDeclaration(Box::new(VariableDeclaration {
start: 0,
end: 11,
declarations: vec![VariableDeclarator {
@ -36,7 +36,7 @@ fn basic() {
visibility: ItemVisibility::Default,
kind: VariableKind::Const,
digest: None,
})],
}))],
non_code_meta: NonCodeMeta::default(),
digest: None,
};

View File

@ -6,10 +6,10 @@ edition = "2021"
license = "MIT"
[dependencies]
anyhow = "1.0.89"
anyhow = "1.0.91"
hyper = { version = "0.14.29", features = ["http1", "server", "tcp"] }
kcl-lib = { version = "0.2", path = "../kcl" }
pico-args = "0.5.0"
serde = { version = "1.0.210", features = ["derive"] }
serde = { version = "1.0.213", features = ["derive"] }
serde_json = "1.0.128"
tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread"] }

View File

@ -11,7 +11,7 @@ keywords = ["kcl", "KittyCAD", "CAD"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = { version = "1.0.89", features = ["backtrace"] }
anyhow = { version = "1.0.91", features = ["backtrace"] }
async-recursion = "1.1.1"
async-trait = "0.1.83"
base64 = "0.22.1"
@ -38,11 +38,11 @@ pyo3 = { version = "0.22.5", optional = true }
reqwest = { version = "0.12", default-features = false, features = ["stream", "rustls-tls"] }
ropey = "1.6.1"
schemars = { version = "0.8.17", features = ["impl_json_schema", "url", "uuid1", "preserve_order"] }
serde = { version = "1.0.210", features = ["derive"] }
serde = { version = "1.0.213", features = ["derive"] }
serde_json = "1.0.128"
sha2 = "0.10.8"
tabled = { version = "0.15.0", optional = true }
thiserror = "1.0.64"
thiserror = "1.0.65"
toml = "0.8.19"
ts-rs = { version = "10.0.0", features = ["uuid-impl", "url-impl", "chrono-impl", "no-serde-warnings", "serde-json-impl"] }
url = { version = "2.5.2", features = ["serde"] }

View File

@ -53,8 +53,11 @@ pub struct Program {
pub start: usize,
pub end: usize,
pub body: Vec<BodyItem>,
#[serde(default, skip_serializing_if = "NonCodeMeta::is_empty")]
pub non_code_meta: NonCodeMeta,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -451,7 +454,7 @@ pub(crate) use impl_value_meta;
pub enum BodyItem {
ImportStatement(Box<ImportStatement>),
ExpressionStatement(ExpressionStatement),
VariableDeclaration(VariableDeclaration),
VariableDeclaration(Box<VariableDeclaration>),
ReturnStatement(ReturnStatement),
}
@ -837,6 +840,8 @@ pub struct NonCodeNode {
pub end: usize,
pub value: NonCodeValue,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -981,6 +986,8 @@ pub struct NonCodeMeta {
pub non_code_nodes: HashMap<usize, Vec<NonCodeNode>>,
pub start: Vec<NonCodeNode>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -1053,6 +1060,8 @@ pub struct ImportItem {
pub start: usize,
pub end: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -1105,6 +1114,8 @@ pub struct ImportStatement {
pub path: String,
pub raw_path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -1146,6 +1157,8 @@ pub struct ExpressionStatement {
pub end: usize,
pub expression: Expr,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -1162,6 +1175,8 @@ pub struct CallExpression {
pub arguments: Vec<Expr>,
pub optional: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -1321,6 +1336,8 @@ pub struct VariableDeclaration {
pub visibility: ItemVisibility,
pub kind: VariableKind, // Change to enum if there are specific values
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -1575,6 +1592,8 @@ pub struct VariableDeclarator {
/// The value of the variable.
pub init: Expr,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -1606,6 +1625,8 @@ pub struct Literal {
pub value: LiteralValue,
pub raw: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -1662,6 +1683,8 @@ pub struct Identifier {
pub end: usize,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -1703,6 +1726,8 @@ pub struct TagDeclarator {
#[serde(rename = "value")]
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -1821,6 +1846,8 @@ pub struct PipeSubstitution {
pub start: usize,
pub end: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -1859,6 +1886,8 @@ pub struct ArrayExpression {
#[serde(default, skip_serializing_if = "NonCodeMeta::is_empty")]
pub non_code_meta: NonCodeMeta,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -1934,6 +1963,8 @@ pub struct ArrayRangeExpression {
/// Is the `end_element` included in the range?
pub end_inclusive: bool,
// TODO (maybe) comments on range components?
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -2000,6 +2031,8 @@ pub struct ObjectExpression {
#[serde(default, skip_serializing_if = "NonCodeMeta::is_empty")]
pub non_code_meta: NonCodeMeta,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -2067,6 +2100,8 @@ pub struct ObjectProperty {
pub key: Identifier,
pub value: Expr,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -2197,6 +2232,8 @@ pub struct MemberExpression {
pub property: LiteralIdentifier,
pub computed: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -2256,6 +2293,8 @@ pub struct BinaryExpression {
pub left: BinaryPart,
pub right: BinaryPart,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -2438,6 +2477,8 @@ pub struct UnaryExpression {
pub operator: UnaryOperator,
pub argument: BinaryPart,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -2513,8 +2554,11 @@ pub struct PipeExpression {
// TODO: Only the first body expression can be any Value.
// The rest will be CallExpression, and the AST type should reflect this.
pub body: Vec<Expr>,
#[serde(default, skip_serializing_if = "NonCodeMeta::is_empty")]
pub non_code_meta: NonCodeMeta,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -2650,6 +2694,8 @@ pub struct Parameter {
/// Is the parameter optional?
pub optional: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
@ -2665,13 +2711,15 @@ pub struct FunctionExpression {
#[serde(skip)]
pub return_type: Option<FnArgType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}
impl_value_meta!(FunctionExpression);
#[derive(Debug, PartialEq, Clone)]
pub struct RequiredParamAfterOptionalParam(pub Parameter);
pub struct RequiredParamAfterOptionalParam(pub Box<Parameter>);
impl std::fmt::Display for RequiredParamAfterOptionalParam {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
@ -2703,7 +2751,7 @@ impl FunctionExpression {
if param.optional {
found_optional = true;
} else if found_optional {
return Err(RequiredParamAfterOptionalParam(param.clone()));
return Err(RequiredParamAfterOptionalParam(Box::new(param.clone())));
}
}
let boundary = self.params.partition_point(|param| !param.optional);
@ -2751,6 +2799,8 @@ pub struct ReturnStatement {
pub end: usize,
pub argument: Expr,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub digest: Option<Digest>,
}

View File

@ -65,93 +65,9 @@ impl MemberExpression {
}))
}
}
pub fn get_result(&self, exec_state: &mut ExecState) -> Result<KclValue, KclError> {
#[derive(Debug)]
enum Property {
Number(usize),
String(String),
}
impl Property {
fn type_name(&self) -> &'static str {
match self {
Property::Number(_) => "number",
Property::String(_) => "string",
}
}
}
let property_src: SourceRange = self.property.clone().into();
let property_sr = vec![property_src];
let property: Property = match self.property.clone() {
LiteralIdentifier::Identifier(identifier) => {
let name = identifier.name;
if !self.computed {
// Treat the property as a literal
Property::String(name.to_string())
} else {
// Actually evaluate memory to compute the property.
let prop = exec_state.memory.get(&name, property_src)?;
let KclValue::UserVal(prop) = prop else {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: property_sr,
message: format!(
"{name} is not a valid property/index, you can only use a string or int (>= 0) here",
),
}));
};
match prop.value {
JValue::Number(ref num) => {
num
.as_u64()
.and_then(|x| usize::try_from(x).ok())
.map(Property::Number)
.ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
source_ranges: property_sr,
message: format!(
"{name}'s value is not a valid property/index, you can only use a string or int (>= 0) here",
),
})
})?
}
JValue::String(ref x) => Property::String(x.to_owned()),
_ => {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: property_sr,
message: format!(
"{name} is not a valid property/index, you can only use a string to get the property of an object, or an int (>= 0) to get an item in an array",
),
}));
}
}
}
}
LiteralIdentifier::Literal(literal) => {
let value = literal.value.clone();
match value {
LiteralValue::IInteger(x) => {
if let Ok(x) = u64::try_from(x) {
Property::Number(x.try_into().unwrap())
} else {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: property_sr,
message: format!("{x} is not a valid index, indices must be whole numbers >= 0"),
}));
}
}
LiteralValue::String(s) => Property::String(s),
_ => {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: vec![self.into()],
message: "Only strings or ints (>= 0) can be properties/indexes".to_owned(),
}));
}
}
}
};
let property = Property::try_from(self.computed, self.property.clone(), exec_state, self.into())?;
let object = match &self.object {
// TODO: Don't use recursion here, use a loop.
MemberObject::MemberExpression(member_expr) => member_expr.get_result(exec_state)?,
@ -635,13 +551,13 @@ impl ArrayRangeExpression {
.execute_expr(&self.start_element, exec_state, &metadata, StatementKind::Expression)
.await?
.get_json_value()?;
let start = parse_json_number_as_u64(&start, (&*self.start_element).into())?;
let start = parse_json_number_as_i64(&start, (&*self.start_element).into())?;
let metadata = Metadata::from(&*self.end_element);
let end = ctx
.execute_expr(&self.end_element, exec_state, &metadata, StatementKind::Expression)
.await?
.get_json_value()?;
let end = parse_json_number_as_u64(&end, (&*self.end_element).into())?;
let end = parse_json_number_as_i64(&end, (&*self.end_element).into())?;
if end < start {
return Err(KclError::Semantic(KclErrorDetails {
@ -687,9 +603,9 @@ impl ObjectExpression {
}
}
pub fn parse_json_number_as_u64(j: &serde_json::Value, source_range: SourceRange) -> Result<u64, KclError> {
fn parse_json_number_as_i64(j: &serde_json::Value, source_range: SourceRange) -> Result<i64, KclError> {
if let serde_json::Value::Number(n) = &j {
n.as_u64().ok_or_else(|| {
n.as_i64().ok_or_else(|| {
KclError::Syntax(KclErrorDetails {
source_ranges: vec![source_range],
message: format!("Invalid integer: {}", j),
@ -783,3 +699,105 @@ impl IfExpression {
.map(|expr| expr.unwrap())
}
}
#[derive(Debug)]
enum Property {
Number(usize),
String(String),
}
impl Property {
fn try_from(
computed: bool,
value: LiteralIdentifier,
exec_state: &ExecState,
sr: SourceRange,
) -> Result<Self, KclError> {
let property_sr = vec![sr];
let property_src: SourceRange = value.clone().into();
match value {
LiteralIdentifier::Identifier(identifier) => {
let name = identifier.name;
if !computed {
// Treat the property as a literal
Ok(Property::String(name.to_string()))
} else {
// Actually evaluate memory to compute the property.
let prop = exec_state.memory.get(&name, property_src)?;
let KclValue::UserVal(prop) = prop else {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: property_sr,
message: format!(
"{name} is not a valid property/index, you can only use a string or int (>= 0) here",
),
}));
};
jvalue_to_prop(&prop.value, property_sr, &name)
}
}
LiteralIdentifier::Literal(literal) => {
let value = literal.value.clone();
match value {
LiteralValue::IInteger(x) => {
if let Ok(x) = u64::try_from(x) {
Ok(Property::Number(x.try_into().unwrap()))
} else {
Err(KclError::Semantic(KclErrorDetails {
source_ranges: property_sr,
message: format!("{x} is not a valid index, indices must be whole numbers >= 0"),
}))
}
}
LiteralValue::String(s) => Ok(Property::String(s)),
_ => Err(KclError::Semantic(KclErrorDetails {
source_ranges: vec![sr],
message: "Only strings or ints (>= 0) can be properties/indexes".to_owned(),
})),
}
}
}
}
}
fn jvalue_to_prop(value: &JValue, property_sr: Vec<SourceRange>, name: &str) -> Result<Property, KclError> {
let make_err = |message: String| {
Err::<Property, _>(KclError::Semantic(KclErrorDetails {
source_ranges: property_sr,
message,
}))
};
const MUST_BE_POSINT: &str = "indices must be whole positive numbers";
const TRY_INT: &str = "try using the int() function to make this a whole number";
match value {
JValue::Number(ref num) => {
let maybe_uint = num.as_u64().and_then(|x| usize::try_from(x).ok());
if let Some(uint) = maybe_uint {
Ok(Property::Number(uint))
} else if let Some(iint) = num.as_i64() {
make_err(format!("'{iint}' is not a valid index, {MUST_BE_POSINT}"))
} else if let Some(fnum) = num.as_f64() {
if fnum < 0.0 {
make_err(format!("'{fnum}' is not a valid index, {MUST_BE_POSINT}"))
} else if fnum.fract() == 0.0 {
make_err(format!("'{fnum:.1}' is stored as a fractional number but indices must be whole numbers, {TRY_INT}"))
} else {
make_err(format!("'{fnum}' is not a valid index, {MUST_BE_POSINT}, {TRY_INT}"))
}
} else {
make_err(format!("'{num}' is not a valid index, {MUST_BE_POSINT}"))
}
}
JValue::String(ref x) => Ok(Property::String(x.to_owned())),
_ => {
make_err(format!("{name} is not a valid property/index, you can only use a string to get the property of an object, or an int (>= 0) to get an item in an array"))
}
}
}
impl Property {
fn type_name(&self) -> &'static str {
match self {
Property::Number(_) => "number",
Property::String(_) => "string",
}
}
}

View File

@ -1137,10 +1137,10 @@ pub struct TagEngineInfo {
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
pub struct Sketch {
/// The id of the sketch (this will change when the engine's reference to it changes.
/// The id of the sketch (this will change when the engine's reference to it changes).
pub id: uuid::Uuid,
/// The paths in the sketch.
pub value: Vec<Path>,
pub paths: Vec<Path>,
/// What the sketch is on (can be a plane or a face).
pub on: SketchSurface,
/// The starting path.
@ -1215,7 +1215,7 @@ impl Sketch {
/// Get the path most recently sketched.
pub(crate) fn latest_path(&self) -> Option<&Path> {
self.value.last()
self.paths.last()
}
/// The "pen" is an imaginary pen drawing the path.

View File

@ -1342,7 +1342,7 @@ fn declaration_keyword(i: TokenSlice) -> PResult<(VariableKind, Token)> {
}
/// Parse a variable/constant declaration.
fn declaration(i: TokenSlice) -> PResult<VariableDeclaration> {
fn declaration(i: TokenSlice) -> PResult<Box<VariableDeclaration>> {
let (visibility, visibility_token) = opt(terminated(item_visibility, whitespace))
.parse_next(i)?
.map_or((ItemVisibility::Default, None), |pair| (pair.0, Some(pair.1)));
@ -1404,7 +1404,7 @@ fn declaration(i: TokenSlice) -> PResult<VariableDeclaration> {
.map_err(|e| e.cut())?;
let end = val.end();
Ok(VariableDeclaration {
Ok(Box::new(VariableDeclaration {
start,
end,
declarations: vec![VariableDeclarator {
@ -1417,7 +1417,7 @@ fn declaration(i: TokenSlice) -> PResult<VariableDeclaration> {
visibility,
kind,
digest: None,
})
}))
}
impl TryFrom<Token> for Identifier {

View File

@ -13,8 +13,7 @@ expression: actual
"start": 0,
"end": 1,
"value": 1,
"raw": "1",
"digest": null
"raw": "1"
},
"right": {
"type": "Literal",
@ -22,8 +21,6 @@ expression: actual
"start": 4,
"end": 5,
"value": 2,
"raw": "2",
"digest": null
},
"digest": null
"raw": "2"
}
}

View File

@ -13,8 +13,7 @@ expression: actual
"start": 0,
"end": 1,
"value": 1,
"raw": "1",
"digest": null
"raw": "1"
},
"right": {
"type": "Literal",
@ -22,8 +21,6 @@ expression: actual
"start": 2,
"end": 3,
"value": 2,
"raw": "2",
"digest": null
},
"digest": null
"raw": "2"
}
}

View File

@ -13,8 +13,7 @@ expression: actual
"start": 0,
"end": 1,
"value": 1,
"raw": "1",
"digest": null
"raw": "1"
},
"right": {
"type": "Literal",
@ -22,8 +21,6 @@ expression: actual
"start": 3,
"end": 4,
"value": 2,
"raw": "2",
"digest": null
},
"digest": null
"raw": "2"
}
}

View File

@ -13,8 +13,7 @@ expression: actual
"start": 0,
"end": 1,
"value": 1,
"raw": "1",
"digest": null
"raw": "1"
},
"right": {
"type": "BinaryExpression",
@ -28,8 +27,7 @@ expression: actual
"start": 4,
"end": 5,
"value": 2,
"raw": "2",
"digest": null
"raw": "2"
},
"right": {
"type": "Literal",
@ -37,10 +35,7 @@ expression: actual
"start": 8,
"end": 9,
"value": 3,
"raw": "3",
"digest": null
},
"digest": null
},
"digest": null
"raw": "3"
}
}
}

View File

@ -13,8 +13,7 @@ expression: actual
"start": 0,
"end": 1,
"value": 1,
"raw": "1",
"digest": null
"raw": "1"
},
"right": {
"type": "BinaryExpression",
@ -28,8 +27,7 @@ expression: actual
"start": 6,
"end": 7,
"value": 2,
"raw": "2",
"digest": null
"raw": "2"
},
"right": {
"type": "Literal",
@ -37,10 +35,7 @@ expression: actual
"start": 10,
"end": 11,
"value": 3,
"raw": "3",
"digest": null
},
"digest": null
},
"digest": null
"raw": "3"
}
}
}

View File

@ -19,8 +19,7 @@ expression: actual
"start": 0,
"end": 1,
"value": 1,
"raw": "1",
"digest": null
"raw": "1"
},
"right": {
"type": "BinaryExpression",
@ -34,8 +33,7 @@ expression: actual
"start": 6,
"end": 7,
"value": 2,
"raw": "2",
"digest": null
"raw": "2"
},
"right": {
"type": "Literal",
@ -43,12 +41,9 @@ expression: actual
"start": 10,
"end": 11,
"value": 3,
"raw": "3",
"digest": null
},
"digest": null
},
"digest": null
"raw": "3"
}
}
},
"right": {
"type": "Literal",
@ -56,8 +51,6 @@ expression: actual
"start": 16,
"end": 17,
"value": 4,
"raw": "4",
"digest": null
},
"digest": null
"raw": "4"
}
}

View File

@ -13,8 +13,7 @@ expression: actual
"start": 0,
"end": 1,
"value": 1,
"raw": "1",
"digest": null
"raw": "1"
},
"right": {
"type": "BinaryExpression",
@ -34,8 +33,7 @@ expression: actual
"start": 6,
"end": 7,
"value": 2,
"raw": "2",
"digest": null
"raw": "2"
},
"right": {
"type": "Literal",
@ -43,10 +41,8 @@ expression: actual
"start": 10,
"end": 11,
"value": 3,
"raw": "3",
"digest": null
},
"digest": null
"raw": "3"
}
},
"right": {
"type": "Literal",
@ -54,10 +50,7 @@ expression: actual
"start": 16,
"end": 17,
"value": 4,
"raw": "4",
"digest": null
},
"digest": null
},
"digest": null
"raw": "4"
}
}
}

View File

@ -13,8 +13,7 @@ expression: actual
"start": 0,
"end": 1,
"value": 1,
"raw": "1",
"digest": null
"raw": "1"
},
"right": {
"type": "BinaryExpression",
@ -40,8 +39,7 @@ expression: actual
"start": 7,
"end": 8,
"value": 2,
"raw": "2",
"digest": null
"raw": "2"
},
"right": {
"type": "Literal",
@ -49,10 +47,8 @@ expression: actual
"start": 11,
"end": 12,
"value": 3,
"raw": "3",
"digest": null
},
"digest": null
"raw": "3"
}
},
"right": {
"type": "Literal",
@ -60,10 +56,8 @@ expression: actual
"start": 17,
"end": 18,
"value": 4,
"raw": "4",
"digest": null
},
"digest": null
"raw": "4"
}
},
"right": {
"type": "Literal",
@ -71,10 +65,7 @@ expression: actual
"start": 21,
"end": 22,
"value": 5,
"raw": "5",
"digest": null
},
"digest": null
},
"digest": null
"raw": "5"
}
}
}

View File

@ -13,8 +13,7 @@ expression: actual
"start": 0,
"end": 1,
"value": 1,
"raw": "1",
"digest": null
"raw": "1"
},
"right": {
"type": "BinaryExpression",
@ -28,8 +27,7 @@ expression: actual
"start": 8,
"end": 9,
"value": 2,
"raw": "2",
"digest": null
"raw": "2"
},
"right": {
"type": "Literal",
@ -37,10 +35,7 @@ expression: actual
"start": 12,
"end": 13,
"value": 3,
"raw": "3",
"digest": null
},
"digest": null
},
"digest": null
"raw": "3"
}
}
}

View File

@ -30,28 +30,23 @@ expression: actual
"type": "Identifier",
"start": 0,
"end": 8,
"name": "distance",
"digest": null
"name": "distance"
},
"right": {
"type": "Identifier",
"type": "Identifier",
"start": 11,
"end": 12,
"name": "p",
"digest": null
},
"digest": null
"name": "p"
}
},
"right": {
"type": "Identifier",
"type": "Identifier",
"start": 15,
"end": 18,
"name": "FOS",
"digest": null
},
"digest": null
"name": "FOS"
}
},
"right": {
"type": "Literal",
@ -59,10 +54,8 @@ expression: actual
"start": 21,
"end": 22,
"value": 6,
"raw": "6",
"digest": null
},
"digest": null
"raw": "6"
}
},
"right": {
"type": "BinaryExpression",
@ -75,18 +68,14 @@ expression: actual
"type": "Identifier",
"start": 26,
"end": 36,
"name": "sigmaAllow",
"digest": null
"name": "sigmaAllow"
},
"right": {
"type": "Identifier",
"type": "Identifier",
"start": 39,
"end": 44,
"name": "width",
"digest": null
},
"digest": null
},
"digest": null
"name": "width"
}
}
}

View File

@ -13,8 +13,7 @@ expression: actual
"start": 0,
"end": 1,
"value": 2,
"raw": "2",
"digest": null
"raw": "2"
},
"right": {
"type": "Literal",
@ -22,8 +21,6 @@ expression: actual
"start": 7,
"end": 8,
"value": 3,
"raw": "3",
"digest": null
},
"digest": null
"raw": "3"
}
}

View File

@ -20,8 +20,7 @@ expression: actual
"type": "Identifier",
"start": 6,
"end": 15,
"name": "boxSketch",
"digest": null
"name": "boxSketch"
},
"init": {
"type": "PipeExpression",
@ -38,8 +37,7 @@ expression: actual
"type": "Identifier",
"start": 18,
"end": 31,
"name": "startSketchAt",
"digest": null
"name": "startSketchAt"
},
"arguments": [
{
@ -54,8 +52,7 @@ expression: actual
"start": 33,
"end": 34,
"value": 0,
"raw": "0",
"digest": null
"raw": "0"
},
{
"type": "Literal",
@ -63,15 +60,12 @@ expression: actual
"start": 36,
"end": 37,
"value": 0,
"raw": "0",
"digest": null
"raw": "0"
}
],
"digest": null
]
}
],
"optional": false,
"digest": null
"optional": false
},
{
"type": "CallExpression",
@ -82,8 +76,7 @@ expression: actual
"type": "Identifier",
"start": 47,
"end": 51,
"name": "line",
"digest": null
"name": "line"
},
"arguments": [
{
@ -98,8 +91,7 @@ expression: actual
"start": 53,
"end": 54,
"value": 0,
"raw": "0",
"digest": null
"raw": "0"
},
{
"type": "Literal",
@ -107,22 +99,18 @@ expression: actual
"start": 56,
"end": 58,
"value": 10,
"raw": "10",
"digest": null
"raw": "10"
}
],
"digest": null
]
},
{
"type": "PipeSubstitution",
"type": "PipeSubstitution",
"start": 61,
"end": 62,
"digest": null
"end": 62
}
],
"optional": false,
"digest": null
"optional": false
},
{
"type": "CallExpression",
@ -133,8 +121,7 @@ expression: actual
"type": "Identifier",
"start": 71,
"end": 84,
"name": "tangentialArc",
"digest": null
"name": "tangentialArc"
},
"arguments": [
{
@ -155,10 +142,8 @@ expression: actual
"start": 87,
"end": 88,
"value": 5,
"raw": "5",
"digest": null
},
"digest": null
"raw": "5"
}
},
{
"type": "Literal",
@ -166,22 +151,18 @@ expression: actual
"start": 90,
"end": 91,
"value": 5,
"raw": "5",
"digest": null
"raw": "5"
}
],
"digest": null
]
},
{
"type": "PipeSubstitution",
"type": "PipeSubstitution",
"start": 94,
"end": 95,
"digest": null
"end": 95
}
],
"optional": false,
"digest": null
"optional": false
},
{
"type": "CallExpression",
@ -192,8 +173,7 @@ expression: actual
"type": "Identifier",
"start": 104,
"end": 108,
"name": "line",
"digest": null
"name": "line"
},
"arguments": [
{
@ -208,8 +188,7 @@ expression: actual
"start": 110,
"end": 111,
"value": 5,
"raw": "5",
"digest": null
"raw": "5"
},
{
"type": "UnaryExpression",
@ -223,24 +202,19 @@ expression: actual
"start": 114,
"end": 116,
"value": 15,
"raw": "15",
"digest": null
},
"digest": null
"raw": "15"
}
}
],
"digest": null
]
},
{
"type": "PipeSubstitution",
"type": "PipeSubstitution",
"start": 119,
"end": 120,
"digest": null
"end": 120
}
],
"optional": false,
"digest": null
"optional": false
},
{
"type": "CallExpression",
@ -251,8 +225,7 @@ expression: actual
"type": "Identifier",
"start": 129,
"end": 136,
"name": "extrude",
"digest": null
"name": "extrude"
},
"arguments": [
{
@ -261,39 +234,22 @@ expression: actual
"start": 137,
"end": 139,
"value": 10,
"raw": "10",
"digest": null
"raw": "10"
},
{
"type": "PipeSubstitution",
"type": "PipeSubstitution",
"start": 141,
"end": 142,
"digest": null
"end": 142
}
],
"optional": false,
"digest": null
"optional": false
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
},
"digest": null
]
}
}
],
"kind": "const",
"digest": null
"kind": "const"
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
]
}

View File

@ -20,8 +20,7 @@ expression: actual
"type": "Identifier",
"start": 6,
"end": 8,
"name": "sg",
"digest": null
"name": "sg"
},
"init": {
"type": "UnaryExpression",
@ -34,22 +33,12 @@ expression: actual
"type": "Identifier",
"start": 12,
"end": 17,
"name": "scale",
"digest": null
},
"digest": null
},
"digest": null
"name": "scale"
}
}
}
],
"kind": "const",
"digest": null
"kind": "const"
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
]
}

View File

@ -20,8 +20,7 @@ expression: actual
"type": "Identifier",
"start": 0,
"end": 6,
"name": "lineTo",
"digest": null
"name": "lineTo"
},
"arguments": [
{
@ -38,8 +37,7 @@ expression: actual
"type": "Identifier",
"start": 9,
"end": 11,
"name": "to",
"digest": null
"name": "to"
},
"value": {
"type": "ArrayExpression",
@ -53,8 +51,7 @@ expression: actual
"start": 14,
"end": 15,
"value": 0,
"raw": "0",
"digest": null
"raw": "0"
},
{
"type": "UnaryExpression",
@ -68,30 +65,17 @@ expression: actual
"start": 18,
"end": 19,
"value": 1,
"raw": "1",
"digest": null
},
"digest": null
"raw": "1"
}
}
],
"digest": null
},
"digest": null
]
}
}
],
"digest": null
]
}
],
"optional": false,
"digest": null
},
"digest": null
"optional": false
}
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
]
}

View File

@ -20,8 +20,7 @@ expression: actual
"type": "Identifier",
"start": 6,
"end": 13,
"name": "myArray",
"digest": null
"name": "myArray"
},
"init": {
"type": "ArrayRangeExpression",
@ -34,8 +33,7 @@ expression: actual
"start": 17,
"end": 18,
"value": 0,
"raw": "0",
"digest": null
"raw": "0"
},
"endElement": {
"type": "Literal",
@ -43,23 +41,13 @@ expression: actual
"start": 20,
"end": 22,
"value": 10,
"raw": "10",
"digest": null
"raw": "10"
},
"endInclusive": true,
"digest": null
},
"digest": null
"endInclusive": true
}
}
],
"kind": "const",
"digest": null
"kind": "const"
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
]
}

View File

@ -20,8 +20,7 @@ expression: actual
"type": "Identifier",
"start": 8,
"end": 24,
"name": "firstPrimeNumber",
"digest": null
"name": "firstPrimeNumber"
},
"init": {
"type": "FunctionExpression",
@ -44,26 +43,15 @@ expression: actual
"start": 50,
"end": 51,
"value": 2,
"raw": "2",
"digest": null
},
"digest": null
"raw": "2"
}
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
},
"digest": null
},
"digest": null
]
}
}
}
],
"kind": "fn",
"digest": null
"kind": "fn"
},
{
"type": "ExpressionStatement",
@ -79,20 +67,11 @@ expression: actual
"type": "Identifier",
"start": 62,
"end": 78,
"name": "firstPrimeNumber",
"digest": null
"name": "firstPrimeNumber"
},
"arguments": [],
"optional": false,
"digest": null
},
"digest": null
"optional": false
}
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
]
}

View File

@ -20,8 +20,7 @@ expression: actual
"type": "Identifier",
"start": 3,
"end": 8,
"name": "thing",
"digest": null
"name": "thing"
},
"init": {
"type": "FunctionExpression",
@ -35,11 +34,9 @@ expression: actual
"type": "Identifier",
"start": 12,
"end": 17,
"name": "param",
"digest": null
"name": "param"
},
"optional": false,
"digest": null
"optional": false
}
],
"body": {
@ -57,26 +54,15 @@ expression: actual
"start": 39,
"end": 43,
"value": true,
"raw": "true",
"digest": null
},
"digest": null
"raw": "true"
}
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
},
"digest": null
},
"digest": null
]
}
}
}
],
"kind": "fn",
"digest": null
"kind": "fn"
},
{
"type": "ExpressionStatement",
@ -92,8 +78,7 @@ expression: actual
"type": "Identifier",
"start": 54,
"end": 59,
"name": "thing",
"digest": null
"name": "thing"
},
"arguments": [
{
@ -102,20 +87,11 @@ expression: actual
"start": 60,
"end": 65,
"value": false,
"raw": "false",
"digest": null
"raw": "false"
}
],
"optional": false,
"digest": null
},
"digest": null
"optional": false
}
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
]
}

View File

@ -20,8 +20,7 @@ expression: actual
"type": "Identifier",
"start": 6,
"end": 14,
"name": "mySketch",
"digest": null
"name": "mySketch"
},
"init": {
"type": "PipeExpression",
@ -38,8 +37,7 @@ expression: actual
"type": "Identifier",
"start": 17,
"end": 30,
"name": "startSketchAt",
"digest": null
"name": "startSketchAt"
},
"arguments": [
{
@ -54,8 +52,7 @@ expression: actual
"start": 32,
"end": 33,
"value": 0,
"raw": "0",
"digest": null
"raw": "0"
},
{
"type": "Literal",
@ -63,15 +60,12 @@ expression: actual
"start": 34,
"end": 35,
"value": 0,
"raw": "0",
"digest": null
"raw": "0"
}
],
"digest": null
]
}
],
"optional": false,
"digest": null
"optional": false
},
{
"type": "CallExpression",
@ -82,8 +76,7 @@ expression: actual
"type": "Identifier",
"start": 49,
"end": 55,
"name": "lineTo",
"digest": null
"name": "lineTo"
},
"arguments": [
{
@ -98,8 +91,7 @@ expression: actual
"start": 57,
"end": 58,
"value": 0,
"raw": "0",
"digest": null
"raw": "0"
},
{
"type": "Literal",
@ -107,30 +99,25 @@ expression: actual
"start": 60,
"end": 61,
"value": 1,
"raw": "1",
"digest": null
"raw": "1"
}
],
"digest": null
]
},
{
"type": "PipeSubstitution",
"type": "PipeSubstitution",
"start": 64,
"end": 65,
"digest": null
"end": 65
},
{
"type": "TagDeclarator",
"type": "TagDeclarator",
"start": 67,
"end": 74,
"value": "myPath",
"digest": null
"value": "myPath"
}
],
"optional": false,
"digest": null
"optional": false
},
{
"type": "CallExpression",
@ -141,8 +128,7 @@ expression: actual
"type": "Identifier",
"start": 87,
"end": 93,
"name": "lineTo",
"digest": null
"name": "lineTo"
},
"arguments": [
{
@ -157,8 +143,7 @@ expression: actual
"start": 95,
"end": 96,
"value": 1,
"raw": "1",
"digest": null
"raw": "1"
},
{
"type": "Literal",
@ -166,22 +151,18 @@ expression: actual
"start": 98,
"end": 99,
"value": 1,
"raw": "1",
"digest": null
"raw": "1"
}
],
"digest": null
]
},
{
"type": "PipeSubstitution",
"type": "PipeSubstitution",
"start": 102,
"end": 103,
"digest": null
"end": 103
}
],
"optional": false,
"digest": null
"optional": false
},
{
"type": "CallExpression",
@ -192,8 +173,7 @@ expression: actual
"type": "Identifier",
"start": 116,
"end": 122,
"name": "lineTo",
"digest": null
"name": "lineTo"
},
"arguments": [
{
@ -208,8 +188,7 @@ expression: actual
"start": 124,
"end": 125,
"value": 1,
"raw": "1",
"digest": null
"raw": "1"
},
{
"type": "Literal",
@ -217,30 +196,25 @@ expression: actual
"start": 127,
"end": 128,
"value": 0,
"raw": "0",
"digest": null
"raw": "0"
}
],
"digest": null
]
},
{
"type": "PipeSubstitution",
"type": "PipeSubstitution",
"start": 131,
"end": 132,
"digest": null
"end": 132
},
{
"type": "TagDeclarator",
"type": "TagDeclarator",
"start": 134,
"end": 144,
"value": "rightPath",
"digest": null
"value": "rightPath"
}
],
"optional": false,
"digest": null
"optional": false
},
{
"type": "CallExpression",
@ -251,40 +225,23 @@ expression: actual
"type": "Identifier",
"start": 157,
"end": 162,
"name": "close",
"digest": null
"name": "close"
},
"arguments": [
{
"type": "PipeSubstitution",
"type": "PipeSubstitution",
"start": 163,
"end": 164,
"digest": null
"end": 164
}
],
"optional": false,
"digest": null
"optional": false
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
},
"digest": null
]
}
}
],
"kind": "const",
"digest": null
"kind": "const"
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
]
}

View File

@ -20,8 +20,7 @@ expression: actual
"type": "Identifier",
"start": 6,
"end": 14,
"name": "mySketch",
"digest": null
"name": "mySketch"
},
"init": {
"type": "PipeExpression",
@ -38,8 +37,7 @@ expression: actual
"type": "Identifier",
"start": 17,
"end": 30,
"name": "startSketchAt",
"digest": null
"name": "startSketchAt"
},
"arguments": [
{
@ -54,8 +52,7 @@ expression: actual
"start": 32,
"end": 33,
"value": 0,
"raw": "0",
"digest": null
"raw": "0"
},
{
"type": "Literal",
@ -63,15 +60,12 @@ expression: actual
"start": 34,
"end": 35,
"value": 0,
"raw": "0",
"digest": null
"raw": "0"
}
],
"digest": null
]
}
],
"optional": false,
"digest": null
"optional": false
},
{
"type": "CallExpression",
@ -82,8 +76,7 @@ expression: actual
"type": "Identifier",
"start": 41,
"end": 47,
"name": "lineTo",
"digest": null
"name": "lineTo"
},
"arguments": [
{
@ -98,8 +91,7 @@ expression: actual
"start": 49,
"end": 50,
"value": 1,
"raw": "1",
"digest": null
"raw": "1"
},
{
"type": "Literal",
@ -107,22 +99,18 @@ expression: actual
"start": 52,
"end": 53,
"value": 1,
"raw": "1",
"digest": null
"raw": "1"
}
],
"digest": null
]
},
{
"type": "PipeSubstitution",
"type": "PipeSubstitution",
"start": 56,
"end": 57,
"digest": null
"end": 57
}
],
"optional": false,
"digest": null
"optional": false
},
{
"type": "CallExpression",
@ -133,40 +121,23 @@ expression: actual
"type": "Identifier",
"start": 62,
"end": 67,
"name": "close",
"digest": null
"name": "close"
},
"arguments": [
{
"type": "PipeSubstitution",
"type": "PipeSubstitution",
"start": 68,
"end": 69,
"digest": null
"end": 69
}
],
"optional": false,
"digest": null
"optional": false
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
},
"digest": null
]
}
}
],
"kind": "const",
"digest": null
"kind": "const"
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
]
}

View File

@ -20,8 +20,7 @@ expression: actual
"type": "Identifier",
"start": 6,
"end": 11,
"name": "myBox",
"digest": null
"name": "myBox"
},
"init": {
"type": "CallExpression",
@ -32,8 +31,7 @@ expression: actual
"type": "Identifier",
"start": 14,
"end": 27,
"name": "startSketchAt",
"digest": null
"name": "startSketchAt"
},
"arguments": [
{
@ -41,24 +39,14 @@ expression: actual
"type": "Identifier",
"start": 28,
"end": 29,
"name": "p",
"digest": null
"name": "p"
}
],
"optional": false,
"digest": null
},
"digest": null
"optional": false
}
}
],
"kind": "const",
"digest": null
"kind": "const"
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
]
}

View File

@ -20,8 +20,7 @@ expression: actual
"type": "Identifier",
"start": 6,
"end": 11,
"name": "myBox",
"digest": null
"name": "myBox"
},
"init": {
"type": "PipeExpression",
@ -38,8 +37,7 @@ expression: actual
"type": "Identifier",
"start": 14,
"end": 15,
"name": "f",
"digest": null
"name": "f"
},
"arguments": [
{
@ -48,12 +46,10 @@ expression: actual
"start": 16,
"end": 17,
"value": 1,
"raw": "1",
"digest": null
"raw": "1"
}
],
"optional": false,
"digest": null
"optional": false
},
{
"type": "CallExpression",
@ -64,8 +60,7 @@ expression: actual
"type": "Identifier",
"start": 22,
"end": 23,
"name": "g",
"digest": null
"name": "g"
},
"arguments": [
{
@ -74,39 +69,22 @@ expression: actual
"start": 24,
"end": 25,
"value": 2,
"raw": "2",
"digest": null
"raw": "2"
},
{
"type": "PipeSubstitution",
"type": "PipeSubstitution",
"start": 27,
"end": 28,
"digest": null
"end": 28
}
],
"optional": false,
"digest": null
"optional": false
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
},
"digest": null
]
}
}
],
"kind": "const",
"digest": null
"kind": "const"
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
]
}

View File

@ -20,8 +20,7 @@ expression: actual
"type": "Identifier",
"start": 6,
"end": 11,
"name": "myBox",
"digest": null
"name": "myBox"
},
"init": {
"type": "PipeExpression",
@ -38,8 +37,7 @@ expression: actual
"type": "Identifier",
"start": 14,
"end": 27,
"name": "startSketchAt",
"digest": null
"name": "startSketchAt"
},
"arguments": [
{
@ -47,12 +45,10 @@ expression: actual
"type": "Identifier",
"start": 28,
"end": 29,
"name": "p",
"digest": null
"name": "p"
}
],
"optional": false,
"digest": null
"optional": false
},
{
"type": "CallExpression",
@ -63,8 +59,7 @@ expression: actual
"type": "Identifier",
"start": 34,
"end": 38,
"name": "line",
"digest": null
"name": "line"
},
"arguments": [
{
@ -79,50 +74,31 @@ expression: actual
"start": 40,
"end": 41,
"value": 0,
"raw": "0",
"digest": null
"raw": "0"
},
{
"type": "Identifier",
"type": "Identifier",
"start": 43,
"end": 44,
"name": "l",
"digest": null
"name": "l"
}
],
"digest": null
]
},
{
"type": "PipeSubstitution",
"type": "PipeSubstitution",
"start": 47,
"end": 48,
"digest": null
"end": 48
}
],
"optional": false,
"digest": null
"optional": false
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
},
"digest": null
]
}
}
],
"kind": "const",
"digest": null
"kind": "const"
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
]
}

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