Compare commits

..

47 Commits

Author SHA1 Message Date
7c935741e4 Update snapshots from the upload artifact run 2025-02-05 16:36:14 -05:00
87e299e0bb Merge branch 'main' into pierremtb/make-snapshot-bot-upload-instead-of-commit 2025-02-05 16:02:18 -05:00
465e71c12f WIP 2025-02-05 15:54:47 -05:00
df86c93a04 Restrict snapshot bot to non-main branches and to the snapshot dir (#5266)
* Disable snapshot commit bot on main

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Restrict snapshot bot git add dir

* Clean up sanps

* Other git add .

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Clean up after bot (bad bot)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Remove -a from -am

* Clean up

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-05 15:44:36 -05:00
824669a1c2 WIP 2025-02-05 15:41:08 -05:00
ba8f8a1722 WIP 2025-02-05 15:30:41 -05:00
f4a4e6c5be Upload only the changes 2025-02-05 15:18:51 -05:00
0d148e80aa Clean up 2025-02-05 15:01:03 -05:00
3300993ac8 Separate snapshot from flow tests 2025-02-05 14:53:54 -05:00
033eaed32e Make snapshot bot upload the changes instead of commit 2025-02-05 14:33:05 -05:00
8aabac0be7 Update types.md with keyword args data (#5270)
A few issues:

- There was no description of how `|>` works
- Need to explain our keyword arguments implementation
- It was using old syntax for `angledLine` which now takes an object as its first parameter, not an array
2025-02-05 13:03:28 -06:00
138728a95d Move Helix button to a section with offset plane (3d 'construction' elements) (#5235)
* Move Helix button to a section with offset plane (3d 'construction' elements)
Fixes #5234

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-05 09:59:58 -05:00
9a92e7d642 Faster local playwright electron test scripts (#5242) 2025-02-05 09:51:23 -05:00
efedc8de58 Point snapshot bot tokens to the right ones (#5265)
Add prefix to secrets for create-github-app-token
2025-02-05 09:46:46 -05:00
f7ee248a26 Fix to use more accurate types with custom isArray() and add lint (#5261)
* Fix to use more accurate types with custom isArray()

* Add lint against Array.isArray()
2025-02-05 09:01:45 -05:00
336f4f27ba Release new kcl-lib and derive-docs (#5259)
* Release new kcl-lib and derive-docs

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-05 08:53:50 -05:00
e1f128d64a Refactor execution module (#5162)
* cargo update, etc

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Refactor execution/mod.rs (code motion)

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Refactor caching out of ExecutorContext plus some tidying up

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Move caching logic to inside execution

Signed-off-by: Nick Cameron <nrc@ncameron.org>

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-02-05 17:53:49 +13:00
f858a611f1 Bump vitest to 1.6.1 and 2.1.9 (#5257)
* Bump vitest to 1.6.1 and 2.1.9

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* bad bot

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Pierre Jacquier <pierre@zoo.dev>
Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
2025-02-05 02:56:48 +00:00
6ce270c0d0 Fix artifact graph to survive when there's an execution error (#5246)
This also changes artifact graph errors to point to the KCL that the
command originated from.
2025-02-05 01:34:56 +00:00
30ac0e4f48 Add a different class for highlights in dark mode (#5244)
That was dumb easy, we should remember how easy this is when we come
back through and make better redesign choices.
2025-02-05 00:42:57 +00:00
8f90c352fe Add 'Clone' feature to file tree (#5232)
* Add 'Clone' file / folder feature to file tree

* E2E Test: clone file in file tree

* Don't stat if there's no target
2025-02-04 18:13:59 -05:00
bc6f0fceca Allow snapshot bot to trigger CI (#5253)
* WIP: allow snapshot bot to trigger CI

* To revert: change toolbars button order to trigger snapshots

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Revert "To revert: change toolbars button order to trigger snapshots"

This reverts commit d6e2550921.

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Clean up identical snaps

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* bad bot

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-04 17:33:34 -05:00
5c830a4ed4 Add skipWin to a large set of playwright tests (#5254)
* WIP: disable more tests

* Fix lint and add 2 windows skips

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Two more skips on win

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* More skips

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* More skips

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* More skips

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* One more skip

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* More skips

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Replace many win32 skips with tag @skipWin

* More clean upp

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-04 16:49:49 -05:00
8a9d50226f Disable text-to-cad tests post kw arg merge (#5252) 2025-02-04 12:21:03 -05:00
8397405998 KCL: Use keyword arguments for line, lineTo, extrude and close (#5249)
Part of #4600.

PR: https://github.com/KittyCAD/modeling-app/pull/4826

# Changes to KCL stdlib

- `line(point, sketch, tag)` and `lineTo(point, sketch, tag)` are combined into `line(@sketch, end?, endAbsolute?, tag?)`
- `close(sketch, tag?)` is now `close(@sketch, tag?)`
- `extrude(length, sketch)` is now `extrude(@sketch, length)`

Note that if a parameter starts with `@` like `@sketch`, it doesn't have any label when called, so you call it like this:

```
sketch = startSketchAt([0, 0])
line(sketch, end = [3, 3], tag = $hi)
```

Note also that if you're using a `|>` pipeline, you can omit the `@` argument and it will be assumed to be the LHS of the `|>`. So the above could be written as

```
sketch = startSketchAt([0, 0])
|> line(end = [3, 3], tag = $hi)
```

Also changes frontend tests to use KittyCAD/kcl-samples#139 instead of its main

The regex find-and-replace I use for migrating code (note these don't work with multi-line expressions) are:

```
 line\(([^=]*), %\)
 line(end = $1)

 line\((.*), %, (.*)\)
 line(end = $1, tag = $2)

 lineTo\((.*), %\)
 line(endAbsolute = $1)

 lineTo\((.*), %, (.*)\)
 line(endAbsolute = $1, tag = $2)

 extrude\((.*), %\)
 extrude(length = $1)

extrude\(([^=]*), ([a-zA-Z0-9]+)\)
extrude($2, length = $1)

 close\(%, (.*)\)
 close(tag = $1)
```

# Selected notes from commits before I squash them all

* Fix test 'yRelative to horizontal distance'

Fixes:
 - Make a lineTo helper
 - Fix pathToNode to go through the labeled arg .arg property

* Fix test by changing lookups into transformMap

Parts of the code assumed that `line` is always a relative call. But
actually now it might be absolute, if it's got an `endAbsolute` parameter.

So, change whether to look up `line` or `lineTo` and the relevant absolute
or relative line types based on that parameter.

* Stop asserting on exact source ranges

When I changed line to kwargs, all the source ranges we assert on became
slightly different. I find these assertions to be very very low value.
So I'm removing them.

* Fix more tests: getConstraintType calls weren't checking if the
'line' fn was absolute or relative.

* Fixed another queryAst test

There were 2 problems:
 - Test was looking for the old style of `line` call to choose an offset
   for pathToNode
 - Test assumed that the `tag` param was always the third one, but in
   a kwarg call, you have to look it up by label

* Fix test: traverse was not handling CallExpressionKw

* Fix another test, addTagKw

addTag helper was not aware of kw args.

* Convert close from positional to kwargs

If the close() call has 0 args, or a single unlabeled arg, the parser
interprets it as a CallExpression (positional) not a CallExpressionKw.

But then if a codemod wants to add a tag to it, it tries adding a kwarg
called 'tag', which fails because the CallExpression doesn't need
kwargs inserted into it.

The fix is: change the node from CallExpression to CallExpressionKw, and
update getNodeFromPath to take a 'replacement' arg, so we can replace
the old node with the new node in the AST.

* Fix the last test

Test was looking for `lineTo` as a substring of the input KCL program.
But there's no more lineTo function, so I changed it to look for
line() with an endAbsolute arg, which is the new equivalent.

Also changed the getConstraintInfo code to look up the lineTo if using
line with endAbsolute.

* Fix many bad regex find-replaces

I wrote a regex find-and-replace which converted `line` calls from
positional to keyword calls. But it was accidentally applied to more
places than it should be, for example, angledLine, xLine and yLine calls.

Fixes this.

* Fixes test 'Basic sketch › code pane closed at start'

Problem was, the getNodeFromPath call might not actually find a callExpressionKw,
it might find a callExpression. So the `giveSketchFnCallTag` thought
it was modifying a kwargs call, but it was actually modifying a positional
call.

This meant it tried to push a labeled argument in, rather than a normal
arg, and a lot of other problems. Fixed by doing runtime typechecking.

* Fix: Optional args given with wrong type were silently ignored

Optional args don't have to be given. But if the user gives them, they
should be the right type.

Bug: if the KCL interpreter found an optional arg, which was given, but
was the wrong type, it would ignore it and pretend the arg was never
given at all. This was confusing for users.

Fix: Now if you give an optional arg, but it's the wrong type, KCL will
emit a type error just like it would for a mandatory argument.

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
Co-authored-by: Nick Cameron <nrc@ncameron.org>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Frank Noirot <frank@kittycad.io>
Co-authored-by: Kevin Nadro <kevin@zoo.dev>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2025-02-04 08:31:43 -06:00
f5b8298735 Revert "Turn off snapshot commit bot" (#5198)
* Turn off snapshot commit bot

Our team is actively investigating a recent spike in flaky WebRTC
connection behavior, which has impacted the web-only Playwright snapshot
tests particularly hard. The bot squashes other GH Actions in it's wake,
so I think we should turn it off so we can see the other E2E tests more
clearly

* Revert "Turn off snapshot commit bot"

This reverts commit ab80bdb08a.

* Revert "Turn off snapshot commit bot"

This reverts commit ab80bdb08a.

---------

Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
Co-authored-by: Pierre Jacquier <pierre@zoo.dev>
2025-02-04 06:16:09 -05:00
max
25ad603502 Refactor Edge Treatment Module to Break Cyclic Dependency (#5243)
* break cycle

* yarn fmt
2025-02-03 19:14:53 -05:00
86349375d0 Don't run thumbnail hook in browser (#5229)
* Don't run thumbnail hook in browser

* fmt
2025-02-03 21:09:47 +00:00
56d861f2cc Wait for ICE gathering completion before requesting video track + create file e2e test fix (#5193)
* Test main e2e

* Create projects separately in home page tests

I think creating them in Promise.all was introducing nondeterminism and
making tests flaky.

* Query the homepage projects in an order-insensitive way

* Wait for ICE candidate gathering to complete before requesting video track

* Update src/lang/std/engineConnection.ts

Co-authored-by: Jonathan Tran <jonnytran@gmail.com>

* Fix create file e2e failure

* Yarn fmt

* Fix typo: s/that/this

yarn tsc was failing with this error:

```
src/lang/std/engineConnection.ts:1285:7 - error TS2304: Cannot find name 'that'.

1285       that.triggeredStart = false
           ~~~~
```

* Fix up revolve tests

* Turn off 3 flaky Windows tests

* Fix tags

---------

Co-authored-by: Adam Chalmers <adam.chalmers@zoo.dev>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2025-02-03 13:41:23 -05:00
max
3e8ee3ffc4 Add Point-and-Click Deletion for Fillets and Chamfers (#5098)
* ast mod

* point and click test

* tsc

* test test

* unit test edit

* topLevelRange

* disable unit test

* remove bad imports

* fix typo

* Fix cyclic dependency hell with getNodePathFromSourceRange

* tsc

* fix ImportStatement

* fix isValueZero

* pre-emptively ==> preemptively

* yarn fmt-check

* reenable the unit test

* fmt

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* add test

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* several treatments

* consolidate

* typos

* fix imports, consolidate

* consolidate import

* fix imports

* add tests

* stress test CI

* fix test

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* fix tests

* clean test for fillets

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* test chamfers

* comments

* simplify main tests

* typo

* typo2

* remove import

* clean up comments

---------

Co-authored-by: 49lf <ircsurfer33@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-03 12:11:26 -05:00
a44516bc7e Turn on Share Link in nightly builds (#5153)
* WIP: Turn on link sharing in released apps with electron-builder
Fixes #5136

* Add import.meta.env defaults

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Add convenience scripts for windows development; fix protocol name for electron; enable share cmd

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Force release builds

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* Fix lint

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* CSC_FOR_PULL_REQUEST: true for release build testing

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Adding ://

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Back to debug builds

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Back to debug builds

* WIP: origin

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* To revert: Add logs and custom package version for easier testing

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* More messing with env vars

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Messing with help from deep links docs

* Removed alerts

* Working on macos

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Working second window on windows. Cold start not yet working

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Handle windows cold start

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Clean up after macos testing

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Replace tron:package (Forge) with tronb📦dev (Builder) for e2e

* Add new env var for web app link

* tronb:vite:dev for e2e

* Remove app.requestSingleInstanceLock() call

* Fix unit test

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Revert snap changes

* Only nightly at first

* Remove test app

* Update package.json

* Update src/main.ts

* Remove fetch:wasm:windows

* Final line

* Clean up

* Back to test app for final test

* Fix tsc

* Back to https://app.dev.zoo.dev from vercel branch deploy

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-03 10:03:41 -05:00
ce62fe67cf Bump google-github-actions/auth from 2.1.7 to 2.1.8 (#5218)
Bumps [google-github-actions/auth](https://github.com/google-github-actions/auth) from 2.1.7 to 2.1.8.
- [Release notes](https://github.com/google-github-actions/auth/releases)
- [Changelog](https://github.com/google-github-actions/auth/blob/main/CHANGELOG.md)
- [Commits](https://github.com/google-github-actions/auth/compare/v2.1.7...v2.1.8)

---
updated-dependencies:
- dependency-name: google-github-actions/auth
  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>
2025-02-03 05:13:11 -05:00
763a1b6628 Bump google-github-actions/upload-cloud-storage from 2.2.1 to 2.2.2 (#5217)
Bumps [google-github-actions/upload-cloud-storage](https://github.com/google-github-actions/upload-cloud-storage) from 2.2.1 to 2.2.2.
- [Release notes](https://github.com/google-github-actions/upload-cloud-storage/releases)
- [Changelog](https://github.com/google-github-actions/upload-cloud-storage/blob/main/CHANGELOG.md)
- [Commits](https://github.com/google-github-actions/upload-cloud-storage/compare/v2.2.1...v2.2.2)

---
updated-dependencies:
- dependency-name: google-github-actions/upload-cloud-storage
  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>
2025-02-03 05:13:00 -05:00
3281e62e6b Bump google-github-actions/setup-gcloud from 2.1.2 to 2.1.4 (#5216)
Bumps [google-github-actions/setup-gcloud](https://github.com/google-github-actions/setup-gcloud) from 2.1.2 to 2.1.4.
- [Release notes](https://github.com/google-github-actions/setup-gcloud/releases)
- [Changelog](https://github.com/google-github-actions/setup-gcloud/blob/main/CHANGELOG.md)
- [Commits](https://github.com/google-github-actions/setup-gcloud/compare/v2.1.2...v2.1.4)

---
updated-dependencies:
- dependency-name: google-github-actions/setup-gcloud
  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>
2025-02-03 05:12:50 -05:00
f1a458f124 Add a trackball camera setting (#4764)
* Add a setting that does nothing

* Make the setting actually change the interaction type

* fmt

* Bump `@kittycad/lib` to get the proper camera drag interaction types

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Fix camera orientation bugs to support proper camera resetting on "camera orbit" setting change (#5031)

* Add a setting that does nothing

* Make the setting actually change the interaction type

* fmt

* fix: up vector bug fix and camera reset fix. Pushing code to cleanup after debugging

* fix: deleting debugging code

* fix: removing debugging code

* fix: removing debugging console log

* fix: removing console log debugs

* fix: adding comment, restoring code from debugging

* fix: removed lookAt when the orientation is already set from the engine.. I do not think we should be recomputing it?

* fix: this fixes the bug because I was pointing to the getter not the value

* Remove unused imports

* Fix lint for unawaited Promise

* Remove pointless change

---------

Co-authored-by: Frank Noirot <frank@kittycad.io>
Co-authored-by: Frank Noirot <frankjohnson1993@gmail.com>

* Re-run CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Re-run CI

* Add display attributes to try to fix cargo test

* Remove backwards compat test case

it's failing because I didn't add cameraOrbit to that type and I don't
want to

* Fix test value (prev user value would have been Spherical before Trackball)

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Kevin Nadro <nadr0@users.noreply.github.com>
Co-authored-by: Pierre Jacquier <pierre@zoo.dev>
2025-02-01 20:03:04 +00:00
229433126d Feature: Implemented thumbnail.png saving and load. Projects on homepage will have images (#5133)
* feature: implemented saving thumbnail.png to have project thumbnails in the home page

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* bump

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* bump

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* bump

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* bump

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* bump

* Fix the failing test by increasing window height (related to toast covering now-larger project tiles)

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Pierre Jacquier <pierre@zoo.dev>
Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
Co-authored-by: Frank Noirot <frank@zoo.dev>
2025-02-01 11:40:02 +00:00
b962b5fcb3 Feature: Release Revolve to all users, added E2E test for revolve in command bar (#5085)
* chore: implemented E2E test for revolve

* fix: revert testing code

* fix: codespell

* fix: added access via the toolbar

* fix: saving off bugging code

* fix: removing error message

* fix: cleaning up testing code

* chore: adding more e2e tests for revolve

---------

Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
2025-02-01 06:02:43 -05:00
428d125139 Make point-and-click Sweep generally available (#5159)
* Make point-and-click Sweep generally available
Fixes #5156

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Remove replace /segment/face

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Selections change will be done in separate PR #5183

* Toolbar button

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Reset snaps

* Revert screenshot

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-01 05:21:25 -05:00
cffeb52b4b test: Add testing the artifact graph when there's an execution error (#5154)
* Add testing the artifact graph snapshots when there's an execution error

* Update output to check artifact graph in error cases

* Rename helper function to be clearer

* Add test that has meaningful output, followed by an error
2025-01-31 18:32:30 -05:00
e0ef10e7bb Bump express from 4.21.0 to 4.21.2 for path-to-regexp fix (#5188) 2025-01-31 22:43:58 +00:00
7095ce2377 Fix the '1 face' mislabelling of selection for sweep segments (#5183)
* Fix the '1 face' mislabelling of selection for sweep segments
Fixes #5182

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Reset snapshots

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Fix lint

* Revert snap

* Fix chamfer and fillet test selection

* Fix other test

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-31 16:49:57 -05:00
5b207d7d1a chore: adding unit test to test getNodeFromPath failures (#5134)
* chore: adding unit test to test the parsing of the code

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* chore: trying to console error more content for getNodeByPath since it is cryptic

* fix: removing testing unit test

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* fix: fmt

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* bump

* fix: ...

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* bump

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* bump

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* bump

* fix: found da bug

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-31 15:13:43 -06:00
2fac213c58 Add engine message to dry run validation error toasts (#5175)
* Add engine message to dry run validation error toasts
Fixes #5174

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Add unit tests

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Reset snapshots

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Revert snapshot changes

* Fix lint

* Fix test

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-31 16:13:35 -05:00
2f72a8ef14 Helper functions for meta settings (get/update) (#5200)
* get the units back out

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

* updates

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

* edit

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

* updates

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

* updates

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

* add to wasmts

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

* fmt

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

* updates

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-01-31 21:11:15 +00:00
27ce9f8aa4 Remove project.toml fetch during Open Sample (#5203)
* Fix units in tests

* Don't default to mm

* Last fix

* FIx lint

* Remove project.toml fetching from samples
2025-01-31 15:38:04 -05:00
b0426e3f94 Refactor: separate authMachine from React (#5110)
* Create a global appMachine

* Strip authMachine of side-effects

* Replace react-bound authMachine use with XState actor use

* Fix import goof

* Register auth commands directly!

* @lf94 feedback: conver `AuthNavigationHandler` to `useAuthNavigation`

* Uh, fix signing out thank you @lf94

* Fix tsc

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)"

This reverts commit 8dc50b6a26.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-31 14:47:08 -05:00
d707c66e53 Migrate Extrude AST mod from XState action to actor (#5146)
* Migrate Extrude AST mod from XState action to actor

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* @lf94 feedback

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-31 18:34:38 +00:00
255 changed files with 10892 additions and 7197 deletions

View File

@ -2,8 +2,8 @@ NODE_ENV=development
DEV=true
VITE_KC_API_WS_MODELING_URL=wss://api.dev.zoo.dev/ws/modeling/commands
VITE_KC_API_BASE_URL=https://api.dev.zoo.dev
BASE_URL=https://api.dev.zoo.dev
VITE_KC_SITE_BASE_URL=https://dev.zoo.dev
VITE_KC_SITE_APP_URL=https://app.dev.zoo.dev
VITE_KC_SKIP_AUTH=false
VITE_KC_CONNECTION_TIMEOUT_MS=5000
# ONLY add your token in .env.development.local if you want to skip auth, otherwise this token takes precedence!

View File

@ -1,5 +1,8 @@
NODE_ENV=production
DEV=false
VITE_KC_API_WS_MODELING_URL=wss://api.zoo.dev/ws/modeling/commands
VITE_KC_API_BASE_URL=https://api.zoo.dev
VITE_KC_SITE_BASE_URL=https://zoo.dev
VITE_KC_SITE_APP_URL=https://app.zoo.dev
VITE_KC_SKIP_AUTH=false
VITE_KC_CONNECTION_TIMEOUT_MS=15000

View File

@ -29,6 +29,13 @@
{
"name": "isNaN",
"message": "Use Number.isNaN() instead."
},
],
"no-restricted-syntax": [
"error",
{
"selector": "CallExpression[callee.object.name='Array'][callee.property.name='isArray']",
"message": "Use isArray() in lib/utils.ts instead of Array.isArray()."
}
],
"semi": [

View File

@ -134,8 +134,6 @@ jobs:
max_attempts: 3
command: yarn install
- run: yarn tronb:vite
- name: Prepare certificate and variables (Windows only)
if: ${{ (env.IS_RELEASE == 'true' || env.IS_NIGHTLY == 'true') && matrix.os == 'windows-2022' }}
run: |
@ -165,8 +163,8 @@ jobs:
- name: Build the app (debug)
if: ${{ env.IS_RELEASE == 'false' && env.IS_NIGHTLY == 'false' }}
# electron-builder doesn't have a concept of release vs debug,
# this is just not doing any codesign or release yml generation
run: yarn electron-builder --config
# this is just not doing any codesign or release yml generation, and points to dev infra
run: yarn tronb:package:dev
- name: Build the app (release)
if: ${{ env.IS_RELEASE == 'true' || env.IS_NIGHTLY == 'true' }}
@ -185,7 +183,7 @@ jobs:
with:
timeout_minutes: 10
max_attempts: 3
command: yarn electron-builder --config --publish always
command: yarn tronb:package:prod
- name: List artifacts in out/
run: ls -R out
@ -246,7 +244,7 @@ jobs:
with:
timeout_minutes: 10
max_attempts: 3
command: yarn electron-builder --config --publish always
command: yarn tronb:package:prod
- uses: actions/upload-artifact@v4
if: ${{ env.IS_RELEASE == 'true' }}
@ -390,19 +388,19 @@ jobs:
- name: Authenticate to Google Cloud
if: ${{ env.IS_NIGHTLY == 'true' }}
uses: 'google-github-actions/auth@v2.1.7'
uses: 'google-github-actions/auth@v2.1.8'
with:
credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}'
- name: Set up Google Cloud SDK
if: ${{ env.IS_NIGHTLY == 'true' }}
uses: google-github-actions/setup-gcloud@v2.1.2
uses: google-github-actions/setup-gcloud@v2.1.4
with:
project_id: ${{ env.GOOGLE_CLOUD_PROJECT_ID }}
- name: Upload nightly files to public bucket
if: ${{ env.IS_NIGHTLY == 'true' }}
uses: google-github-actions/upload-cloud-storage@v2.2.1
uses: google-github-actions/upload-cloud-storage@v2.2.2
with:
path: out
glob: '*'

View File

@ -1,4 +1,4 @@
name: E2E Tests
name: E2E Flow Tests
on:
push:
branches: [ main ]
@ -33,7 +33,7 @@ jobs:
rust:
- 'src/wasm-lib/**'
electron:
flow-tests:
timeout-minutes: 60
name: playwright:electron:${{ matrix.os }} ${{ matrix.shardIndex }} ${{ matrix.shardTotal }}
strategy:
@ -47,23 +47,29 @@ jobs:
needs: check-rust-changes
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- uses: KittyCAD/action-install-cli@main
- name: Install dependencies
shell: bash
run: yarn
- name: Cache Playwright Browsers
uses: actions/cache@v4
with:
path: |
~/.cache/ms-playwright/
key: ${{ runner.os }}-playwright-${{ hashFiles('yarn.lock') }}
- name: Install Playwright Browsers
shell: bash
run: yarn playwright install --with-deps
- name: Download Wasm Cache
id: download-wasm
if: needs.check-rust-changes.outputs.rust-changed == 'false'
@ -75,29 +81,35 @@ jobs:
workflow: build-and-store-wasm.yml
branch: main
path: src/wasm-lib/pkg
- name: copy wasm blob
if: needs.check-rust-changes.outputs.rust-changed == 'false'
shell: bash
run: cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
continue-on-error: true
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache Wasm (because rust diff)
if: needs.check-rust-changes.outputs.rust-changed == 'true'
uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: OR Cache Wasm (because wasm cache failed)
if: steps.download-wasm.outcome == 'failure'
uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: install good sed
if: ${{ startsWith(matrix.os, 'macos') }}
shell: bash
run: |
brew install gnu-sed
echo "/opt/homebrew/opt/gnu-sed/libexec/gnubin" >> $GITHUB_PATH
- name: Install vector
shell: bash
# TODO: figure out what to do with this, it's failing
@ -115,81 +127,33 @@ jobs:
sed -i "s#GH_ACTIONS_AXIOM_TOKEN#${{secrets.GH_ACTIONS_AXIOM_TOKEN}}#g" /tmp/vector.toml
cat /tmp/vector.toml
${HOME}/.vector/bin/vector --config /tmp/vector.toml &
- name: Build Wasm (because rust diff)
if: needs.check-rust-changes.outputs.rust-changed == 'true'
shell: bash
run: yarn build:wasm
- name: OR Build Wasm (because wasm cache failed)
if: steps.download-wasm.outcome == 'failure'
shell: bash
run: yarn build:wasm
- name: build electron
- name: build web
shell: bash
run: yarn tron:package
# - name: Run ubuntu/chrome snapshots
# if: ${{ matrix.os == 'namespace-profile-ubuntu-8-cores' && matrix.shardIndex == 1 }}
# shell: bash
# # TODO: break this in its own job, for now it's not slowing down the overall execution as ubuntu is the quickest,
# # but we could do better. This forces a large 1/1 shard of all 20 snapshot tests that runs in about 3 minutes.
# run: |
# PLATFORM=web yarn playwright test --config=playwright.config.ts --retries="3" --update-snapshots --grep=@snapshot --shard=1/1
# env:
# CI: true
# NODE_ENV: development
# VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
# VITE_KC_SKIP_AUTH: true
# token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
# snapshottoken: ${{ secrets.KITTYCAD_API_TOKEN }}
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() && (success() || failure()) }}
with:
name: playwright-report-${{ matrix.os }}-snapshot-${{ matrix.shardIndex }}-${{ github.sha }}
path: playwright-report/
include-hidden-files: true
retention-days: 30
overwrite: true
run: yarn tronb:vite:dev
- name: Clean up test-results
if: ${{ !cancelled() && (success() || failure()) }}
continue-on-error: true
run: rm -r test-results
- name: check for changes
if: ${{ matrix.os == 'namespace-profile-ubuntu-8-cores' && matrix.shardIndex == 1 }}
shell: bash
id: git-check
run: |
git add .
if git status | grep -q "Changes to be committed"
then echo "modified=true" >> $GITHUB_OUTPUT
else echo "modified=false" >> $GITHUB_OUTPUT
fi
# - name: Commit changes, if any
# if: steps.git-check.outputs.modified == 'true'
# shell: bash
# run: |
# git add .
# git config --local user.email "github-actions[bot]@users.noreply.github.com"
# git config --local user.name "github-actions[bot]"
# git remote set-url origin https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
# git fetch origin
# echo ${{ github.head_ref }}
# git checkout ${{ github.head_ref }}
# git commit -am "A snapshot a day keeps the bugs away! 📷🐛 (OS: ${{matrix.os}})" || true
# git push
# git push origin ${{ github.head_ref }}
# only upload artifacts if there's actually changes
- uses: actions/upload-artifact@v4
if: steps.git-check.outputs.modified == 'true'
with:
name: playwright-report-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
path: playwright-report/
include-hidden-files: true
retention-days: 30
- uses: actions/download-artifact@v4
if: ${{ !cancelled() && (success() || failure()) }}
continue-on-error: true
with:
name: test-results-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
path: test-results/
- name: Run playwright/electron flow (with retries)
id: retry
if: ${{ !cancelled() && (success() || failure()) }}
@ -203,6 +167,7 @@ jobs:
VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
VITE_KC_SKIP_AUTH: true
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
- uses: actions/upload-artifact@v4
if: always()
with:
@ -211,6 +176,7 @@ jobs:
include-hidden-files: true
retention-days: 30
overwrite: true
- uses: actions/upload-artifact@v4
if: always()
with:
@ -219,4 +185,3 @@ jobs:
include-hidden-files: true
retention-days: 30
overwrite: true

145
.github/workflows/e2e-snapshot-tests.yml vendored Normal file
View File

@ -0,0 +1,145 @@
name: E2E Snapshot Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions:
contents: write
pull-requests: write
actions: read
jobs:
check-rust-changes:
runs-on: ubuntu-latest
outputs:
rust-changed: ${{ steps.filter.outputs.rust }}
steps:
- uses: actions/checkout@v4
- id: filter
name: Check for Rust changes
uses: dorny/paths-filter@v3
with:
filters: |
rust:
- 'src/wasm-lib/**'
snapshot-tests:
runs-on: ubuntu-22.04
needs: check-rust-changes
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- name: Install dependencies
run: yarn
- name: Cache Playwright Browsers
uses: actions/cache@v4
with:
path: |
~/.cache/ms-playwright/
key: ${{ runner.os }}-playwright-${{ hashFiles('yarn.lock') }}
- name: Install Playwright Browsers
run: yarn playwright install --with-deps
- name: Download Wasm Cache
id: download-wasm
if: needs.check-rust-changes.outputs.rust-changed == 'false'
uses: dawidd6/action-download-artifact@v7
continue-on-error: true
with:
github_token: ${{secrets.GITHUB_TOKEN}}
name: wasm-bundle
workflow: build-and-store-wasm.yml
branch: main
path: src/wasm-lib/pkg
- name: copy wasm blob
if: needs.check-rust-changes.outputs.rust-changed == 'false'
run: cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
continue-on-error: true
- name: Setup Rust
if: needs.check-rust-changes.outputs.rust-changed == 'true'
uses: dtolnay/rust-toolchain@stable
- name: Cache Wasm (because rust diff)
if: needs.check-rust-changes.outputs.rust-changed == 'true'
uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: OR Cache Wasm (because wasm cache failed)
if: steps.download-wasm.outcome == 'failure'
uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: Build Wasm (because rust diff)
if: needs.check-rust-changes.outputs.rust-changed == 'true'
run: yarn build:wasm
- name: OR Build Wasm (because wasm cache failed)
if: steps.download-wasm.outcome == 'failure'
run: yarn build:wasm
- name: build web
run: yarn tronb:vite:dev
- name: Run chrome snapshots
run: |
PLATFORM=web yarn playwright test --config=playwright.config.ts --retries="3" --update-snapshots --grep=@snapshot
env:
CI: true
NODE_ENV: development
VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
VITE_KC_SKIP_AUTH: true
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
snapshottoken: ${{ secrets.KITTYCAD_API_TOKEN }}
- name: check for changes
id: git-check
run: |
{
echo 'changes<<EOF'
git diff --name-only e2e/playwright/snapshot-tests.spec.ts-snapshots
echo EOF
} >> "$GITHUB_OUTPUT"
# only upload artifacts if there's actually changes
- name: Upload changes, if any
if: steps.git-check.outputs.changes != ''
uses: actions/upload-artifact@v4
with:
name: playwright-snapshots-${{ runner.os }}-${{ github.sha }}
path: ${{ steps.git-check.outputs.changes }}
- name: Upload report, if any
uses: actions/upload-artifact@v4
if: steps.git-check.outputs.changes != ''
with:
name: playwright-report-${{ runner.os }}-${{ github.sha }}
path: playwright-report/
include-hidden-files: true
retention-days: 30
- name: Fail the run if we have snapshot updates
if: steps.git-check.outputs.changes != ''
run: exit 1
# TODO: check if we could comment on the PR as well

View File

@ -108,17 +108,17 @@ jobs:
run: yarn files:set-notes
- name: Authenticate to Google Cloud
uses: 'google-github-actions/auth@v2.1.7'
uses: 'google-github-actions/auth@v2.1.8'
with:
credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}'
- name: Set up Google Cloud SDK
uses: google-github-actions/setup-gcloud@v2.1.2
uses: google-github-actions/setup-gcloud@v2.1.4
with:
project_id: ${{ env.GOOGLE_CLOUD_PROJECT_ID }}
- name: Upload release files to public bucket
uses: google-github-actions/upload-cloud-storage@v2.2.1
uses: google-github-actions/upload-cloud-storage@v2.2.2
with:
path: out
glob: '*'

View File

@ -101,7 +101,7 @@ This will start the application and hot-reload on changes.
Devtools can be opened with the usual Cmd-Opt-I (Mac) or Ctrl-Shift-I (Linux and Windows).
To build, run `yarn tron:package`.
To build with electron-builder, run `yarn tronb:package:dev` (or `yarn tronb:package:prod` to point to the .env.production variables)
## Checking out commits / Bisecting

View File

@ -47,21 +47,6 @@ myObj = { a = 0, b = "thing" }
We support two different ways of getting properties from objects, you can call
`myObj.a` or `myObj["a"]` both work.
## Functions
We also have support for defining your own functions. Functions can take in any
type of argument. Below is an example of the syntax:
```
fn myFn(x) {
return x
}
```
As you can see above `myFn` just returns whatever it is given.
## Binary expressions
You can also do math! Let's show an example below:
@ -76,6 +61,120 @@ You can nest expressions in parenthesis as well:
myMathExpression = 3 + (1 * 2 / (3 - 7))
```
## Functions
We also have support for defining your own functions. Functions can take in any
type of argument. Below is an example of the syntax:
```
fn myFn(x) {
return x
}
```
As you can see above `myFn` just returns whatever it is given.
KCL's early drafts used positional arguments, but we now use keyword arguments. If you declare a
function like this:
```
fn add(left, right) {
return left + right
}
```
You can call it like this:
```
total = add(left = 1, right = 2)
```
Functions can also declare one *unlabeled* arg. If you do want to declare an unlabeled arg, it must
be the first arg declared.
```
// The @ indicates an argument can be used without a label.
// Note that only the first argument can use @.
fn increment(@x) {
return x + 1
}
fn add(@x, delta) {
return x + delta
}
two = increment(1)
three = add(1, delta = 2)
```
## Pipelines
It can be hard to read repeated function calls, because of all the nested brackets.
```
i = 1
x = h(g(f(i)))
```
You can make this easier to read by breaking it into many declarations, but that is a bit annoying.
```
i = 1
x0 = f(i)
x1 = g(x0)
x = h(x1)
```
Instead, you can use the pipeline operator (`|>`) to simplify this.
Basically, `x |> f(%)` is a shorthand for `f(x)`. The left-hand side of the `|>` gets put into
the `%` in the right-hand side.
So, this means `x |> f(%) |> g(%)` is shorthand for `g(f(x))`. The code example above, with its
somewhat-clunky `x0` and `x1` constants could be rewritten as
```
i = 1
x = i
|> f(%)
|> g(%)
|> h(%)
```
This helps keep your code neat and avoid unnecessary declarations.
## Pipelines and keyword arguments
Say you have a long pipeline of sketch functions, like this:
```
startSketch()
|> line(%, end = [3, 4])
|> line(%, end = [10, 10])
|> line(%, end = [-13, -14])
|> close(%)
```
In this example, each function call outputs a sketch, and it gets put into the next function call via
the `%`, into the first (unlabeled) argument.
If a function call uses an unlabeled first parameter, it will default to `%` if it's not given. This
means that `|> line(%, end = [3, 4])` and `|> line(end = [3, 4])` are equivalent! So the above
could be rewritten as
```
startSketch()
|> line(end = [3, 4])
|> line(end = [10, 10])
|> line(end = [-13, -14])
|> close()
```
Note that we are still in the process of migrating KCL's standard library to use keyword arguments. So some
functions are still unfortunately using positional arguments. We're moving them over, so keep checking back.
Some functions like `angledLine`, `startProfileAt` etc are still using the old positional argument syntax.
Check the docs page for each function and look at its examples to see.
## Tags
Tags are used to give a name (tag) to a specific path.
@ -88,17 +187,17 @@ way:
```
startSketchOn('XZ')
|> startProfileAt(origin, %)
|> angledLine([0, 191.26], %, $rectangleSegmentA001)
|> angledLine([
segAng(rectangleSegmentA001) - 90,
196.99
], %, $rectangleSegmentB001)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
|> angledLine({angle = 0, length = 191.26}, %, $rectangleSegmentA001)
|> angledLine({
angle = segAng(rectangleSegmentA001) - 90,
length = 196.99,
}, %, $rectangleSegmentB001)
|> angledLine({
angle = segAng(rectangleSegmentA001),
length = -segLen(rectangleSegmentA001),
}, %, $rectangleSegmentC001)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
```
### Tag Identifier
@ -121,17 +220,17 @@ However if the code was written like this:
fn rect(origin) {
return startSketchOn('XZ')
|> startProfileAt(origin, %)
|> angledLine([0, 191.26], %, $rectangleSegmentA001)
|> angledLine([
segAng(rectangleSegmentA001) - 90,
196.99
], %, $rectangleSegmentB001)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
|> angledLine({angle = 0, length = 191.26}, %, $rectangleSegmentA001)
|> angledLine({
angle = segAng(rectangleSegmentA001) - 90,
length = 196.99
}, %, $rectangleSegmentB001)
|> angledLine({
angle = segAng(rectangleSegmentA001),
length = -segLen(rectangleSegmentA001)
}, %, $rectangleSegmentC001)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
}
rect([0, 0])
@ -149,17 +248,17 @@ For example the following code works.
fn rect(origin) {
return startSketchOn('XZ')
|> startProfileAt(origin, %)
|> angledLine([0, 191.26], %, $rectangleSegmentA001)
|> angledLine([
segAng(rectangleSegmentA001) - 90,
196.99
], %, $rectangleSegmentB001)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
|> angledLine({angle = 0, length = 191.26}, %, $rectangleSegmentA001)
|> angledLine({
angle = segAng(rectangleSegmentA001) - 90,
length = 196.99
}, %, $rectangleSegmentB001)
|> angledLine({
angle = segAng(rectangleSegmentA001),
length = -segLen(rectangleSegmentA001)
}, %, $rectangleSegmentC001)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
}
rect([0, 0])

View File

@ -144,7 +144,7 @@ async function doBasicSketch(
|> xLine(-segLen(seg01), %)`)
}
test.describe('Basic sketch', () => {
test.describe('Basic sketch', { tag: ['@skipWin'] }, () => {
test.fixme('code pane open at start', async ({ page, homePage }) => {
await doBasicSketch(page, homePage, ['code'])
})

View File

@ -4,7 +4,10 @@ import { getUtils } from './test-utils'
import { EngineCommand } from 'lang/std/artifactGraph'
import { uuidv4 } from 'lib/utils'
test.describe('Can create sketches on all planes and their back sides', () => {
test.describe(
'Can create sketches on all planes and their back sides',
{ tag: ['@skipWin'] },
() => {
const sketchOnPlaneAndBackSideTest = async (
page: Page,
homePage: HomePageFixture,
@ -88,11 +91,17 @@ test.describe('Can create sketches on all planes and their back sides', () => {
})
test('YZ', async ({ page, homePage }) => {
await sketchOnPlaneAndBackSideTest(page, homePage, 'YZ', { x: 700, y: 250 }) // green plane
await sketchOnPlaneAndBackSideTest(page, homePage, 'YZ', {
x: 700,
y: 250,
}) // green plane
})
test('XZ', async ({ page, homePage }) => {
await sketchOnPlaneAndBackSideTest(page, homePage, '-XZ', { x: 700, y: 80 }) // blue plane
await sketchOnPlaneAndBackSideTest(page, homePage, '-XZ', {
x: 700,
y: 80,
}) // blue plane
})
test('-XY', async ({ page, homePage }) => {
@ -110,6 +119,10 @@ test.describe('Can create sketches on all planes and their back sides', () => {
})
test('-XZ', async ({ page, homePage }) => {
await sketchOnPlaneAndBackSideTest(page, homePage, 'XZ', { x: 700, y: 427 }) // back of blue plane
await sketchOnPlaneAndBackSideTest(page, homePage, 'XZ', {
x: 700,
y: 427,
}) // back of blue plane
})
})
}
)

View File

@ -6,7 +6,7 @@ import { bracket } from 'lib/exampleKcl'
import { TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW } from './storageStates'
import fsp from 'fs/promises'
test.describe('Code pane and errors', () => {
test.describe('Code pane and errors', { tag: ['@skipWin'] }, () => {
test('Typing KCL errors induces a badge on the code pane button', async ({
page,
homePage,

View File

@ -4,7 +4,7 @@ import { executorInputPath, getUtils } from './test-utils'
import { KCL_DEFAULT_LENGTH } from 'lib/constants'
import path from 'path'
test.describe('Command bar tests', () => {
test.describe('Command bar tests', { tag: ['@skipWin'] }, () => {
test('Extrude from command bar selects extrude line after', async ({
page,
homePage,
@ -167,10 +167,10 @@ test.describe('Command bar tests', () => {
await expect(commandLevelArgButton).toHaveText('level: project')
})
test('Command bar keybinding works from code editor and can change a setting', async ({
page,
homePage,
}) => {
test(
'Command bar keybinding works from code editor and can change a setting',
{ tag: ['@skipWin'] },
async ({ page, homePage }) => {
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
@ -202,10 +202,9 @@ test.describe('Command bar tests', () => {
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await expect(page.getByRole('option', { name: 'system' })).toHaveAttribute(
'data-headlessui-state',
'active'
)
await expect(
page.getByRole('option', { name: 'system' })
).toHaveAttribute('data-headlessui-state', 'active')
await page.keyboard.press('Enter')
// Check the toast appeared
@ -214,7 +213,8 @@ test.describe('Command bar tests', () => {
).toBeVisible()
// Check that the theme changed
await expect(page.locator('body')).not.toHaveClass(`body-bg dark`)
})
}
)
test('Can extrude from the command bar', async ({ page, homePage }) => {
await page.addInitScript(async () => {

View File

@ -10,7 +10,7 @@ import {
import { join } from 'path'
test.describe('Editor tests', () => {
test.describe('Editor tests', { tag: ['@skipWin'] }, () => {
test('can comment out code with ctrl+/', async ({ page, homePage }) => {
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1000, height: 500 })
@ -966,10 +966,10 @@ test.describe('Editor tests', () => {
|> close()`)
})
test('Can undo a sketch modification with ctrl+z', async ({
page,
homePage,
}) => {
test(
'Can undo a sketch modification with ctrl+z',
{ tag: ['@skipWin'] },
async ({ page, homePage }) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
@ -1113,7 +1113,8 @@ test.describe('Editor tests', () => {
|> tangentialArcTo([24.95, -0.38], %)
|> close()
|> extrude(length = 5)`)
})
}
)
test.fixme(
`Can use the import stdlib function on a local OBJ file`,

View File

@ -19,7 +19,7 @@ test.describe('integrations tests', () => {
)
})
const [clickObj] = await scene.makeMouseHelpers(600, 300)
const [clickObj] = await scene.makeMouseHelpers(726, 272)
await test.step('setup test', async () => {
await homePage.expectState({
@ -61,6 +61,7 @@ test.describe('integrations tests', () => {
})
await test.step('setup for next assertion', async () => {
await toolbar.openFile('main.kcl')
await scene.waitForExecutionDone()
await clickObj()
await scene.moveNoWhere()
await editor.expectState({
@ -1185,4 +1186,56 @@ test.describe('Undo and redo do not keep history when navigating between files',
})
}
)
test(
`cloned file has an incremented name and same contents`,
{ tag: '@electron' },
async ({ page, context, homePage }, testInfo) => {
const { panesOpen, createNewFile, cloneFile } = await getUtils(page, test)
const { dir } = await context.folderSetupFn(async (dir) => {
const finalDir = join(dir, 'testDefault')
await fsp.mkdir(finalDir, { recursive: true })
await fsp.copyFile(
executorInputPath('e2e-can-sketch-on-chamfer.kcl'),
join(finalDir, 'lee.kcl')
)
})
const contentOriginal = await fsp.readFile(
join(dir, 'testDefault', 'lee.kcl'),
'utf-8'
)
await page.setBodyDimensions({ width: 1200, height: 500 })
page.on('console', console.log)
await panesOpen(['files'])
await homePage.openProject('testDefault')
await cloneFile('lee.kcl')
await cloneFile('lee-1.kcl')
await cloneFile('lee-2.kcl')
await cloneFile('lee-3.kcl')
await cloneFile('lee-4.kcl')
await test.step('Postcondition: there are 5 new lee-*.kcl files', async () => {
await expect(
page
.locator('[data-testid="file-pane-scroll-container"] button')
.filter({ hasText: /lee[-]?[0-5]?/ })
).toHaveCount(5)
})
await test.step('Postcondition: the files have the same contents', async () => {
for (let n = 0; n < 5; n += 1) {
const content = await fsp.readFile(
join(dir, 'testDefault', `lee-${n + 1}.kcl`),
'utf-8'
)
await expect(content).toEqual(contentOriginal)
}
})
}
)
})

View File

@ -89,18 +89,11 @@ export class HomePageFixture {
* Maybe there a good sanity check we can do each time?
*/
expectState = async (expectedState: HomePageState) => {
await expect
.poll(async () => {
const [projectCards, sortBy] = await Promise.all([
this._serialiseProjectCards(),
this._serialiseSortBy(),
])
return {
projectCards,
sortBy,
await expect.poll(this._serialiseSortBy).toEqual(expectedState.sortBy)
for (const projectCard of expectedState.projectCards) {
await expect.poll(this._serialiseProjectCards).toContainEqual(projectCard)
}
})
.toEqual(expectedState)
}
createAndGoToProject = async (projectTitle = 'project-$nnn') => {

View File

@ -18,6 +18,7 @@ export class ToolbarFixture {
filletButton!: Locator
chamferButton!: Locator
shellButton!: Locator
revolveButton!: Locator
offsetPlaneButton!: Locator
startSketchBtn!: Locator
lineBtn!: Locator
@ -47,6 +48,7 @@ export class ToolbarFixture {
this.filletButton = page.getByTestId('fillet3d')
this.chamferButton = page.getByTestId('chamfer3d')
this.shellButton = page.getByTestId('shell')
this.revolveButton = page.getByTestId('revolve')
this.offsetPlaneButton = page.getByTestId('plane-offset')
this.startSketchBtn = page.getByTestId('sketch')
this.lineBtn = page.getByTestId('line')
@ -60,7 +62,9 @@ export class ToolbarFixture {
this.filePane = page.locator('#files-pane')
this.featureTreePane = page.locator('#feature-tree-pane')
this.fileCreateToast = page.getByText('Successfully created')
this.exeIndicator = page.getByTestId('model-state-indicator-execution-done')
this.exeIndicator = page.getByTestId(
'model-state-indicator-receive-reliable'
)
}
get logoLink() {

View File

@ -8,16 +8,15 @@ import { getUtils } from './test-utils'
// test file is for testing point an click code gen functionality that's not sketch mode related
test('verify extruding circle works', async ({
test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
test('verify extruding circle works', async ({
context,
homePage,
cmdBar,
editor,
toolbar,
scene,
}) => {
// TODO: fix this test on windows after the electron migration
test.skip(process.platform === 'win32', 'Skip on windows')
}) => {
const file = await fs.readFile(
path.resolve(
__dirname,
@ -94,11 +93,9 @@ test('verify extruding circle works', async ({
await editor.expectEditor.toContain(expectString)
})
})
})
test.describe('verify sketch on chamfer works', () => {
// TODO: fix this test on windows after the electron migration
test.skip(process.platform === 'win32', 'Skip on windows')
test.describe('verify sketch on chamfer works', () => {
const _sketchOnAChamfer =
(
page: Page,
@ -217,7 +214,8 @@ test.describe('verify sketch on chamfer works', () => {
getOppositeEdge(seg01)
]}, %)`,
afterChamferSelectSnippet: 'sketch002 = startSketchOn(extrude001, seg03)',
afterChamferSelectSnippet:
'sketch002 = startSketchOn(extrude001, seg03)',
afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)',
afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002)
|> angledLine([
@ -248,7 +246,8 @@ test.describe('verify sketch on chamfer works', () => {
]
}, %)`,
afterChamferSelectSnippet: 'sketch003 = startSketchOn(extrude001, seg04)',
afterChamferSelectSnippet:
'sketch003 = startSketchOn(extrude001, seg04)',
afterRectangle1stClickSnippet: 'startProfileAt([-209.64, 255.28], %)',
afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003)
|> angledLine([
@ -273,7 +272,8 @@ test.describe('verify sketch on chamfer works', () => {
getNextAdjacentEdge(seg02)
]
}, %)`,
afterChamferSelectSnippet: 'sketch003 = startSketchOn(extrude001, seg04)',
afterChamferSelectSnippet:
'sketch003 = startSketchOn(extrude001, seg04)',
afterRectangle1stClickSnippet: 'startProfileAt([75.8, 317.2], %)',
afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003)
|> angledLine([
@ -296,7 +296,8 @@ test.describe('verify sketch on chamfer works', () => {
length = 30,
tags = [getNextAdjacentEdge(yo)]
}, %)`,
afterChamferSelectSnippet: 'sketch005 = startSketchOn(extrude001, seg06)',
afterChamferSelectSnippet:
'sketch005 = startSketchOn(extrude001, seg06)',
afterRectangle1stClickSnippet: 'startProfileAt([-23.43, 19.69], %)',
afterRectangle2ndClickSnippet: `angledLine([0, 9.1], %, $rectangleSegmentA005)
@ -436,7 +437,8 @@ test.describe('verify sketch on chamfer works', () => {
getOppositeEdge(seg01)
]}, extrude001)`,
beforeChamferSnippetEnd: '}, extrude001)',
afterChamferSelectSnippet: 'sketch002 = startSketchOn(extrude001, seg03)',
afterChamferSelectSnippet:
'sketch002 = startSketchOn(extrude001, seg03)',
afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)',
afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002)
|> angledLine([
@ -494,15 +496,15 @@ sketch002 = startSketchOn(extrude001, seg03)
{ shouldNormalise: true }
)
})
})
})
test(`Verify axis, origin, and horizontal snapping`, async ({
test(`Verify axis, origin, and horizontal snapping`, async ({
page,
homePage,
editor,
toolbar,
scene,
}) => {
}) => {
const viewPortSize = { width: 1200, height: 500 }
await page.setBodyDimensions(viewPortSize)
@ -599,16 +601,16 @@ test(`Verify axis, origin, and horizontal snapping`, async ({
expectedCodeSnippets.afterSegmentDraggedOnYAxis
)
})
})
})
test(`Verify user can double-click to edit a sketch`, async ({
test(`Verify user can double-click to edit a sketch`, async ({
context,
page,
homePage,
editor,
toolbar,
scene,
}) => {
}) => {
const u = await getUtils(page)
const initialCode = `closedSketch = startSketchOn('XZ')
@ -690,7 +692,11 @@ openSketch = startSketchOn('XY')
await test.step(`Double-click on the open sketch`, async () => {
await moveToOpenPath()
await scene.expectPixelColor([250, 250, 250], pointOnPathAfterSketching, 15)
await scene.expectPixelColor(
[250, 250, 250],
pointOnPathAfterSketching,
15
)
// There is a full execution after exiting sketch that clears the scene.
await page.waitForTimeout(500)
await dblClickOpenPath()
@ -704,9 +710,9 @@ openSketch = startSketchOn('XY')
diagnostics: [],
})
})
})
})
test(`Offset plane point-and-click`, async ({
test(`Offset plane point-and-click`, async ({
context,
page,
homePage,
@ -714,7 +720,7 @@ test(`Offset plane point-and-click`, async ({
editor,
toolbar,
cmdBar,
}) => {
}) => {
// One dumb hardcoded screen pixel value
const testPoint = { x: 700, y: 150 }
const [clickOnXzPlane] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
@ -767,13 +773,13 @@ test(`Offset plane point-and-click`, async ({
await page.keyboard.press('Backspace')
await scene.expectPixelColor([50, 51, 96], testPoint, 15)
})
})
})
const loftPointAndClickCases = [
const loftPointAndClickCases = [
{ shouldPreselect: true },
{ shouldPreselect: false },
]
loftPointAndClickCases.forEach(({ shouldPreselect }) => {
]
loftPointAndClickCases.forEach(({ shouldPreselect }) => {
test(`Loft point-and-click (preselected sketches: ${shouldPreselect})`, async ({
context,
page,
@ -859,16 +865,16 @@ loftPointAndClickCases.forEach(({ shouldPreselect }) => {
await scene.expectPixelColor([254, 254, 254], testPoint, 15)
})
})
})
})
// TODO: merge with above test. Right now we're not able to delete a loft
// right after creation via selection for some reason, so we go with a new instance
test('Loft and offset plane deletion via selection', async ({
// TODO: merge with above test. Right now we're not able to delete a loft
// right after creation via selection for some reason, so we go with a new instance
test('Loft and offset plane deletion via selection', async ({
context,
page,
homePage,
scene,
}) => {
}) => {
const initialCode = `sketch001 = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 30 }, %)
plane001 = offsetPlane('XZ', 50)
@ -885,7 +891,10 @@ loft001 = loft([sketch001, sketch002])
// One dumb hardcoded screen pixel value
const testPoint = { x: 575, y: 200 }
const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const [clickOnSketch2] = scene.makeMouseHelpers(testPoint.x, testPoint.y + 80)
const [clickOnSketch2] = scene.makeMouseHelpers(
testPoint.x,
testPoint.y + 80
)
await test.step(`Delete loft`, async () => {
// Check for loft
@ -920,9 +929,9 @@ loft001 = loft([sketch001, sketch002])
// Check for sketch 1
await scene.expectPixelColor([254, 254, 254], testPoint, 15)
})
})
})
test(`Sweep point-and-click`, async ({
test(`Sweep point-and-click`, async ({
context,
page,
homePage,
@ -930,7 +939,7 @@ test(`Sweep point-and-click`, async ({
editor,
toolbar,
cmdBar,
}) => {
}) => {
const initialCode = `sketch001 = startSketchOn('YZ')
|> circle({
center = [0, 0],
@ -951,7 +960,10 @@ sketch002 = startSketchOn('XZ')
// One dumb hardcoded screen pixel value
const testPoint = { x: 700, y: 250 }
const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const [clickOnSketch2] = scene.makeMouseHelpers(testPoint.x - 50, testPoint.y)
const [clickOnSketch2] = scene.makeMouseHelpers(
testPoint.x - 50,
testPoint.y
)
const sweepDeclaration = 'sweep001 = sweep({ path = sketch002 }, sketch001)'
await test.step(`Look for sketch001`, async () => {
@ -1012,16 +1024,16 @@ sketch002 = startSketchOn('XZ')
await toolbar.closePane('feature-tree')
await scene.expectPixelColor([53, 53, 53], testPoint, 15)
})
})
})
test(`Sweep point-and-click failing validation`, async ({
test(`Sweep point-and-click failing validation`, async ({
context,
page,
homePage,
scene,
toolbar,
cmdBar,
}) => {
}) => {
const initialCode = `sketch001 = startSketchOn('YZ')
|> circle({
center = [0, 0],
@ -1042,7 +1054,10 @@ sketch002 = startSketchOn('XZ')
// One dumb hardcoded screen pixel value
const testPoint = { x: 700, y: 250 }
const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const [clickOnSketch2] = scene.makeMouseHelpers(testPoint.x - 50, testPoint.y)
const [clickOnSketch2] = scene.makeMouseHelpers(
testPoint.x - 50,
testPoint.y
)
await test.step(`Look for sketch001`, async () => {
await toolbar.closePane('code')
@ -1078,12 +1093,12 @@ sketch002 = startSketchOn('XZ')
await page.waitForTimeout(500)
await cmdBar.progressCmdBar()
await expect(
page.getByText('Unable to sweep with the provided selection')
page.getByText('Unable to sweep with the current selection. Reason:')
).toBeVisible()
})
})
})
test(`Fillet point-and-click`, async ({
test(`Fillet point-and-click`, async ({
context,
page,
homePage,
@ -1091,7 +1106,7 @@ test(`Fillet point-and-click`, async ({
editor,
toolbar,
cmdBar,
}) => {
}) => {
// Code samples
const initialCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-12, -6], %)
@ -1100,7 +1115,7 @@ test(`Fillet point-and-click`, async ({
|> line(end = [0, -12])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(-12, sketch001)
extrude001 = extrude(sketch001, length = -12)
`
const firstFilletDeclaration = 'fillet({ radius = 5, tags = [seg01] }, %)'
const secondFilletDeclaration =
@ -1183,7 +1198,7 @@ extrude001 = extrude(-12, sketch001)
currentArgKey: 'radius',
currentArgValue: '5',
headerArguments: {
Selection: '1 face',
Selection: '1 segment',
Radius: '',
},
stage: 'arguments',
@ -1192,7 +1207,7 @@ extrude001 = extrude(-12, sketch001)
await cmdBar.expectState({
commandName: 'Fillet',
headerArguments: {
Selection: '1 face',
Selection: '1 segment',
Radius: '5',
},
stage: 'review',
@ -1296,9 +1311,173 @@ extrude001 = extrude(-12, sketch001)
lowTolerance
)
})
})
test(`Chamfer point-and-click`, async ({
// Test 3: Delete fillets
await test.step('Delete fillet via feature tree selection', async () => {
await test.step('Open Feature Tree Pane', async () => {
await toolbar.openPane('feature-tree')
await page.waitForTimeout(500)
})
await test.step('Delete fillet via feature tree selection', async () => {
await editor.expectEditor.toContain(secondFilletDeclaration)
const operationButton = await toolbar.getFeatureTreeOperation(
'Fillet',
1
)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Backspace')
await page.waitForTimeout(500)
await scene.expectPixelColor(edgeColorWhite, secondEdgeLocation, 15) // deleted
await editor.expectEditor.not.toContain(secondFilletDeclaration)
await scene.expectPixelColor(filletColor, firstEdgeLocation, 15) // stayed
})
})
})
test(`Fillet point-and-click delete`, async ({
context,
page,
homePage,
scene,
editor,
toolbar,
}) => {
// Code samples
const initialCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-12, -6], %)
|> line(end = [0, 12])
|> line(end = [24, 0], tag = $seg02)
|> line(end = [0, -12])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg01)
|> close()
extrude001 = extrude(sketch001, length = -12)
|> fillet({ radius = 5, tags = [seg01] }, %) // fillet01
|> fillet({ radius = 5, tags = [seg02] }, %) // fillet02
fillet03 = fillet({ radius = 5, tags = [getOppositeEdge(seg01)]}, extrude001)
fillet04 = fillet({ radius = 5, tags = [getOppositeEdge(seg02)]}, extrude001)
`
const pipedFilletDeclaration = 'fillet({ radius = 5, tags = [seg01] }, %)'
const secondPipedFilletDeclaration =
'fillet({ radius = 5, tags = [seg02] }, %)'
const standaloneFilletDeclaration =
'fillet03 = fillet({ radius = 5, tags = [getOppositeEdge(seg01)]}, extrude001)'
const secondStandaloneFilletDeclaration =
'fillet04 = fillet({ radius = 5, tags = [getOppositeEdge(seg02)]}, extrude001)'
// Locators
const pipedFilletEdgeLocation = { x: 600, y: 193 }
const standaloneFilletEdgeLocation = { x: 600, y: 383 }
const bodyLocation = { x: 630, y: 290 }
// Colors
const edgeColorWhite: [number, number, number] = [248, 248, 248]
const bodyColor: [number, number, number] = [155, 155, 155]
const filletColor: [number, number, number] = [127, 127, 127]
const backgroundColor: [number, number, number] = [30, 30, 30]
const lowTolerance = 20
const highTolerance = 40
// Setup
await test.step(`Initial test setup`, async () => {
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
// verify modeling scene is loaded
await scene.expectPixelColor(
backgroundColor,
standaloneFilletEdgeLocation,
lowTolerance
)
// wait for stream to load
await scene.expectPixelColor(bodyColor, bodyLocation, highTolerance)
})
// Test
await test.step('Delete fillet via feature tree selection', async () => {
await test.step('Open Feature Tree Pane', async () => {
await toolbar.openPane('feature-tree')
await page.waitForTimeout(500)
})
await test.step('Delete piped fillet via feature tree selection', async () => {
await test.step('Verify all fillets are present in the editor', async () => {
await editor.expectEditor.toContain(pipedFilletDeclaration)
await editor.expectEditor.toContain(secondPipedFilletDeclaration)
await editor.expectEditor.toContain(standaloneFilletDeclaration)
await editor.expectEditor.toContain(secondStandaloneFilletDeclaration)
})
await test.step('Verify test fillets are present in the scene', async () => {
await scene.expectPixelColor(
filletColor,
pipedFilletEdgeLocation,
lowTolerance
)
await scene.expectPixelColor(
backgroundColor,
standaloneFilletEdgeLocation,
lowTolerance
)
})
await test.step('Delete piped fillet', async () => {
const operationButton = await toolbar.getFeatureTreeOperation(
'Fillet',
0
)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Backspace')
await page.waitForTimeout(500)
})
await test.step('Verify piped fillet is deleted but other fillets are not (in the editor)', async () => {
await editor.expectEditor.not.toContain(pipedFilletDeclaration)
await editor.expectEditor.toContain(secondPipedFilletDeclaration)
await editor.expectEditor.toContain(standaloneFilletDeclaration)
await editor.expectEditor.toContain(secondStandaloneFilletDeclaration)
})
await test.step('Verify piped fillet is deleted but non-piped is not (in the scene)', async () => {
await scene.expectPixelColor(
edgeColorWhite, // you see edge because fillet is deleted
pipedFilletEdgeLocation,
lowTolerance
)
await scene.expectPixelColor(
backgroundColor, // you see background because fillet is not deleted
standaloneFilletEdgeLocation,
lowTolerance
)
})
})
await test.step('Delete non-piped fillet via feature tree selection', async () => {
await test.step('Delete non-piped fillet', async () => {
const operationButton = await toolbar.getFeatureTreeOperation(
'Fillet',
1
)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Backspace')
await page.waitForTimeout(500)
})
await test.step('Verify non-piped fillet is deleted but other two fillets are not (in the editor)', async () => {
await editor.expectEditor.toContain(secondPipedFilletDeclaration)
await editor.expectEditor.not.toContain(standaloneFilletDeclaration)
await editor.expectEditor.toContain(secondStandaloneFilletDeclaration)
})
await test.step('Verify non-piped fillet is deleted but piped is not (in the scene)', async () => {
await scene.expectPixelColor(
edgeColorWhite,
standaloneFilletEdgeLocation,
lowTolerance
)
})
})
})
})
test(`Chamfer point-and-click`, async ({
context,
page,
homePage,
@ -1306,7 +1485,7 @@ test(`Chamfer point-and-click`, async ({
editor,
toolbar,
cmdBar,
}) => {
}) => {
// Code samples
const initialCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-12, -6], %)
@ -1398,7 +1577,7 @@ extrude001 = extrude(sketch001, length = -12)
currentArgKey: 'length',
currentArgValue: '5',
headerArguments: {
Selection: '1 face',
Selection: '1 segment',
Length: '',
},
stage: 'arguments',
@ -1407,7 +1586,7 @@ extrude001 = extrude(sketch001, length = -12)
await cmdBar.expectState({
commandName: 'Chamfer',
headerArguments: {
Selection: '1 face',
Selection: '1 segment',
Length: '5',
},
stage: 'review',
@ -1425,7 +1604,11 @@ extrude001 = extrude(sketch001, length = -12)
})
await test.step(`Confirm scene has changed`, async () => {
await scene.expectPixelColor(chamferColor, firstEdgeLocation, lowTolerance)
await scene.expectPixelColor(
chamferColor,
firstEdgeLocation,
lowTolerance
)
})
// Test 2: Command bar flow without preselected edges
@ -1511,13 +1694,179 @@ extrude001 = extrude(sketch001, length = -12)
lowTolerance
)
})
})
const shellPointAndClickCapCases = [
// Test 3: Delete chamfer via feature tree selection
await test.step('Open Feature Tree Pane', async () => {
await toolbar.openPane('feature-tree')
await page.waitForTimeout(500)
})
await test.step('Delete chamfer via feature tree selection', async () => {
const operationButton = await toolbar.getFeatureTreeOperation(
'Chamfer',
1
)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Backspace')
await page.waitForTimeout(500)
await scene.expectPixelColor(edgeColorWhite, secondEdgeLocation, 15) // deleted
await scene.expectPixelColor(chamferColor, firstEdgeLocation, 15) // stayed
})
})
test(`Chamfer point-and-click delete`, async ({
context,
page,
homePage,
scene,
editor,
toolbar,
}) => {
// Code samples
const initialCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-12, -6], %)
|> line(end = [0, 12])
|> line(end = [24, 0], tag = $seg02)
|> line(end = [0, -12])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg01)
|> close()
extrude001 = extrude(sketch001, length = -12)
|> chamfer({ length = 5, tags = [seg01] }, %) // chamfer01
|> chamfer({ length = 5, tags = [seg02] }, %) // chamfer02
chamfer03 = chamfer({ length = 5, tags = [getOppositeEdge(seg01)]}, extrude001)
chamfer04 = chamfer({ length = 5, tags = [getOppositeEdge(seg02)]}, extrude001)
`
const pipedChamferDeclaration = 'chamfer({ length = 5, tags = [seg01] }, %)'
const secondPipedChamferDeclaration =
'chamfer({ length = 5, tags = [seg02] }, %)'
const standaloneChamferDeclaration =
'chamfer03 = chamfer({ length = 5, tags = [getOppositeEdge(seg01)]}, extrude001)'
const secondStandaloneChamferDeclaration =
'chamfer04 = chamfer({ length = 5, tags = [getOppositeEdge(seg02)]}, extrude001)'
// Locators
const pipedChamferEdgeLocation = { x: 600, y: 193 }
const standaloneChamferEdgeLocation = { x: 600, y: 383 }
const bodyLocation = { x: 630, y: 290 }
// Colors
const edgeColorWhite: [number, number, number] = [248, 248, 248]
const bodyColor: [number, number, number] = [155, 155, 155]
const chamferColor: [number, number, number] = [168, 168, 168]
const backgroundColor: [number, number, number] = [30, 30, 30]
const lowTolerance = 20
const highTolerance = 40
// Setup
await test.step(`Initial test setup`, async () => {
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
// verify modeling scene is loaded
await scene.expectPixelColor(
backgroundColor,
standaloneChamferEdgeLocation,
lowTolerance
)
// wait for stream to load
await scene.expectPixelColor(bodyColor, bodyLocation, highTolerance)
})
// Test
await test.step('Delete chamfer via feature tree selection', async () => {
await test.step('Open Feature Tree Pane', async () => {
await toolbar.openPane('feature-tree')
await page.waitForTimeout(500)
})
await test.step('Delete piped chamfer via feature tree selection', async () => {
await test.step('Verify all chamfers are present in the editor', async () => {
await editor.expectEditor.toContain(pipedChamferDeclaration)
await editor.expectEditor.toContain(secondPipedChamferDeclaration)
await editor.expectEditor.toContain(standaloneChamferDeclaration)
await editor.expectEditor.toContain(
secondStandaloneChamferDeclaration
)
})
await test.step('Verify test chamfers are present in the scene', async () => {
await scene.expectPixelColor(
chamferColor,
pipedChamferEdgeLocation,
lowTolerance
)
await scene.expectPixelColor(
backgroundColor,
standaloneChamferEdgeLocation,
lowTolerance
)
})
await test.step('Delete piped chamfer', async () => {
const operationButton = await toolbar.getFeatureTreeOperation(
'Chamfer',
0
)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Backspace')
await page.waitForTimeout(500)
})
await test.step('Verify piped chamfer is deleted but other chamfers are not (in the editor)', async () => {
await editor.expectEditor.not.toContain(pipedChamferDeclaration)
await editor.expectEditor.toContain(secondPipedChamferDeclaration)
await editor.expectEditor.toContain(standaloneChamferDeclaration)
await editor.expectEditor.toContain(
secondStandaloneChamferDeclaration
)
})
await test.step('Verify piped chamfer is deleted but non-piped is not (in the scene)', async () => {
await scene.expectPixelColor(
edgeColorWhite, // you see edge color because chamfer is deleted
pipedChamferEdgeLocation,
lowTolerance
)
await scene.expectPixelColor(
backgroundColor, // you see background color instead of edge because it's chamfered
standaloneChamferEdgeLocation,
lowTolerance
)
})
})
await test.step('Delete non-piped chamfer via feature tree selection', async () => {
await test.step('Delete non-piped chamfer', async () => {
const operationButton = await toolbar.getFeatureTreeOperation(
'Chamfer',
1
)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Backspace')
await page.waitForTimeout(500)
})
await test.step('Verify non-piped chamfer is deleted but other two chamfers are not (in the editor)', async () => {
await editor.expectEditor.toContain(secondPipedChamferDeclaration)
await editor.expectEditor.not.toContain(standaloneChamferDeclaration)
await editor.expectEditor.toContain(
secondStandaloneChamferDeclaration
)
})
await test.step('Verify non-piped chamfer is deleted but piped is not (in the scene)', async () => {
await scene.expectPixelColor(
edgeColorWhite,
standaloneChamferEdgeLocation,
lowTolerance
)
})
})
})
})
const shellPointAndClickCapCases = [
{ shouldPreselect: true },
{ shouldPreselect: false },
]
shellPointAndClickCapCases.forEach(({ shouldPreselect }) => {
]
shellPointAndClickCapCases.forEach(({ shouldPreselect }) => {
test(`Shell point-and-click cap (preselected sketches: ${shouldPreselect})`, async ({
context,
page,
@ -1527,8 +1876,6 @@ shellPointAndClickCapCases.forEach(({ shouldPreselect }) => {
toolbar,
cmdBar,
}) => {
// TODO: fix this test on windows after the electron migration
test.skip(process.platform === 'win32', 'Skip on windows')
const initialCode = `sketch001 = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 30 }, %)
extrude001 = extrude(sketch001, length = 30)
@ -1611,9 +1958,9 @@ shellPointAndClickCapCases.forEach(({ shouldPreselect }) => {
await scene.expectPixelColor([146, 146, 146], testPoint, 15)
})
})
})
})
test('Shell point-and-click wall', async ({
test('Shell point-and-click wall', async ({
context,
page,
homePage,
@ -1621,7 +1968,7 @@ test('Shell point-and-click wall', async ({
editor,
toolbar,
cmdBar,
}) => {
}) => {
const initialCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-20, 20], %)
|> xLine(40, %)
@ -1700,9 +2047,9 @@ extrude001 = extrude(sketch001, length = 40)
await page.keyboard.press('Backspace')
await scene.expectPixelColor([99, 99, 99], testPoint, 15)
})
})
})
const shellSketchOnFacesCases = [
const shellSketchOnFacesCases = [
`sketch001 = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 100 }, %)
|> extrude(length = 100)
@ -1719,8 +2066,8 @@ sketch002 = startSketchOn(extrude001, 'END')
|> circle({ center = [0, 0], radius = 50 }, %)
extrude002 = extrude(sketch002, length = 50)
`,
]
shellSketchOnFacesCases.forEach((initialCode, index) => {
]
shellSketchOnFacesCases.forEach((initialCode, index) => {
const hasExtrudesInPipe = index === 0
test(`Shell point-and-click sketch on face (extrudes in pipes: ${hasExtrudesInPipe})`, async ({
context,
@ -1792,9 +2139,9 @@ shellSketchOnFacesCases.forEach((initialCode, index) => {
await scene.expectPixelColor([73, 73, 73], testPoint, 15)
})
})
})
})
test(`Shell dry-run validation rejects sweeps`, async ({
test(`Shell dry-run validation rejects sweeps`, async ({
context,
page,
homePage,
@ -1802,7 +2149,7 @@ test(`Shell dry-run validation rejects sweeps`, async ({
editor,
toolbar,
cmdBar,
}) => {
}) => {
const initialCode = `sketch001 = startSketchOn('YZ')
|> circle({
center = [0, 0],
@ -1846,8 +2193,169 @@ sweep001 = sweep({ path = sketch002 }, sketch001)
await page.waitForTimeout(500)
await cmdBar.progressCmdBar()
await expect(
page.getByText('Unable to shell with the provided selection')
page.getByText('Unable to shell with the current selection. Reason:')
).toBeVisible()
await page.waitForTimeout(1000)
})
})
test.describe('Revolve point and click workflows', () => {
test('Base case workflow, auto spam continue in command bar', async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
const initialCode = `
sketch001 = startSketchOn('XZ')
|> startProfileAt([-100.0, 100.0], %)
|> angledLine([0, 200.0], %, $rectangleSegmentA001)
|> angledLine([segAng(rectangleSegmentA001) - 90, 200], %, $rectangleSegmentB001)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %, $rectangleSegmentC001)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(sketch001, length = 200)
sketch002 = startSketchOn(extrude001, rectangleSegmentA001)
|> startProfileAt([-66.77, 84.81], %)
|> angledLine([180, 27.08], %, $rectangleSegmentA002)
|> angledLine([
segAng(rectangleSegmentA002) - 90,
27.8
], %, $rectangleSegmentB002)
|> angledLine([
segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002)
], %, $rectangleSegmentC002)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
`
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
// select line of code
const codeToSelecton = `segAng(rectangleSegmentA002) - 90,`
// revolve
await page.getByText(codeToSelecton).click()
await toolbar.revolveButton.click()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
const newCodeToFind = `revolve001 = revolve({ angle = 360, axis = 'X' }, sketch002)`
expect(editor.expectEditor.toContain(newCodeToFind)).toBeTruthy()
})
test('revolve surface around edge from an extruded solid2d', async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
const initialCode = `
sketch001 = startSketchOn('XZ')
|> startProfileAt([-102.57, 101.72], %)
|> angledLine([0, 202.6], %, $rectangleSegmentA001)
|> angledLine([
segAng(rectangleSegmentA001) - 90,
202.6
], %, $rectangleSegmentB001)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %, $rectangleSegmentC001)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(sketch001, length = 50)
sketch002 = startSketchOn(extrude001, rectangleSegmentA001)
|> circle({
center = [-11.34, 10.0],
radius = 8.69
}, %)
`
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
// select line of code
const codeToSelecton = `center = [-11.34, 10.0]`
// revolve
await page.getByText(codeToSelecton).click()
await toolbar.revolveButton.click()
await page.getByText('Edge', { exact: true }).click()
const lineCodeToSelection = `|> angledLine([0, 202.6], %, $rectangleSegmentA001)`
await page.getByText(lineCodeToSelection).click()
await cmdBar.progressCmdBar()
const newCodeToFind = `revolve001 = revolve({angle = 360, axis = getOppositeEdge(rectangleSegmentA001)}, sketch002) `
expect(editor.expectEditor.toContain(newCodeToFind)).toBeTruthy()
})
test('revolve sketch circle around line segment from startProfileAt sketch', async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
const initialCode = `
sketch002 = startSketchOn('XY')
|> startProfileAt([-2.02, 1.79], %)
|> xLine(2.6, %)
sketch001 = startSketchOn('-XY')
|> startProfileAt([-0.48, 1.25], %)
|> angledLine([0, 2.38], %, $rectangleSegmentA001)
|> angledLine([segAng(rectangleSegmentA001) - 90, 2.4], %, $rectangleSegmentB001)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %, $rectangleSegmentC001)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(sketch001, length = 5)
sketch003 = startSketchOn(extrude001, 'START')
|> circle({
center = [-0.69, 0.56],
radius = 0.28
}, %)
`
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
// select line of code
const codeToSelecton = `center = [-0.69, 0.56]`
// revolve
await page.getByText(codeToSelecton).click()
await toolbar.revolveButton.click()
await page.getByText('Edge', { exact: true }).click()
const lineCodeToSelection = `|> xLine(2.6, %)`
await page.getByText(lineCodeToSelection).click()
await cmdBar.progressCmdBar()
const newCodeToFind = `revolve001 = revolve({ angle = 360, axis = seg01 }, sketch003)`
expect(editor.expectEditor.toContain(newCodeToFind)).toBeTruthy()
})
})
})

View File

@ -185,7 +185,7 @@ test(
// error text on hover
await page.hover('.cm-lint-marker-error')
const crypticErrorText = `Expected a tag identifier`
const crypticErrorText = `Expected a tag declarator`
await expect(page.getByText(crypticErrorText).first()).toBeVisible()
// black pixel means the scene has been cleared.
@ -572,7 +572,7 @@ test(
fs.utimesSync(`${dir}/lego/main.kcl`, _1995, _1995)
})
await page.setBodyDimensions({ width: 1200, height: 500 })
await page.setBodyDimensions({ width: 1200, height: 600 })
page.on('console', console.log)
@ -1527,12 +1527,10 @@ test(
{ tag: '@electron' },
async ({ context, page, cmdBar, homePage }, testInfo) => {
await context.folderSetupFn(async (dir) => {
await Promise.all([
fsp.mkdir(path.join(dir, 'router-template-slate'), { recursive: true }),
fsp.mkdir(path.join(dir, 'bracket'), { recursive: true }),
])
await Promise.all([
fsp.copyFile(
await fsp.mkdir(path.join(dir, 'router-template-slate'), {
recursive: true,
})
await fsp.copyFile(
path.join(
'src',
'wasm-lib',
@ -1542,8 +1540,9 @@ test(
'router-template-slate.kcl'
),
path.join(dir, 'router-template-slate', 'main.kcl')
),
fsp.copyFile(
)
await fsp.mkdir(path.join(dir, 'bracket'), { recursive: true })
await fsp.copyFile(
path.join(
'src',
'wasm-lib',
@ -1553,8 +1552,7 @@ test(
'focusrite_scarlett_mounting_braket.kcl'
),
path.join(dir, 'bracket', 'main.kcl')
),
])
)
})
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })

View File

@ -35,7 +35,7 @@ sketch003 = startSketchOn('XY')
extrude003 = extrude(sketch003, length = 20)
`
test.describe('Check the happy path, for basic changing color', () => {
test.fixme('Check the happy path, for basic changing color', () => {
const cases = [
{
desc: 'User accepts change',
@ -134,7 +134,7 @@ test.describe('Check the happy path, for basic changing color', () => {
}
})
test.describe('bad path', () => {
test.describe('bad path', { tag: ['@skipWin'] }, () => {
test(`bad edit prompt`, async ({
context,
homePage,

View File

@ -5,7 +5,7 @@ import { getUtils, executorInputPath } from './test-utils'
import { TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR } from './storageStates'
import { bracket } from 'lib/exampleKcl'
test.describe('Regression tests', () => {
test.describe('Regression tests', { tag: ['@skipWin'] }, () => {
// bugs we found that don't fit neatly into other categories
test('bad model has inline error #3251', async ({
context,
@ -239,12 +239,6 @@ extrude001 = extrude(sketch001, length = 50)
'Position _ Is Out Of Range... regression test',
{ tag: ['@skipWin'] },
async ({ context, page, homePage }) => {
// SKip on windows, its being weird.
test.skip(
process.platform === 'win32',
'This test is being weird on windows'
)
const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setBodyDimensions({ width: 1200, height: 500 })
@ -425,13 +419,6 @@ extrude001 = extrude(sketch001, length = 50)
'ensure you can not export while an export is already going',
{ tag: ['@skipLinux', '@skipWin'] },
async ({ page, homePage }) => {
// This is being weird on ubuntu and windows.
test.skip(
// eslint-disable-next-line jest/valid-title
process.platform === 'linux' || process.platform === 'win32',
'This test is being weird on ubuntu'
)
const u = await getUtils(page)
await test.step('Set up the code and durations', async () => {
await page.addInitScript(
@ -560,8 +547,6 @@ extrude001 = extrude(sketch001, length = 50)
page,
homePage,
}) => {
// TODO: fix this test on windows after the electron migration
test.skip(process.platform === 'win32', 'Skip on windows')
const u = await getUtils(page)
// Constants and locators

View File

@ -10,7 +10,7 @@ import {
} from './test-utils'
import { uuidv4, roundOff } from 'lib/utils'
test.describe('Sketch tests', () => {
test.describe('Sketch tests', { tag: ['@skipWin'] }, () => {
test('multi-sketch file shows multiple Edit Sketch buttons', async ({
page,
context,
@ -312,7 +312,10 @@ test.describe('Sketch tests', () => {
|> line(end = [1.97, 2.06])
|> close()`)
}
test('code pane open at start-handles', async ({ page, homePage }) => {
test(
'code pane open at start-handles',
{ tag: ['@skipWin'] },
async ({ page, homePage }) => {
// Load the app with the code panes
await page.addInitScript(async () => {
localStorage.setItem(
@ -326,9 +329,13 @@ test.describe('Sketch tests', () => {
)
})
await doEditSegmentsByDraggingHandle(page, homePage, ['code'])
})
}
)
test('code pane closed at start-handles', async ({ page, homePage }) => {
test(
'code pane closed at start-handles',
{ tag: ['@skipWin'] },
async ({ page, homePage }) => {
// Load the app with the code panes
await page.addInitScript(async (persistModelingContext) => {
localStorage.setItem(
@ -337,7 +344,8 @@ test.describe('Sketch tests', () => {
)
}, PERSIST_MODELING_CONTEXT)
await doEditSegmentsByDraggingHandle(page, homePage, [])
})
}
)
})
test('Can edit a circle center and radius by dragging its handles', async ({
@ -636,8 +644,6 @@ test.describe('Sketch tests', () => {
|> revolve({ axis = "X" }, %)`)
})
test('Can add multiple sketches', async ({ page, homePage }) => {
// TODO: fix this test on windows after the electron migration
test.skip(process.platform === 'win32', 'Skip on windows')
const u = await getUtils(page)
const viewportSize = { width: 1200, height: 500 }
@ -835,8 +841,6 @@ test.describe('Sketch tests', () => {
page,
homePage,
}) => {
// TODO: fix this test on windows after the electron migration
test.skip(process.platform === 'win32', 'Skip on windows')
// this was a regression https://github.com/KittyCAD/modeling-app/issues/2832
await page.addInitScript(async () => {
localStorage.setItem(
@ -886,7 +890,7 @@ test.describe('Sketch tests', () => {
// sketch selection should already have been made. "Selection: 1 face" only show up when the selection has been made already
// otherwise the cmdbar would be waiting for a selection.
await expect(
page.getByRole('button', { name: 'selection : 1 face', exact: false })
page.getByRole('button', { name: 'selection : 1 segment', exact: false })
).toBeVisible({
timeout: 10_000,
})
@ -1427,7 +1431,7 @@ test.describe('Redirecting to home page and back to the original file should cle
'persistCode',
` sketch001 = startSketchOn('XZ')
|> startProfileAt([256.85, 14.41], %)
|> lineTo([0, 211.07], %)
|> line(endAbsolute = [0, 211.07])
`
)
})

View File

@ -55,13 +55,6 @@ test.skip(
'exports of each format should work',
{ tag: ['@snapshot', '@skipWin', '@skipMacos'] },
async ({ page, context }) => {
// skip on macos and windows.
test.skip(
// eslint-disable-next-line jest/valid-title
process.platform === 'darwin' || process.platform === 'win32',
'Skip on macos and windows'
)
// FYI this test doesn't work with only engine running locally
// And you will need to have the KittyCAD CLI installed
const u = await getUtils(page)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -552,6 +552,16 @@ export async function getUtils(page: Page, test_?: typeof test) {
})
},
cloneFile: async (name: string) => {
return test?.step(`Cloning file '${name}'`, async () => {
await page
.locator('[data-testid="file-pane-scroll-container"] button')
.filter({ hasText: name })
.click({ button: 'right' })
await page.getByTestId('context-menu-clone').click()
})
},
selectFile: async (name: string) => {
return test?.step(`Select ${name}`, async () => {
await page

View File

@ -3,13 +3,8 @@ import { EngineCommand } from 'lang/std/artifactGraph'
import { uuidv4 } from 'lib/utils'
import { getUtils } from './test-utils'
test.describe('Testing Camera Movement', () => {
test.describe('Testing Camera Movement', { tag: ['@skipWin'] }, () => {
test('Can move camera reliably', async ({ page, context, homePage }) => {
// TODO: fix this test on windows too after the electron migration
const winOrMac =
process.platform === 'win32' || process.platform === 'darwin'
// eslint-disable-next-line
test.skip(winOrMac, 'Skip on windows')
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
@ -347,8 +342,6 @@ test.describe('Testing Camera Movement', () => {
homePage,
page,
}) => {
// TODO: fix this test on windows after the electron migration
test.skip(process.platform === 'win32', 'Skip on windows')
/**
* Currently we only allow zooming by scroll when no other camera movement is happening,
* set within cameraMouseDragGuards in cameraControls.ts,

View File

@ -9,7 +9,7 @@ import {
import { XOR } from 'lib/utils'
import path from 'node:path'
test.describe('Testing constraints', () => {
test.describe('Testing constraints', { tag: ['@skipWin'] }, () => {
test('Can constrain line length', async ({ page, homePage }) => {
await page.addInitScript(async () => {
localStorage.setItem(
@ -129,8 +129,6 @@ test.describe('Testing constraints', () => {
await expect(page.getByTestId('segment-overlay')).toHaveCount(4)
})
test.describe('Test perpendicular distance constraint', () => {
// TODO: fix this test on windows after the electron migration
test.skip(process.platform === 'win32', 'Skip on windows')
const cases = [
{
testName: 'Add variable',
@ -251,8 +249,6 @@ test.describe('Testing constraints', () => {
}
})
test.describe('Test distance between constraint', () => {
// TODO: fix this test on windows after the electron migration
test.skip(process.platform === 'win32', 'Skip on windows')
const cases = [
{
testName: 'Add variable',
@ -472,8 +468,6 @@ test.describe('Testing constraints', () => {
}
})
test.describe('Test Angle constraint double segment selection', () => {
// TODO: fix this test on windows after the electron migration
test.skip(process.platform === 'win32', 'Skip on windows')
const cases = [
{
testName: 'Add variable',
@ -664,8 +658,6 @@ test.describe('Testing constraints', () => {
}
})
test.describe('Test Length constraint single selection', () => {
// TODO: fix this test on windows after the electron migration
test.skip(process.platform === 'win32', 'Skip on windows')
const cases = [
{
testName: 'Length - Add variable',
@ -851,8 +843,6 @@ part002 = startSketchOn('XZ')
}
})
test.describe('Two segment - no modal constraints', () => {
// TODO: fix this test on windows after the electron migration
test.skip(process.platform === 'win32', 'Skip on windows')
const cases = [
{
codeAfter: `|> angledLine([83, segLen(seg01)], %)`,

View File

@ -3,9 +3,7 @@ import { getUtils } from './test-utils'
import { uuidv4 } from 'lib/utils'
import { TEST_CODE_GIZMO } from './storageStates'
test.describe('Testing Gizmo', () => {
// TODO: fix this test on windows after the electron migration
test.skip(process.platform === 'win32', 'Skip on windows')
test.describe('Testing Gizmo', { tag: ['@skipWin'] }, () => {
const cases = [
{
testDescription: 'top view',

View File

@ -69,7 +69,6 @@ test.describe('Testing in-app sample loading', () => {
await confirmButton.click()
await editor.expectEditor.toContain('// ' + newSample.title)
await expect(unitsToast('in')).toBeVisible()
})
})
@ -158,7 +157,6 @@ test.describe('Testing in-app sample loading', () => {
await editor.expectEditor.toContain('// ' + sampleOne.title)
await expect(newlyCreatedFile(sampleOne.file)).toBeVisible()
await expect(projectMenuButton).toContainText(sampleOne.file)
await expect(unitsToast('in')).toBeVisible()
})
await test.step(`Now overwrite the current file`, async () => {
@ -188,7 +186,6 @@ test.describe('Testing in-app sample loading', () => {
await expect(newlyCreatedFile(sampleOne.file)).toBeVisible()
await expect(newlyCreatedFile(sampleTwo.file)).not.toBeVisible()
await expect(projectMenuButton).toContainText(sampleOne.file)
await expect(unitsToast('mm')).toBeVisible()
})
}
)

View File

@ -5,10 +5,10 @@ import { LineInputsType } from 'lang/std/sketchcombos'
import { uuidv4 } from 'lib/utils'
import { EditorFixture } from './fixtures/editorFixture'
test.describe('Testing segment overlays', () => {
// TODO: fix this test on windows after the electron migration
test.skip(process.platform === 'win32', 'Skip on windows')
test.describe('Hover over a segment should show its overlay, hovering over the input overlays should show its popover, clicking the input overlay should constrain/unconstrain it:\nfor the following segments', () => {
test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
test.fixme(
'Hover over a segment should show its overlay, hovering over the input overlays should show its popover, clicking the input overlay should constrain/unconstrain it:\nfor the following segments',
() => {
// TODO: fix this test on mac after the electron migration
test.skip(process.platform === 'darwin', 'Skip on mac')
/**
@ -557,7 +557,8 @@ test.describe('Testing segment overlays', () => {
'angledLineOfYLength({ angle = -91, length = 19 + 0 }, %)',
expectAfterUnconstrained:
'angledLineOfYLength({ angle = angle002, length = 19 + 0 }, %)',
expectFinal: 'angledLineOfYLength({ angle = -91, length = 19 + 0 }, %)',
expectFinal:
'angledLineOfYLength({ angle = -91, length = 19 + 0 }, %)',
ang: ang + 180,
steps: 6,
locator: '[data-overlay-toolbar-index="8"]',
@ -656,7 +657,9 @@ test.describe('Testing segment overlays', () => {
locator: '[data-overlay-toolbar-index="9"]',
})
const angledLineToY = await u.getBoundingBox(`[data-overlay-index="10"]`)
const angledLineToY = await u.getBoundingBox(
`[data-overlay-index="10"]`
)
ang = await u.getAngle(`[data-overlay-index="10"]`)
console.log('angledLineToY')
await clickUnconstrained({
@ -677,7 +680,8 @@ test.describe('Testing segment overlays', () => {
constraintType: 'yAbsolute',
expectBeforeUnconstrained:
'angledLineToY({ angle = 89, to = 9.14 + 0 }, %)',
expectAfterUnconstrained: 'angledLineToY({ angle = 89, to = 9.14 }, %)',
expectAfterUnconstrained:
'angledLineToY({ angle = 89, to = 9.14 }, %)',
expectFinal: 'angledLineToY({ angle = 89, to = yAbs001 }, %)',
ang: ang + 180,
locator: '[data-overlay-toolbar-index="10"]',
@ -857,7 +861,8 @@ test.describe('Testing segment overlays', () => {
constraintType: 'xAbsolute',
expectBeforeUnconstrained:
'circle({ center = [1 + 0, 0], radius = 8 }, %)',
expectAfterUnconstrained: 'circle({ center = [1, 0], radius = 8 }, %)',
expectAfterUnconstrained:
'circle({ center = [1, 0], radius = 8 }, %)',
expectFinal: 'circle({ center = [xAbs001, 0], radius = 8 }, %)',
ang: ang + 105,
steps: 6,
@ -890,7 +895,8 @@ test.describe('Testing segment overlays', () => {
locator: '[data-overlay-toolbar-index="0"]',
})
})
})
}
)
test.describe('Testing deleting a segment', () => {
const _deleteSegmentSequence =
(page: Page, editor: EditorFixture) =>

View File

@ -5,15 +5,12 @@ import { Coords2d } from 'lang/std/sketch'
import { KCL_DEFAULT_LENGTH } from 'lib/constants'
import { uuidv4 } from 'lib/utils'
test.describe('Testing selections', () => {
test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
test.setTimeout(90_000)
test(
'Selections work on fresh and edited sketch',
{ tag: ['@skipWin'] },
async ({ page, homePage }) => {
// Skip on windows its being weird.
test.skip(process.platform === 'win32', 'Skip on windows')
test('Selections work on fresh and edited sketch', async ({
page,
homePage,
}) => {
// tests mapping works on fresh sketch and edited sketch
// tests using hovers which is the same as selections, because if
// source ranges are wrong, hovers won't work
@ -160,9 +157,7 @@ test.describe('Testing selections', () => {
// check the same selection again by putting cursor in code first then selecting axis
await test.step(`Same selection but code selection then axis`, async () => {
await page
.getByText(` |> xLine(${commonPoints.num2 * -1}, %)`)
.click()
await page.getByText(` |> xLine(${commonPoints.num2 * -1}, %)`).click()
await page.keyboard.down('Shift')
await constrainButton.click()
await expect(absXButton).toBeDisabled()
@ -190,9 +185,7 @@ test.describe('Testing selections', () => {
await expect(page.locator('.cm-cursor')).toHaveCount(2)
await page.waitForTimeout(500)
await page.keyboard.up(
process.platform === 'linux' ? 'Control' : 'Meta'
)
await page.keyboard.up(process.platform === 'linux' ? 'Control' : 'Meta')
// clear selection by clicking on nothing
await emptySpaceClick()
@ -253,8 +246,7 @@ test.describe('Testing selections', () => {
await test.step(`Test hovering and selecting on edited sketch`, async () => {
await selectionSequence()
})
}
)
})
test('Solids should be select and deletable', async ({ page, homePage }) => {
test.setTimeout(90_000)
@ -492,8 +484,6 @@ test.describe('Testing selections', () => {
page,
homePage,
}) => {
// TODO: fix this test on windows after the electron migration
test.skip(process.platform === 'win32', 'Skip on windows')
const u = await getUtils(page)
await page.addInitScript(async (KCL_DEFAULT_LENGTH) => {
localStorage.setItem(

View File

@ -435,11 +435,6 @@ test.describe('Text-to-CAD tests', () => {
async ({ page, homePage }) => {
// Let this test run longer since we've seen it timeout.
test.setTimeout(180_000)
// skip on windows
test.skip(
process.platform === 'win32',
'This test is flaky, skipping for now'
)
const u = await getUtils(page)

View File

@ -75,3 +75,6 @@ publish:
channel: latest
releaseInfo:
releaseNotesFile: release-notes.md
protocols:
- name: Zoo Studio
schemes: ['zoo-studio']

View File

@ -9,23 +9,8 @@ const rootDir = process.cwd()
const config: ForgeConfig = {
packagerConfig: {
asar: true,
osxSign: (process.env.BUILD_RELEASE === 'true' && {}) || undefined,
osxNotarize:
(process.env.BUILD_RELEASE === 'true' && {
appleId: process.env.APPLE_ID || '',
appleIdPassword: process.env.APPLE_PASSWORD || '',
teamId: process.env.APPLE_TEAM_ID || '',
}) ||
undefined,
executableName: 'zoo-modeling-app',
icon: path.resolve(rootDir, 'assets', 'icon'),
protocols: [
{
name: 'Zoo Studio',
schemes: ['zoo-studio'],
},
],
extendInfo: 'Info.plist', // Information for file associations.
},
rebuildConfig: {},
makers: [],

2
interface.d.ts vendored
View File

@ -32,6 +32,7 @@ export interface IElectronAPI {
callback: (eventType: string, path: string) => void
) => void
readFile: typeof fs.readFile
copyFile: typeof fs.copyFile
watchFileOff: (path: string, key: string) => void
writeFile: (
path: string,
@ -65,6 +66,7 @@ export interface IElectronAPI {
VITE_KC_API_WS_MODELING_URL: string
VITE_KC_API_BASE_URL: string
VITE_KC_SITE_BASE_URL: string
VITE_KC_SITE_APP_URL: string
VITE_KC_SKIP_AUTH: string
VITE_KC_CONNECTION_TIMEOUT_MS: string
VITE_KC_DEV_TOKEN: string

View File

@ -103,11 +103,11 @@
"make:dev": "make dev",
"generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts",
"tron:start": "electron-forge start",
"tron:package": "electron-forge package",
"chrome:test": "PLATFORM=web NODE_ENV=development yarn playwright test --config=playwright.config.ts --project='Google Chrome' --grep-invert='@snapshot'",
"tron:test": "NODE_ENV=development yarn playwright test --config=playwright.electron.config.ts --grep-invert='@snapshot'",
"tronb:vite": "vite build -c vite.main.config.ts && vite build -c vite.preload.config.ts && vite build -c vite.renderer.config.ts",
"tronb:package": "electron-builder --config electron-builder.yml",
"tronb:vite:dev": "vite build -c vite.main.config.ts -m development && vite build -c vite.preload.config.ts -m development && vite build -c vite.renderer.config.ts -m development",
"tronb:vite:prod": "vite build -c vite.main.config.ts && vite build -c vite.preload.config.ts && vite build -c vite.renderer.config.ts",
"tronb:package:dev": "yarn tronb:vite:dev && electron-builder --config electron-builder.yml",
"tronb:package:prod": "yarn tronb:vite:prod && electron-builder --config electron-builder.yml --publish always",
"test-setup": "yarn install && yarn build:wasm",
"test": "vitest --mode development",
"test:unit": "vitest run --mode development --exclude **/kclSamples.test.ts",
@ -116,10 +116,10 @@
"test:playwright:electron:windows": "playwright test --config=playwright.electron.config.ts --grep-invert=\"@skipWin|@snapshot\" --quiet",
"test:playwright:electron:macos": "playwright test --config=playwright.electron.config.ts --grep-invert='@skipMacos|@snapshot' --quiet",
"test:playwright:electron:ubuntu": "playwright test --config=playwright.electron.config.ts --grep-invert='@skipLinux|@snapshot' --quiet",
"test:playwright:electron:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@snapshot'",
"test:playwright:electron:windows:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert=\"@skipWin|@snapshot\"",
"test:playwright:electron:macos:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@skipMacos|@snapshot'",
"test:playwright:electron:ubuntu:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@skipLinux|@snapshot'",
"test:playwright:electron:local": "yarn tronb:vite:dev && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@snapshot'",
"test:playwright:electron:windows:local": "yarn tronb:vite:dev && set NODE_ENV='development' && playwright test --config=playwright.electron.config.ts --grep-invert=\"@skipWin|@snapshot\"",
"test:playwright:electron:macos:local": "yarn tronb:vite:dev && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@skipMacos|@snapshot'",
"test:playwright:electron:ubuntu:local": "yarn tronb:vite:dev && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@skipLinux|@snapshot'",
"test:unit:local": "yarn simpleserver:bg && yarn test:unit; kill-port 3000",
"test:unit:kcl-samples:local": "yarn simpleserver:bg && yarn test:unit:kcl-samples; kill-port 3000"
},
@ -204,7 +204,7 @@
"vite": "^5.4.12",
"vite-plugin-package-version": "^1.1.0",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.6.0",
"vitest": "^1.6.1",
"vitest-webgl-canvas-mock": "^1.1.0",
"wasm-pack": "^0.13.1",
"ws": "^8.17.0",

View File

@ -29,7 +29,7 @@
"rollup": "^4.29.1",
"rollup-plugin-dts": "^6.1.1",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^2.1.8"
"vitest": "^2.1.9"
},
"files": [
"dist/"

View File

@ -313,62 +313,62 @@
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==
"@vitest/expect@2.1.8":
version "2.1.8"
resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-2.1.8.tgz#13fad0e8d5a0bf0feb675dcf1d1f1a36a1773bc1"
integrity sha512-8ytZ/fFHq2g4PJVAtDX57mayemKgDR6X3Oa2Foro+EygiOJHUXhCqBAAKQYYajZpFoIfvBCF1j6R6IYRSIUFuw==
"@vitest/expect@2.1.9":
version "2.1.9"
resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-2.1.9.tgz#b566ea20d58ea6578d8dc37040d6c1a47ebe5ff8"
integrity sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==
dependencies:
"@vitest/spy" "2.1.8"
"@vitest/utils" "2.1.8"
"@vitest/spy" "2.1.9"
"@vitest/utils" "2.1.9"
chai "^5.1.2"
tinyrainbow "^1.2.0"
"@vitest/mocker@2.1.8":
version "2.1.8"
resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-2.1.8.tgz#51dec42ac244e949d20009249e033e274e323f73"
integrity sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA==
"@vitest/mocker@2.1.9":
version "2.1.9"
resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-2.1.9.tgz#36243b27351ca8f4d0bbc4ef91594ffd2dc25ef5"
integrity sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==
dependencies:
"@vitest/spy" "2.1.8"
"@vitest/spy" "2.1.9"
estree-walker "^3.0.3"
magic-string "^0.30.12"
"@vitest/pretty-format@2.1.8", "@vitest/pretty-format@^2.1.8":
version "2.1.8"
resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-2.1.8.tgz#88f47726e5d0cf4ba873d50c135b02e4395e2bca"
integrity sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==
"@vitest/pretty-format@2.1.9", "@vitest/pretty-format@^2.1.9":
version "2.1.9"
resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-2.1.9.tgz#434ff2f7611689f9ce70cd7d567eceb883653fdf"
integrity sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==
dependencies:
tinyrainbow "^1.2.0"
"@vitest/runner@2.1.8":
version "2.1.8"
resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-2.1.8.tgz#b0e2dd29ca49c25e9323ea2a45a5125d8729759f"
integrity sha512-17ub8vQstRnRlIU5k50bG+QOMLHRhYPAna5tw8tYbj+jzjcspnwnwtPtiOlkuKC4+ixDPTuLZiqiWWQ2PSXHVg==
"@vitest/runner@2.1.9":
version "2.1.9"
resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-2.1.9.tgz#cc18148d2d797fd1fd5908d1f1851d01459be2f6"
integrity sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==
dependencies:
"@vitest/utils" "2.1.8"
"@vitest/utils" "2.1.9"
pathe "^1.1.2"
"@vitest/snapshot@2.1.8":
version "2.1.8"
resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-2.1.8.tgz#d5dc204f4b95dc8b5e468b455dfc99000047d2de"
integrity sha512-20T7xRFbmnkfcmgVEz+z3AU/3b0cEzZOt/zmnvZEctg64/QZbSDJEVm9fLnnlSi74KibmRsO9/Qabi+t0vCRPg==
"@vitest/snapshot@2.1.9":
version "2.1.9"
resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-2.1.9.tgz#24260b93f798afb102e2dcbd7e61c6dfa118df91"
integrity sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==
dependencies:
"@vitest/pretty-format" "2.1.8"
"@vitest/pretty-format" "2.1.9"
magic-string "^0.30.12"
pathe "^1.1.2"
"@vitest/spy@2.1.8":
version "2.1.8"
resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-2.1.8.tgz#bc41af3e1e6a41ae3b67e51f09724136b88fa447"
integrity sha512-5swjf2q95gXeYPevtW0BLk6H8+bPlMb4Vw/9Em4hFxDcaOxS+e0LOX4yqNxoHzMR2akEB2xfpnWUzkZokmgWDg==
"@vitest/spy@2.1.9":
version "2.1.9"
resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-2.1.9.tgz#cb28538c5039d09818b8bfa8edb4043c94727c60"
integrity sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==
dependencies:
tinyspy "^3.0.2"
"@vitest/utils@2.1.8":
version "2.1.8"
resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-2.1.8.tgz#f8ef85525f3362ebd37fd25d268745108d6ae388"
integrity sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==
"@vitest/utils@2.1.9":
version "2.1.9"
resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-2.1.9.tgz#4f2486de8a54acf7ecbf2c5c24ad7994a680a6c1"
integrity sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==
dependencies:
"@vitest/pretty-format" "2.1.8"
"@vitest/pretty-format" "2.1.9"
loupe "^3.1.2"
tinyrainbow "^1.2.0"
@ -662,10 +662,10 @@ typescript@^5.7.2:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6"
integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==
vite-node@2.1.8:
version "2.1.8"
resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-2.1.8.tgz#9495ca17652f6f7f95ca7c4b568a235e0c8dbac5"
integrity sha512-uPAwSr57kYjAUux+8E2j0q0Fxpn8M9VoyfGiRI8Kfktz9NcYMCenwY5RnZxnF1WTu3TGiYipirIzacLL3VVGFg==
vite-node@2.1.9:
version "2.1.9"
resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-2.1.9.tgz#549710f76a643f1c39ef34bdb5493a944e4f895f"
integrity sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==
dependencies:
cac "^6.7.14"
debug "^4.3.7"
@ -693,18 +693,18 @@ vite@^5.0.0:
optionalDependencies:
fsevents "~2.3.3"
vitest@^2.1.8:
version "2.1.8"
resolved "https://registry.yarnpkg.com/vitest/-/vitest-2.1.8.tgz#2e6a00bc24833574d535c96d6602fb64163092fa"
integrity sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ==
vitest@^2.1.9:
version "2.1.9"
resolved "https://registry.yarnpkg.com/vitest/-/vitest-2.1.9.tgz#7d01ffd07a553a51c87170b5e80fea3da7fb41e7"
integrity sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==
dependencies:
"@vitest/expect" "2.1.8"
"@vitest/mocker" "2.1.8"
"@vitest/pretty-format" "^2.1.8"
"@vitest/runner" "2.1.8"
"@vitest/snapshot" "2.1.8"
"@vitest/spy" "2.1.8"
"@vitest/utils" "2.1.8"
"@vitest/expect" "2.1.9"
"@vitest/mocker" "2.1.9"
"@vitest/pretty-format" "^2.1.9"
"@vitest/runner" "2.1.9"
"@vitest/snapshot" "2.1.9"
"@vitest/spy" "2.1.9"
"@vitest/utils" "2.1.9"
chai "^5.1.2"
debug "^4.3.7"
expect-type "^1.1.0"
@ -716,7 +716,7 @@ vitest@^2.1.8:
tinypool "^1.0.1"
tinyrainbow "^1.2.0"
vite "^5.0.0"
vite-node "2.1.8"
vite-node "2.1.9"
why-is-node-running "^2.3.0"
w3c-keyname@^2.2.4:

View File

@ -0,0 +1,7 @@
/**
* A safer type guard for arrays since the built-in Array.isArray() asserts `any[]`.
*/
export function isArray(val: any): val is unknown[] {
// eslint-disable-next-line no-restricted-syntax
return Array.isArray(val)
}

View File

@ -2,6 +2,7 @@ import { Text } from '@codemirror/state'
import { Marked } from '@ts-stack/markdown'
import type * as LSP from 'vscode-languageserver-protocol'
import { isArray } from '../lib/utils'
// takes a function and executes it after the wait time, if the function is called again before the wait time is up, the timer is reset
export function deferExecution<T>(func: (args: T) => any, wait: number) {
@ -45,7 +46,7 @@ export function offsetToPos(doc: Text, offset: number) {
export function formatMarkdownContents(
contents: LSP.MarkupContent | LSP.MarkedString | LSP.MarkedString[]
): string {
if (Array.isArray(contents)) {
if (isArray(contents)) {
return contents.map((c) => formatMarkdownContents(c) + '\n\n').join('')
} else if (typeof contents === 'string') {
return Marked.parse(contents)

View File

@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useHotKeyListener } from './hooks/useHotKeyListener'
import { Stream } from './components/Stream'
import { AppHeader } from './components/AppHeader'
@ -24,7 +24,12 @@ import { UnitsMenu } from 'components/UnitsMenu'
import { CameraProjectionToggle } from 'components/CameraProjectionToggle'
import { useCreateFileLinkQuery } from 'hooks/useCreateFileLinkQueryWatcher'
import { maybeWriteToDisk } from 'lib/telemetry'
import { takeScreenshotOfVideoStreamCanvas } from 'lib/screenshot'
import { writeProjectThumbnailFile } from 'lib/desktop'
import { useRouteLoaderData } from 'react-router-dom'
import { useEngineCommands } from 'components/EngineCommands'
import { commandBarActor } from 'machines/commandBarMachine'
import { useToken } from 'machines/appMachine'
maybeWriteToDisk()
.then(() => {})
.catch(() => {})
@ -54,14 +59,20 @@ export function App() {
const projectName = project?.name || null
const projectPath = project?.path || null
const [commands] = useEngineCommands()
const [capturedCanvas, setCapturedCanvas] = useState(false)
const loaderData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const lastCommandType = commands[commands.length - 1]?.type
useEffect(() => {
onProjectOpen({ name: projectName, path: projectPath }, file || null)
}, [projectName, projectPath])
useHotKeyListener()
const { auth, settings } = useSettingsAuthContext()
const token = auth?.context?.token
const { settings } = useSettingsAuthContext()
const token = useToken()
const coreDumpManager = useMemo(
() => new CoreDumpManager(engineCommandManager, codeManager, token),
@ -91,6 +102,32 @@ export function App() {
useEngineConnectionSubscriptions()
// Generate thumbnail.png when loading the app
useEffect(() => {
if (
isDesktop() &&
!capturedCanvas &&
lastCommandType === 'execution-done'
) {
setTimeout(() => {
const projectDirectoryWithoutEndingSlash = loaderData?.project?.path
if (!projectDirectoryWithoutEndingSlash) {
return
}
const dataUrl: string = takeScreenshotOfVideoStreamCanvas()
// zoom to fit command does not wait, wait 500ms to see if zoom to fit finishes
writeProjectThumbnailFile(dataUrl, projectDirectoryWithoutEndingSlash)
.then(() => {})
.catch((e) => {
console.error(
`Failed to generate thumbnail for ${projectDirectoryWithoutEndingSlash}`
)
console.error(e)
})
}, 500)
}
}, [lastCommandType])
return (
<div className="relative h-full flex flex-col" ref={ref}>
<AppHeader

View File

@ -1,10 +1,10 @@
import { useAuthState } from 'machines/appMachine'
import Loading from './components/Loading'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
// Wrapper around protected routes, used in src/Router.tsx
export const Auth = ({ children }: React.PropsWithChildren) => {
const { auth } = useSettingsAuthContext()
const isLoggingIn = auth?.state.matches('checkIfLoggedIn')
const authState = useAuthState()
const isLoggingIn = authState.matches('checkIfLoggedIn')
return isLoggingIn ? (
<Loading>

View File

@ -37,7 +37,6 @@ import { KclContextProvider } from 'lang/KclProvider'
import { ASK_TO_OPEN_QUERY_PARAM, BROWSER_PROJECT_NAME } from 'lib/constants'
import { CoreDumpManager } from 'lib/coredump'
import { codeManager, engineCommandManager } from 'lib/singletons'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import useHotkeyWrapper from 'lib/hotkeyWrapper'
import toast from 'react-hot-toast'
import { coreDump } from 'lang/wasm'
@ -47,6 +46,7 @@ import { reportRejection } from 'lib/trap'
import { RouteProvider } from 'components/RouteProvider'
import { ProjectsContextProvider } from 'components/ProjectsContextProvider'
import { OpenInDesktopAppHandler } from 'components/OpenInDesktopAppHandler'
import { useToken } from 'machines/appMachine'
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
@ -203,8 +203,7 @@ export const Router = () => {
}
function CoreDump() {
const { auth } = useSettingsAuthContext()
const token = auth?.context?.token
const token = useToken()
const coreDumpManager = useMemo(
() => new CoreDumpManager(engineCommandManager, codeManager, token),
[]

View File

@ -22,6 +22,7 @@ import {
import { isDesktop } from 'lib/isDesktop'
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { commandBarActor } from 'machines/commandBarMachine'
import { isArray } from 'lib/utils'
export function Toolbar({
className = '',
@ -121,7 +122,7 @@ export function Toolbar({
return toolbarConfig[currentMode].items.map((maybeIconConfig) => {
if (maybeIconConfig === 'break') {
return 'break'
} else if (Array.isArray(maybeIconConfig)) {
} else if (isArray(maybeIconConfig)) {
return maybeIconConfig.map(resolveItemConfig)
} else {
return resolveItemConfig(maybeIconConfig)
@ -180,7 +181,7 @@ export function Toolbar({
className="h-5 w-[1px] block bg-chalkboard-30 dark:bg-chalkboard-80"
/>
)
} else if (Array.isArray(maybeIconConfig)) {
} else if (isArray(maybeIconConfig)) {
// A button with a dropdown
return (
<ActionButtonDropdown

View File

@ -29,6 +29,7 @@ import * as TWEEN from '@tweenjs/tween.js'
import { isQuaternionVertical } from './helpers'
import { reportRejection } from 'lib/trap'
import { CameraProjectionType } from 'wasm-lib/kcl/bindings/CameraProjectionType'
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
const ORTHOGRAPHIC_CAMERA_SIZE = 20
const FRAMES_TO_ANIMATE_IN = 30
@ -406,7 +407,7 @@ export class CameraControls {
.sub(this.mouseDownPosition)
this.mouseDownPosition.copy(this.mouseNewPosition)
const interaction = this.getInteractionType(event)
let interaction = this.getInteractionType(event)
if (interaction === 'none') return
// If there's a valid interaction and the mouse is moving,
@ -753,8 +754,6 @@ export class CameraControls {
didChange = true
}
this.safeLookAtTarget(this.camera.up)
// Update the camera's matrices
this.camera.updateMatrixWorld()
if (didChange || forceUpdate) {
@ -1189,14 +1188,24 @@ export class CameraControls {
this.deferReactUpdate(this.reactCameraProperties)
Object.values(this._camChangeCallbacks).forEach((cb) => cb())
}
getInteractionType = (event: MouseEvent) =>
_getInteractionType(
getInteractionType = (
event: MouseEvent
): CameraDragInteractionType_type | 'none' => {
const initialInteractionType = _getInteractionType(
this.interactionGuards,
event,
this.enablePan,
this.enableRotate,
this.enableZoom
)
if (
initialInteractionType === 'rotate' &&
this.engineCommandManager.settings.cameraOrbit === 'trackball'
) {
return 'rotatetrackball'
}
return initialInteractionType
}
}
// Pure function helpers

View File

@ -2,11 +2,11 @@ import { Toolbar } from '../Toolbar'
import UserSidebarMenu from 'components/UserSidebarMenu'
import { type IndexLoaderData } from 'lib/types'
import ProjectSidebarMenu from './ProjectSidebarMenu'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import styles from './AppHeader.module.css'
import { RefreshButton } from 'components/RefreshButton'
import { CommandBarOpenButton } from './CommandBarOpenButton'
import { isDesktop } from 'lib/isDesktop'
import { useUser } from 'machines/appMachine'
interface AppHeaderProps extends React.PropsWithChildren {
showToolbar?: boolean
@ -24,8 +24,7 @@ export const AppHeader = ({
style,
enableMenu = false,
}: AppHeaderProps) => {
const { auth } = useSettingsAuthContext()
const user = auth?.context?.user
const user = useUser()
return (
<header

View File

@ -7,6 +7,7 @@ import { trap } from 'lib/trap'
import { codeToIdSelections } from 'lib/selections'
import { codeRefFromRange } from 'lang/std/artifactGraph'
import { defaultSourceRange, SourceRange, topLevelRange } from 'lang/wasm'
import { isArray } from 'lib/utils'
export function AstExplorer() {
const { context } = useModelingContext()
@ -166,12 +167,12 @@ function DisplayObj({
{Object.entries(obj).map(([key, value]) => {
if (filterKeys.includes(key)) {
return null
} else if (Array.isArray(value)) {
} else if (isArray(value)) {
return (
<li key={key}>
{`${key}: [`}
<DisplayBody
body={value}
body={value as any}
filterKeys={filterKeys}
node={node}
/>

View File

@ -30,6 +30,7 @@ import {
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { markOnce } from 'lib/performance'
import { commandBarActor } from 'machines/commandBarMachine'
import { useToken } from 'machines/appMachine'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
@ -47,7 +48,8 @@ export const FileMachineProvider = ({
children: React.ReactNode
}) => {
const navigate = useNavigate()
const { settings, auth } = useSettingsAuthContext()
const { settings } = useSettingsAuthContext()
const token = useToken()
const projectData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const { project, file } = projectData
const [kclSamples, setKclSamples] = React.useState<KclSamplesManifestItem[]>(
@ -122,23 +124,44 @@ export const FileMachineProvider = ({
let createdName = input.name.trim() || DEFAULT_FILE_NAME
let createdPath: string
if (input.makeDir) {
if (
(input.targetPathToClone &&
(await window.electron.statIsDirectory(
input.targetPathToClone
))) ||
input.makeDir
) {
let { name, path } = getNextDirName({
entryName: createdName,
baseDir: input.selectedDirectory.path,
entryName: input.targetPathToClone
? window.electron.path.basename(input.targetPathToClone)
: createdName,
baseDir: input.targetPathToClone
? window.electron.path.dirname(input.targetPathToClone)
: input.selectedDirectory.path,
})
createdName = name
createdPath = path
await window.electron.mkdir(createdPath)
} else {
const { name, path } = getNextFileName({
entryName: createdName,
baseDir: input.selectedDirectory.path,
entryName: input.targetPathToClone
? window.electron.path.basename(input.targetPathToClone)
: createdName,
baseDir: input.targetPathToClone
? window.electron.path.dirname(input.targetPathToClone)
: input.selectedDirectory.path,
})
createdName = name
createdPath = path
if (input.targetPathToClone) {
await window.electron.copyFile(
input.targetPathToClone,
createdPath
)
} else {
await window.electron.writeFile(createdPath, input.content ?? '')
}
}
return {
message: `Successfully created "${createdName}"`,
@ -297,7 +320,7 @@ export const FileMachineProvider = ({
const kclCommandMemo = useMemo(
() =>
kclCommands({
authToken: auth?.context?.token ?? '',
authToken: token ?? '',
projectData,
settings: {
defaultUnit: settings?.context?.modeling.defaultUnit.current ?? 'mm',

View File

@ -153,6 +153,7 @@ const FileTreeItem = ({
onClickDirectory,
onCreateFile,
onCreateFolder,
onCloneFileOrFolder,
newTreeEntry,
level = 0,
treeSelection,
@ -171,6 +172,7 @@ const FileTreeItem = ({
) => void
onCreateFile: (name: string) => void
onCreateFolder: (name: string) => void
onCloneFileOrFolder: (path: string) => void
newTreeEntry: TreeEntry
level?: number
treeSelection: FileEntry | undefined
@ -403,6 +405,7 @@ const FileTreeItem = ({
currentFile={currentFile}
onCreateFile={onCreateFile}
onCreateFolder={onCreateFolder}
onCloneFileOrFolder={onCloneFileOrFolder}
newTreeEntry={newTreeEntry}
lastDirectoryClicked={lastDirectoryClicked}
onClickDirectory={onClickDirectory}
@ -441,6 +444,7 @@ const FileTreeItem = ({
itemRef={itemRef}
onRename={addCurrentItemToRenaming}
onDelete={() => setIsConfirmingDelete(true)}
onClone={() => onCloneFileOrFolder(fileOrDir.path)}
/>
</div>
)
@ -450,12 +454,14 @@ interface FileTreeContextMenuProps {
itemRef: React.RefObject<HTMLElement>
onRename: () => void
onDelete: () => void
onClone: () => void
}
function FileTreeContextMenu({
itemRef,
onRename,
onDelete,
onClone,
}: FileTreeContextMenuProps) {
const platform = usePlatform()
const metaKey = platform === 'macos' ? '⌘' : 'Ctrl'
@ -478,6 +484,13 @@ function FileTreeContextMenu({
>
Delete
</ContextMenuItem>,
<ContextMenuItem
data-testid="context-menu-clone"
onClick={onClone}
hotkey=""
>
Clone
</ContextMenuItem>,
]}
/>
)
@ -584,9 +597,22 @@ export const useFileTreeOperations = () => {
})
}
function cloneFileOrDir(args: { path: string }) {
send({
type: 'Create file',
data: {
name: '',
makeDir: false,
shouldSetToRename: false,
targetPathToClone: args.path,
},
})
}
return {
createFile,
createFolder,
cloneFileOrDir,
newTreeEntry,
}
}
@ -595,7 +621,8 @@ export const FileTree = ({
className = '',
onNavigateToFile: closePanel,
}: FileTreeProps) => {
const { createFile, createFolder, newTreeEntry } = useFileTreeOperations()
const { createFile, createFolder, cloneFileOrDir, newTreeEntry } =
useFileTreeOperations()
return (
<div className={className}>
@ -611,6 +638,7 @@ export const FileTree = ({
newTreeEntry={newTreeEntry}
onCreateFile={(name: string) => createFile({ dryRun: false, name })}
onCreateFolder={(name: string) => createFolder({ dryRun: false, name })}
onCloneFileOrFolder={(path: string) => cloneFileOrDir({ path })}
/>
</div>
)
@ -620,10 +648,12 @@ export const FileTreeInner = ({
onNavigateToFile,
onCreateFile,
onCreateFolder,
onCloneFileOrFolder,
newTreeEntry,
}: {
onCreateFile: (name: string) => void
onCreateFolder: (name: string) => void
onCloneFileOrFolder: (path: string) => void
newTreeEntry: TreeEntry
onNavigateToFile?: () => void
}) => {
@ -732,6 +762,7 @@ export const FileTreeInner = ({
fileOrDir={fileOrDir}
onCreateFile={onCreateFile}
onCreateFolder={onCreateFolder}
onCloneFileOrFolder={onCloneFileOrFolder}
newTreeEntry={newTreeEntry}
onClickDirectory={onClickDirectory}
onNavigateToFile={onNavigateToFile_}

View File

@ -27,6 +27,7 @@ import { PROJECT_ENTRYPOINT } from 'lib/constants'
import { err } from 'lib/trap'
import { isDesktop } from 'lib/isDesktop'
import { codeManager } from 'lib/singletons'
import { useToken } from 'machines/appMachine'
function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
return []
@ -69,8 +70,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const [isKclLspReady, setIsKclLspReady] = useState(false)
const [isCopilotLspReady, setIsCopilotLspReady] = useState(false)
const { auth } = useSettingsAuthContext()
const token = auth?.context.token
const token = useToken()
const navigate = useNavigate()
// So this is a bit weird, we need to initialize the lsp server and client.

View File

@ -1,10 +1,8 @@
import { useEngineCommands } from './EngineCommands'
import { Spinner } from './Spinner'
import { CustomIcon } from './CustomIcon'
export const ModelStateIndicator = () => {
const [commands] = useEngineCommands()
const lastCommandType = commands[commands.length - 1]?.type
let className = 'w-6 h-6 '

View File

@ -89,6 +89,7 @@ import { Node } from 'wasm-lib/kcl/bindings/Node'
import { promptToEditFlow } from 'lib/promptToEdit'
import { kclEditorActor } from 'machines/kclEditorMachine'
import { commandBarActor } from 'machines/commandBarMachine'
import { useToken } from 'machines/appMachine'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
@ -110,7 +111,6 @@ export const ModelingMachineProvider = ({
children: React.ReactNode
}) => {
const {
auth,
settings: {
context: {
app: { theme, enableSSAO, allowOrbitInSketchMode },
@ -119,6 +119,7 @@ export const ModelingMachineProvider = ({
cameraProjection,
highlightEdges,
showScaleGrid,
cameraOrbit,
},
},
},
@ -127,7 +128,7 @@ export const ModelingMachineProvider = ({
const navigate = useNavigate()
const { context, send: fileMachineSend } = useFileContext()
const { file } = useLoaderData() as IndexLoaderData
const token = auth?.context?.token
const token = useToken()
const streamRef = useRef<HTMLDivElement>(null)
const persistedContext = useMemo(() => getPersistedContext(), [])
@ -1154,6 +1155,7 @@ export const ModelingMachineProvider = ({
enableSSAO: enableSSAO.current,
showScaleGrid: showScaleGrid.current,
cameraProjection: cameraProjection.current,
cameraOrbit: cameraOrbit.current,
},
token
)
@ -1183,6 +1185,13 @@ export const ModelingMachineProvider = ({
editorManager.selectionRanges = modelingState.context.selectionRanges
}, [modelingState.context.selectionRanges])
// When changing camera modes reset the camera to the default orientation to correct
// the up vector otherwise the conconical orientation for the camera modes will be
// wrong
useEffect(() => {
sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
}, [cameraOrbit.current])
useEffect(() => {
const onConnectionStateChanged = ({ detail }: CustomEvent) => {
// If we are in sketch mode we need to exit it.

View File

@ -14,6 +14,7 @@ import {
} from '@codemirror/state'
import { EditorView } from '@codemirror/view'
import { oneDark } from '@codemirror/theme-one-dark'
import { isArray } from 'lib/utils'
//reference: https://github.com/sachinraja/rodemirror/blob/main/src/use-first-render.ts
const useFirstRender = () => {
@ -86,6 +87,18 @@ const CodeEditor = forwardRef<CodeEditorRef, CodeEditorProps>((props, ref) => {
return <div ref={editor}></div>
})
/**
* The extensions type is quite weird. We need a special helper to preserve the
* readonly array type.
*
* @see https://github.com/microsoft/TypeScript/issues/17002
*/
function isExtensionArray(
extensions: Extension
): extensions is readonly Extension[] {
return isArray(extensions)
}
export function useCodeMirror(props: UseCodeMirror) {
const {
onCreateEditor,
@ -103,7 +116,7 @@ export function useCodeMirror(props: UseCodeMirror) {
const isFirstRender = useFirstRender()
const targetExtensions = useMemo(() => {
let exts = Array.isArray(extensions) ? extensions : []
let exts = isExtensionArray(extensions) ? extensions : []
if (theme === 'dark') {
exts = [...exts, oneDark]
} else if (theme === 'light') {

View File

@ -122,7 +122,8 @@ export const sidebarPanes: SidebarPane[] = [
icon: 'folder',
sidebarName: 'Project Files',
Content: (props: { id: SidebarType; onClose: () => void }) => {
const { createFile, createFolder, newTreeEntry } = useFileTreeOperations()
const { createFile, createFolder, cloneFileOrDir, newTreeEntry } =
useFileTreeOperations()
return (
<>
@ -143,6 +144,7 @@ export const sidebarPanes: SidebarPane[] = [
onCreateFolder={(name: string) =>
createFolder({ dryRun: false, name })
}
onCloneFileOrFolder={(path: string) => cloneFileOrDir({ path })}
newTreeEntry={newTreeEntry}
/>
</>

View File

@ -33,7 +33,7 @@ export const OpenInDesktopAppHandler = (props: React.PropsWithChildren) => {
function onOpenInDesktopApp() {
const newSearchParams = new URLSearchParams(globalThis.location.search)
newSearchParams.delete(ASK_TO_OPEN_QUERY_PARAM)
const newURL = `${ZOO_STUDIO_PROTOCOL}${globalThis.location.pathname.replace(
const newURL = `${ZOO_STUDIO_PROTOCOL}://${globalThis.location.pathname.replace(
'/',
''
)}${searchParams.size > 0 ? `?${newSearchParams.toString()}` : ''}`

View File

@ -2,7 +2,7 @@ import { FormEvent, useEffect, useRef, useState } from 'react'
import { PATHS } from 'lib/paths'
import { Link } from 'react-router-dom'
import { ActionButton } from '../ActionButton'
import { FILE_EXT } from 'lib/constants'
import { FILE_EXT, PROJECT_IMAGE_NAME } from 'lib/constants'
import { useHotkeys } from 'react-hotkeys-hook'
import Tooltip from '../Tooltip'
import { DeleteConfirmationDialog } from './DeleteProjectDialog'
@ -29,7 +29,7 @@ function ProjectCard({
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
const [numberOfFiles, setNumberOfFiles] = useState(1)
const [numberOfFolders, setNumberOfFolders] = useState(0)
// const [imageUrl, setImageUrl] = useState('')
const [imageUrl, setImageUrl] = useState('')
let inputRef = useRef<HTMLInputElement>(null)
@ -53,18 +53,21 @@ function ProjectCard({
setNumberOfFolders(project.directory_count)
}
// async function setupImageUrl() {
// const projectImagePath = await join(project.file.path, PROJECT_IMAGE_NAME)
// if (await exists(projectImagePath)) {
// const imageData = await readFile(projectImagePath)
// const blob = new Blob([imageData], { type: 'image/jpg' })
// const imageUrl = URL.createObjectURL(blob)
// setImageUrl(imageUrl)
// }
// }
async function setupImageUrl() {
const projectImagePath = window.electron.path.join(
project.path,
PROJECT_IMAGE_NAME
)
if (await window.electron.exists(projectImagePath)) {
const imageData = await window.electron.readFile(projectImagePath)
const blob = new Blob([imageData], { type: 'image/png' })
const imageUrl = URL.createObjectURL(blob)
setImageUrl(imageUrl)
}
}
void getNumberOfFiles()
// void setupImageUrl()
void setupImageUrl()
}, [project.kcl_file_count, project.directory_count])
useEffect(() => {
@ -84,7 +87,7 @@ function ProjectCard({
to={`${PATHS.FILE}/${encodeURIComponent(project.default_file)}`}
className="flex flex-col flex-1 !no-underline !text-chalkboard-110 dark:!text-chalkboard-10 group-hover:!hue-rotate-0 min-h-[5em] divide-y divide-primary/40 dark:divide-chalkboard-80 group-hover:!divide-primary"
>
{/* <div className="h-36 relative overflow-hidden bg-gradient-to-b from-transparent to-primary/10 rounded-t-sm">
<div className="h-36 relative overflow-hidden bg-gradient-to-b from-transparent to-primary/10 rounded-t-sm">
{imageUrl && (
<img
src={imageUrl}
@ -92,7 +95,7 @@ function ProjectCard({
className="h-full w-full transition-transform group-hover:scale-105 object-cover"
/>
)}
</div> */}
</div>
<div className="pb-2 flex flex-col flex-grow flex-auto gap-2 rounded-b-sm">
{isEditing ? (
<ProjectCardRenameForm

View File

@ -19,7 +19,8 @@ import { commandBarActor } from 'machines/commandBarMachine'
import { useSelector } from '@xstate/react'
import { copyFileShareLink } from 'lib/links'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { DEV } from 'env'
import { IS_NIGHTLY_OR_DEBUG } from 'routes/Settings'
import { useToken } from 'machines/appMachine'
const ProjectSidebarMenu = ({
project,
@ -103,13 +104,15 @@ function ProjectMenuPopover({
const location = useLocation()
const navigate = useNavigate()
const filePath = useAbsoluteFilePath()
const { settings, auth } = useSettingsAuthContext()
const { settings } = useSettingsAuthContext()
const token = useToken()
const machineManager = useContext(MachineManagerContext)
const commands = useSelector(commandBarActor, commandsSelector)
const { onProjectClose } = useLspContext()
const exportCommandInfo = { name: 'Export', groupId: 'modeling' }
const makeCommandInfo = { name: 'Make', groupId: 'modeling' }
const shareCommandInfo = { name: 'share-file-link', groupId: 'code' }
const findCommand = (obj: { name: string; groupId: string }) =>
Boolean(
commands.find((c) => c.name === obj.name && c.groupId === obj.groupId)
@ -191,10 +194,10 @@ function ProjectMenuPopover({
id: 'share-link',
Element: 'button',
children: 'Share link to file',
disabled: !DEV,
disabled: IS_NIGHTLY_OR_DEBUG || !findCommand(shareCommandInfo),
onClick: async () => {
await copyFileShareLink({
token: auth?.context.token || '',
token: token ?? '',
code: codeManager.code,
name: project?.name || '',
units: settings.context.modeling.defaultUnit.current,

View File

@ -8,10 +8,10 @@ import Tooltip from './Tooltip'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { reportRejection } from 'lib/trap'
import { toSync } from 'lib/utils'
import { useToken } from 'machines/appMachine'
export const RefreshButton = ({ children }: React.PropsWithChildren) => {
const { auth } = useSettingsAuthContext()
const token = auth?.context?.token
const token = useToken()
const coreDumpManager = useMemo(
() => new CoreDumpManager(engineCommandManager, codeManager, token),
[]

View File

@ -2,10 +2,12 @@ import { useEffect, useState, createContext, ReactNode } from 'react'
import { useNavigation, useLocation } from 'react-router-dom'
import { PATHS } from 'lib/paths'
import { markOnce } from 'lib/performance'
import { useAuthNavigation } from 'hooks/useAuthNavigation'
export const RouteProviderContext = createContext({})
export function RouteProvider({ children }: { children: ReactNode }) {
useAuthNavigation()
const [first, setFirstState] = useState(true)
const navigation = useNavigation()
const location = useLocation()

View File

@ -2,10 +2,7 @@ import { trap } from 'lib/trap'
import { useMachine, useSelector } from '@xstate/react'
import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom'
import { PATHS, BROWSER_PATH } from 'lib/paths'
import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine'
import withBaseUrl from '../lib/withBaseURL'
import React, { createContext, useEffect, useState } from 'react'
import useStateMachineCommands from '../hooks/useStateMachineCommands'
import { settingsMachine } from 'machines/settingsMachine'
import { toast } from 'react-hot-toast'
import {
@ -16,7 +13,6 @@ import {
} from 'lib/theme'
import decamelize from 'decamelize'
import { Actor, AnyStateMachine, ContextFrom, Prop, StateFrom } from 'xstate'
import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig'
import {
kclManager,
sceneInfra,
@ -50,7 +46,6 @@ type MachineContext<T extends AnyStateMachine> = {
}
type SettingsAuthContextType = {
auth: MachineContext<typeof authMachine>
settings: MachineContext<typeof settingsMachine>
}
@ -370,40 +365,9 @@ export const SettingsAuthProviderBase = ({
)
}, [settingsState.context.textEditor.blinkingCursor.current])
// Auth machine setup
const [authState, authSend, authActor] = useMachine(
authMachine.provide({
actions: {
goToSignInPage: () => {
navigate(PATHS.SIGN_IN)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
logout()
},
goToIndexPage: () => {
if (location.pathname.includes(PATHS.SIGN_IN)) {
navigate(PATHS.INDEX)
}
},
},
})
)
useStateMachineCommands({
machineId: 'auth',
state: authState,
send: authSend,
commandBarConfig: authCommandBarConfig,
actor: authActor,
})
return (
<SettingsAuthContext.Provider
value={{
auth: {
state: authState,
context: authState.context,
send: authSend,
},
settings: {
state: settingsState,
context: settingsState.context,
@ -417,12 +381,3 @@ export const SettingsAuthProviderBase = ({
}
export default SettingsAuthProvider
export async function logout() {
localStorage.removeItem(TOKEN_PERSIST_KEY)
if (isDesktop()) return Promise.resolve(null)
return fetch(withBaseUrl('/logout'), {
method: 'POST',
credentials: 'include',
})
}

View File

@ -4,12 +4,12 @@ import { useLocation, useNavigate } from 'react-router-dom'
import { Fragment, useMemo, useState } from 'react'
import { PATHS } from 'lib/paths'
import { Models } from '@kittycad/lib'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import Tooltip from './Tooltip'
import usePlatform from 'hooks/usePlatform'
import { isDesktop } from 'lib/isDesktop'
import { CustomIcon } from './CustomIcon'
import { authActor } from 'machines/appMachine'
type User = Models['User_type']
@ -20,7 +20,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
const displayedName = getDisplayName(user)
const [imageLoadFailed, setImageLoadFailed] = useState(false)
const navigate = useNavigate()
const send = useSettingsAuthContext()?.auth?.send
const send = authActor.send
// We filter this memoized list so that no orphan "break" elements are rendered.
const userMenuItems = useMemo<(ActionButtonProps | 'break')[]>(

View File

@ -45,10 +45,10 @@ export const lineHighlightField = StateField.define({
})
const matchDeco = Decoration.mark({
class: 'bg-yellow-300/70',
class: 'bg-yellow-300/70 dark:bg-blue-800/50',
attributes: { 'data-testid': 'hover-highlight' },
})
const matchDeco2 = Decoration.mark({
class: 'bg-yellow-200/40',
class: 'bg-yellow-200/40 dark:bg-blue-700/50',
attributes: { 'data-testid': 'hover-highlight' },
})

View File

@ -9,6 +9,7 @@ import {
import { Range, Extension, Text } from '@codemirror/state'
import { NodeProp, Tree } from '@lezer/common'
import { language, syntaxTree } from '@codemirror/language'
import { isArray } from 'lib/utils'
interface PickerState {
from: number
@ -79,7 +80,7 @@ function discoverColorsInKCL(
)
if (maybeWidgetOptions) {
if (Array.isArray(maybeWidgetOptions)) {
if (isArray(maybeWidgetOptions)) {
console.error('Unexpected nested overlays')
ret.push(...maybeWidgetOptions)
} else {
@ -150,7 +151,7 @@ function colorPickersDecorations(
return
}
if (!Array.isArray(maybeWidgetOptions)) {
if (!isArray(maybeWidgetOptions)) {
widgets.push(
Decoration.widget({
widget: new ColorPickerWidget(maybeWidgetOptions),

View File

@ -10,6 +10,7 @@ export const VITE_KC_API_WS_MODELING_URL = env.VITE_KC_API_WS_MODELING_URL as
| undefined
export const VITE_KC_API_BASE_URL = env.VITE_KC_API_BASE_URL as string
export const VITE_KC_SITE_BASE_URL = env.VITE_KC_SITE_BASE_URL as string
export const VITE_KC_SITE_APP_URL = env.VITE_KC_SITE_APP_URL as string
export const VITE_KC_SKIP_AUTH = env.VITE_KC_SKIP_AUTH as string | undefined
export const VITE_KC_CONNECTION_TIMEOUT_MS =
env.VITE_KC_CONNECTION_TIMEOUT_MS as string | undefined

View File

@ -0,0 +1,29 @@
import { PATHS } from 'lib/paths'
import { useAuthState } from 'machines/appMachine'
import { useEffect } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
/**
* A simple hook that listens to the auth state of the app and navigates
* accordingly.
*/
export function useAuthNavigation() {
const navigate = useNavigate()
const location = useLocation()
const authState = useAuthState()
// Subscribe to the auth state of the app and navigate accordingly.
useEffect(() => {
if (
authState.matches('loggedIn') &&
location.pathname.includes(PATHS.SIGN_IN)
) {
navigate(PATHS.INDEX)
} else if (
authState.matches('loggedOut') &&
!location.pathname.includes(PATHS.SIGN_IN)
) {
navigate(PATHS.SIGN_IN)
}
}, [authState])
}

View File

@ -16,14 +16,15 @@ export function useSetupEngineManager(
streamRef: React.RefObject<HTMLDivElement>,
modelingSend: ReturnType<typeof useModelingContext>['send'],
modelingContext: ReturnType<typeof useModelingContext>['context'],
settings = {
settings: SettingsViaQueryString = {
pool: null,
theme: Themes.System,
highlightEdges: true,
enableSSAO: true,
showScaleGrid: false,
cameraProjection: 'perspective',
} as SettingsViaQueryString,
cameraOrbit: 'spherical',
},
token?: string
) {
const networkContext = useNetworkContext()

View File

@ -57,6 +57,7 @@ import { ExtrudeFacePlane } from 'machines/modelingMachine'
import { Node } from 'wasm-lib/kcl/bindings/Node'
import { KclExpressionWithVariable } from 'lib/commandTypes'
import { findKwArg } from './util'
import { deleteEdgeTreatment } from './modifyAst/addEdgeTreatment'
export function startSketchOnDefault(
node: Node<Program>,
@ -1467,6 +1468,8 @@ export async function deleteFromSelection(
}
// await prom
return astClone
} else if (selection.artifact?.type === 'edgeCut') {
return deleteEdgeTreatment(astClone, selection)
} else if (varDec.node.init.type === 'PipeExpression') {
const pipeBody = varDec.node.init.body
if (

View File

@ -21,13 +21,19 @@ import {
FilletParameters,
ChamferParameters,
EdgeTreatmentParameters,
deleteEdgeTreatment,
} from './addEdgeTreatment'
import { getNodeFromPath } from '../queryAst'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { createLiteral } from 'lang/modifyAst'
import { err } from 'lib/trap'
import { Selection, Selections } from 'lib/selections'
import { engineCommandManager, kclManager } from 'lib/singletons'
import {
codeManager,
editorManager,
engineCommandManager,
kclManager,
} from 'lib/singletons'
import { VITE_KC_DEV_TOKEN } from 'env'
import { isOverlap } from 'lib/utils'
import { codeRefFromRange } from 'lang/std/artifactGraph'
@ -55,6 +61,13 @@ afterAll(() => {
engineCommandManager.tearDown()
})
const dependencies = {
kclManager,
engineCommandManager,
editorManager,
codeManager,
}
const runGetPathToExtrudeForSegmentSelectionTest = async (
code: string,
selectedSegmentSnippet: string,
@ -133,7 +146,8 @@ const runGetPathToExtrudeForSegmentSelectionTest = async (
const pathResult = getPathToExtrudeForSegmentSelection(
ast,
selection,
artifactGraph
artifactGraph,
dependencies
)
if (err(pathResult)) return pathResult
const { pathToExtrudeNode } = pathResult
@ -290,8 +304,13 @@ const runModifyAstCloneWithEdgeTreatmentAndTag = async (
otherSelections: [],
}
// apply edge treatment to seleciton
const result = modifyAstWithEdgeTreatmentAndTag(ast, selection, parameters)
// apply edge treatment to selection
const result = modifyAstWithEdgeTreatmentAndTag(
ast,
selection,
parameters,
dependencies
)
if (err(result)) {
return result
}
@ -301,6 +320,46 @@ const runModifyAstCloneWithEdgeTreatmentAndTag = async (
expect(newCode).toContain(expectedCode)
}
const runDeleteEdgeTreatmentTest = async (
code: string,
edgeTreatmentSnippet: string,
expectedCode: string
) => {
// parse ast
const ast = assertParse(code)
// update artifact graph
await kclManager.executeAst({ ast })
const artifactGraph = engineCommandManager.artifactGraph
// define snippet range
const edgeTreatmentRange = topLevelRange(
code.indexOf(edgeTreatmentSnippet),
code.indexOf(edgeTreatmentSnippet) + edgeTreatmentSnippet.length
)
// find artifact
const maybeArtifact = [...artifactGraph].find(([, artifact]) => {
if (!('codeRef' in artifact)) return false
return isOverlap(artifact.codeRef.range, edgeTreatmentRange)
})
// build selection
const selection: Selection = {
codeRef: codeRefFromRange(edgeTreatmentRange, ast),
artifact: maybeArtifact ? maybeArtifact[1] : undefined,
}
// delete edge treatment
const result = await deleteEdgeTreatment(ast, selection)
if (err(result)) {
return result
}
// recast and check
const newCode = recast(result)
expect(newCode).toContain(expectedCode)
}
const createFilletParameters = (radiusValue: number): FilletParameters => ({
type: EdgeTreatmentType.Fillet,
radius: {
@ -577,6 +636,191 @@ extrude002 = extrude(sketch002, length = -25)
)
})
})
describe(`Testing deleteEdgeTreatment with ${edgeTreatmentType}s`, () => {
// simple cases
it(`should delete a piped ${edgeTreatmentType} from a single segment`, async () => {
const code = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %)
|> line(end = [20, 0])
|> line(end = [0, -20])
|> line(end = [-20, 0], tag = $seg01)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(sketch001, length = -15)
|> ${edgeTreatmentType}({ ${parameterName} = 3, tags = [seg01] }, %)`
const edgeTreatmentSnippet = `${edgeTreatmentType}({ ${parameterName} = 3, tags = [seg01] }, %)`
const expectedCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %)
|> line(end = [20, 0])
|> line(end = [0, -20])
|> line(end = [-20, 0], tag = $seg01)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(sketch001, length = -15)`
await runDeleteEdgeTreatmentTest(
code,
edgeTreatmentSnippet,
expectedCode
)
})
it(`should delete a non-piped ${edgeTreatmentType} from a single segment`, async () => {
const code = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %)
|> line(end = [20, 0])
|> line(end = [0, -20])
|> line(end = [-20, 0], tag = $seg01)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(sketch001, length = -15)
fillet001 = ${edgeTreatmentType}({ ${parameterName} = 3, tags = [seg01] }, extrude001)`
const edgeTreatmentSnippet = `fillet001 = ${edgeTreatmentType}({ ${parameterName} = 3, tags = [seg01] }, extrude001)`
const expectedCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %)
|> line(end = [20, 0])
|> line(end = [0, -20])
|> line(end = [-20, 0], tag = $seg01)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(sketch001, length = -15)`
await runDeleteEdgeTreatmentTest(
code,
edgeTreatmentSnippet,
expectedCode
)
})
// getOppositeEdge and getNextAdjacentEdge cases
it(`should delete a piped ${edgeTreatmentType} tagged with getOppositeEdge`, async () => {
const code = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %)
|> line(end = [20, 0])
|> line(end = [0, -20])
|> line(end = [-20, 0], tag = $seg01)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(sketch001, length = -15)
fillet001 = ${edgeTreatmentType}({ ${parameterName} = 3, tags = [getOppositeEdge(seg01)] }, extrude001)`
const edgeTreatmentSnippet = `fillet001 = ${edgeTreatmentType}({ ${parameterName} = 3, tags = [getOppositeEdge(seg01)] }, extrude001)`
const expectedCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %)
|> line(end = [20, 0])
|> line(end = [0, -20])
|> line(end = [-20, 0], tag = $seg01)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(sketch001, length = -15)`
await runDeleteEdgeTreatmentTest(
code,
edgeTreatmentSnippet,
expectedCode
)
})
it(`should delete a non-piped ${edgeTreatmentType} tagged with getNextAdjacentEdge`, async () => {
const code = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %)
|> line(end = [20, 0])
|> line(end = [0, -20])
|> line(end = [-20, 0], tag = $seg01)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(sketch001, length = -15)
fillet001 = ${edgeTreatmentType}({ ${parameterName} = 3, tags = [getNextAdjacentEdge(seg01)] }, extrude001)`
const edgeTreatmentSnippet = `fillet001 = ${edgeTreatmentType}({ ${parameterName} = 3, tags = [getNextAdjacentEdge(seg01)] }, extrude001)`
const expectedCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %)
|> line(end = [20, 0])
|> line(end = [0, -20])
|> line(end = [-20, 0], tag = $seg01)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(sketch001, length = -15)`
await runDeleteEdgeTreatmentTest(
code,
edgeTreatmentSnippet,
expectedCode
)
})
// cases with several edge treatments
it(`should delete a piped ${edgeTreatmentType} from a body with multiple treatments`, async () => {
const code = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %)
|> line(end = [20, 0], tag = $seg01)
|> line(end = [0, -20])
|> line(end = [-20, 0], tag = $seg02)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(sketch001, length = -15)
|> ${edgeTreatmentType}({ ${parameterName} = 3, tags = [seg01] }, %)
|> fillet({ radius = 5, tags = [getOppositeEdge(seg02)] }, %)
fillet001 = ${edgeTreatmentType}({ ${parameterName} = 6, tags = [seg02] }, extrude001)
chamfer001 = chamfer({ length = 5, tags = [getOppositeEdge(seg01)] }, extrude001)`
const edgeTreatmentSnippet = `${edgeTreatmentType}({ ${parameterName} = 3, tags = [seg01] }, %)`
const expectedCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %)
|> line(end = [20, 0], tag = $seg01)
|> line(end = [0, -20])
|> line(end = [-20, 0], tag = $seg02)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(sketch001, length = -15)
|> fillet({
radius = 5,
tags = [getOppositeEdge(seg02)]
}, %)
fillet001 = ${edgeTreatmentType}({ ${parameterName} = 6, tags = [seg02] }, extrude001)
chamfer001 = chamfer({
length = 5,
tags = [getOppositeEdge(seg01)]
}, extrude001)`
await runDeleteEdgeTreatmentTest(
code,
edgeTreatmentSnippet,
expectedCode
)
})
it(`should delete a non-piped ${edgeTreatmentType} from a body with multiple treatments`, async () => {
const code = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %)
|> line(end = [20, 0], tag = $seg01)
|> line(end = [0, -20])
|> line(end = [-20, 0], tag = $seg02)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(sketch001, length = -15)
|> ${edgeTreatmentType}({ ${parameterName} = 3, tags = [seg01] }, %)
|> fillet({ radius = 5, tags = [getOppositeEdge(seg02)] }, %)
fillet001 = ${edgeTreatmentType}({ ${parameterName} = 6, tags = [seg02] }, extrude001)
chamfer001 = chamfer({ length = 5, tags = [getOppositeEdge(seg01)] }, extrude001)`
const edgeTreatmentSnippet = `fillet001 = ${edgeTreatmentType}({ ${parameterName} = 6, tags = [seg02] }, extrude001)`
const expectedCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %)
|> line(end = [20, 0], tag = $seg01)
|> line(end = [0, -20])
|> line(end = [-20, 0], tag = $seg02)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(sketch001, length = -15)
|> ${edgeTreatmentType}({ ${parameterName} = 3, tags = [seg01] }, %)
|> fillet({
radius = 5,
tags = [getOppositeEdge(seg02)]
}, %)
chamfer001 = chamfer({
length = 5,
tags = [getOppositeEdge(seg01)]
}, extrude001)`
await runDeleteEdgeTreatmentTest(
code,
edgeTreatmentSnippet,
expectedCode
)
})
})
}
)

View File

@ -6,6 +6,7 @@ import {
Identifier,
ObjectExpression,
PathToNode,
PipeExpression,
Program,
VariableDeclaration,
VariableDeclarator,
@ -35,15 +36,14 @@ import {
import { err, trap } from 'lib/trap'
import { Selection, Selections } from 'lib/selections'
import { KclCommandValue } from 'lib/commandTypes'
import { isArray } from 'lib/utils'
import { Artifact, getSweepFromSuspectedPath } from 'lang/std/artifactGraph'
import {
kclManager,
engineCommandManager,
editorManager,
codeManager,
} from 'lib/singletons'
import { Node } from 'wasm-lib/kcl/bindings/Node'
import { findKwArg } from 'lang/util'
import { KclManager } from 'lang/KclSingleton'
import { EngineCommandManager } from 'lang/std/engineConnection'
import EditorManager from 'editor/manager'
import CodeManager from 'lang/codeManager'
// Edge Treatment Types
export enum EdgeTreatmentType {
@ -65,21 +65,38 @@ export type EdgeTreatmentParameters = ChamferParameters | FilletParameters
export async function applyEdgeTreatmentToSelection(
ast: Node<Program>,
selection: Selections,
parameters: EdgeTreatmentParameters
parameters: EdgeTreatmentParameters,
dependencies: {
kclManager: KclManager
engineCommandManager: EngineCommandManager
editorManager: EditorManager
codeManager: CodeManager
}
): Promise<void | Error> {
// 1. clone and modify with edge treatment and tag
const result = modifyAstWithEdgeTreatmentAndTag(ast, selection, parameters)
const result = modifyAstWithEdgeTreatmentAndTag(
ast,
selection,
parameters,
dependencies
)
if (err(result)) return result
const { modifiedAst, pathToEdgeTreatmentNode } = result
// 2. update ast
await updateAstAndFocus(modifiedAst, pathToEdgeTreatmentNode)
await updateAstAndFocus(modifiedAst, pathToEdgeTreatmentNode, dependencies)
}
export function modifyAstWithEdgeTreatmentAndTag(
ast: Node<Program>,
selections: Selections,
parameters: EdgeTreatmentParameters
parameters: EdgeTreatmentParameters,
dependencies: {
kclManager: KclManager
engineCommandManager: EngineCommandManager
editorManager: EditorManager
codeManager: CodeManager
}
):
| { modifiedAst: Node<Program>; pathToEdgeTreatmentNode: Array<PathToNode> }
| Error {
@ -89,7 +106,7 @@ export function modifyAstWithEdgeTreatmentAndTag(
const astResult = insertParametersIntoAst(clonedAst, parameters)
if (err(astResult)) return astResult
const artifactGraph = engineCommandManager.artifactGraph
const artifactGraph = dependencies.engineCommandManager.artifactGraph
// Step 1: modify ast with tags and group them by extrude nodes (bodies)
const extrudeToTagsMap: Map<
@ -102,7 +119,8 @@ export function modifyAstWithEdgeTreatmentAndTag(
const result = getPathToExtrudeForSegmentSelection(
clonedAstForGetExtrude,
selection,
artifactGraph
artifactGraph,
dependencies
)
if (err(result)) return result
const { pathToSegmentNode, pathToExtrudeNode } = result
@ -258,7 +276,13 @@ function insertParametersIntoAst(
export function getPathToExtrudeForSegmentSelection(
ast: Program,
selection: Selection,
artifactGraph: ArtifactGraph
artifactGraph: ArtifactGraph,
dependencies: {
kclManager: KclManager
engineCommandManager: EngineCommandManager
editorManager: EditorManager
codeManager: CodeManager
}
): { pathToSegmentNode: PathToNode; pathToExtrudeNode: PathToNode } | Error {
const pathToSegmentNode = getNodePathFromSourceRange(
ast,
@ -274,7 +298,7 @@ export function getPathToExtrudeForSegmentSelection(
const sketchVar = varDecNode.node.declaration.id.name
const sketch = sketchFromKclValue(
kclManager.programMemory.get(sketchVar),
dependencies.kclManager.programMemory.get(sketchVar),
sketchVar
)
if (trap(sketch)) return sketch
@ -293,16 +317,28 @@ export function getPathToExtrudeForSegmentSelection(
async function updateAstAndFocus(
modifiedAst: Node<Program>,
pathToEdgeTreatmentNode: Array<PathToNode>
pathToEdgeTreatmentNode: Array<PathToNode>,
dependencies: {
kclManager: KclManager
engineCommandManager: EngineCommandManager
editorManager: EditorManager
codeManager: CodeManager
}
): Promise<void> {
const updatedAst = await kclManager.updateAst(modifiedAst, true, {
const updatedAst = await dependencies.kclManager.updateAst(
modifiedAst,
true,
{
focusPath: pathToEdgeTreatmentNode,
})
}
)
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
await dependencies.codeManager.updateEditorWithAstAndWriteToFile(
updatedAst.newAst
)
if (updatedAst?.selections) {
editorManager.selectRange(updatedAst?.selections)
dependencies.editorManager.selectRange(updatedAst?.selections)
}
}
@ -782,3 +818,142 @@ export const isTagUsedInEdgeTreatment = ({
return edges
}
// Delete Edge Treatment
export async function deleteEdgeTreatment(
ast: Node<Program>,
selection: Selection
): Promise<Node<Program> | Error> {
/**
* Deletes an edge treatment (fillet or chamfer)
* from the AST based on the selection.
* Handles both standalone treatments
* and those within a PipeExpression.
*
* Supported cases:
* [+] fillet and chamfer
* [+] piped and non-piped edge treatments
* [-] delete single tag from array of tags (currently whole expression is deleted)
* [-] multiple selections with different edge treatments (currently single selection is supported)
*/
// 1. Validate Selection Type
const { artifact } = selection
if (!artifact || artifact.type !== 'edgeCut') {
return new Error('Selection is not an edge cut')
}
const { subType: edgeTreatmentType } = artifact
if (
!edgeTreatmentType ||
!['fillet', 'chamfer'].includes(edgeTreatmentType)
) {
return new Error('Unsupported or missing edge treatment type')
}
// 2. Clone ast and retrieve the VariableDeclarator
const astClone = structuredClone(ast)
const varDec = getNodeFromPath<VariableDeclarator>(
ast,
selection?.codeRef?.pathToNode,
'VariableDeclarator'
)
if (err(varDec)) return varDec
// 3: Check if edge treatment is in a pipe
const inPipe = varDec.node.init.type === 'PipeExpression'
// 4A. Handle standalone edge treatment
if (!inPipe) {
const varDecPathStep = varDec.shallowPath[1]
if (!isArray(varDecPathStep) || typeof varDecPathStep[0] !== 'number') {
return new Error(
'Invalid shallowPath structure: expected a number at shallowPath[1][0]'
)
}
const varDecIndex: number = varDecPathStep[0]
// Remove entire VariableDeclarator from the ast
astClone.body.splice(varDecIndex, 1)
return astClone
}
// 4B. Handle edge treatment within pipe
if (inPipe) {
// Retrieve the CallExpression path
const callExp =
getNodeFromPath<CallExpression>(
ast,
selection?.codeRef?.pathToNode,
'CallExpression'
) ?? null
if (err(callExp)) return callExp
const shallowPath = callExp.shallowPath
// Initialize variables to hold the PipeExpression path and callIndex
let pipeExpressionPath: PathToNode | null = null
let callIndex: number | null = null
// Iterate through the shallowPath to find the PipeExpression and callIndex
for (let i = 0; i < shallowPath.length - 1; i++) {
const [key, value] = shallowPath[i]
if (key === 'body' && value === 'PipeExpression') {
pipeExpressionPath = shallowPath.slice(0, i + 1)
const nextStep = shallowPath[i + 1]
if (
nextStep &&
nextStep[1] === 'index' &&
typeof nextStep[0] === 'number'
) {
callIndex = nextStep[0]
}
break
}
}
if (!pipeExpressionPath) {
return new Error('PipeExpression not found in path')
}
if (callIndex === null) {
return new Error('Failed to extract CallExpression index')
}
// Retrieve the PipeExpression node
const pipeExpressionNode = getNodeFromPath<PipeExpression>(
astClone,
pipeExpressionPath,
'PipeExpression'
)
if (err(pipeExpressionNode)) return pipeExpressionNode
// Ensure that the PipeExpression.body is an array
if (!isArray(pipeExpressionNode.node.body)) {
return new Error('PipeExpression body is not an array')
}
// Remove the CallExpression at the specified index
pipeExpressionNode.node.body.splice(callIndex, 1)
// Remove VariableDeclarator if PipeExpression.body is empty
if (pipeExpressionNode.node.body.length === 0) {
const varDecPathStep = varDec.shallowPath[1]
if (!isArray(varDecPathStep) || typeof varDecPathStep[0] !== 'number') {
return new Error(
'Invalid shallowPath structure: expected a number at shallowPath[1][0]'
)
}
const varDecIndex: number = varDecPathStep[0]
astClone.body.splice(varDecIndex, 1)
}
return astClone
}
return Error('Delete fillets not implemented')
}

View File

@ -19,17 +19,28 @@ import {
createVariableDeclaration,
} from 'lang/modifyAst'
import { KCL_DEFAULT_CONSTANT_PREFIXES } from 'lib/constants'
import { KclManager } from 'lang/KclSingleton'
import { EngineCommandManager } from 'lang/std/engineConnection'
import EditorManager from 'editor/manager'
import CodeManager from 'lang/codeManager'
export function addShell({
node,
selection,
artifactGraph,
thickness,
dependencies,
}: {
node: Node<Program>
selection: Selections
artifactGraph: ArtifactGraph
thickness: Expr
dependencies: {
kclManager: KclManager
engineCommandManager: EngineCommandManager
editorManager: EditorManager
codeManager: CodeManager
}
}): Error | { modifiedAst: Node<Program>; pathToNode: PathToNode } {
const modifiedAst = structuredClone(node)
@ -42,7 +53,8 @@ export function addShell({
const extrudeLookupResult = getPathToExtrudeForSegmentSelection(
clonedAstForGetExtrude,
graphSelection,
artifactGraph
artifactGraph,
dependencies
)
if (err(extrudeLookupResult)) {
return new Error("Couldn't find extrude")

View File

@ -5,7 +5,11 @@ import {
PathToNode,
Identifier,
topLevelRange,
PipeExpression,
CallExpression,
VariableDeclarator,
} from './wasm'
import { ProgramMemory } from 'lang/wasm'
import {
findAllPreviousVariables,
isNodeSafeToReplace,
@ -25,9 +29,11 @@ import {
createCallExpression,
createLiteral,
createPipeSubstitution,
createCallExpressionStdLib,
} from './modifyAst'
import { err } from 'lib/trap'
import { codeRefFromRange } from './std/artifactGraph'
import { addCallExpressionsToPipe, addCloseToPipe } from 'lang/std/sketch'
beforeAll(async () => {
await initPromise
@ -680,3 +686,115 @@ myNestedVar = [
expect(pathToNode).toEqual(pathToNode2)
})
})
describe('Testing specific sketch getNodeFromPath workflow', () => {
it('should parse the code', () => {
const openSketch = `sketch001 = startSketchOn('XZ')
|> startProfileAt([0.02, 0.22], %)
|> xLine(0.39, %)
|> line([0.02, -0.17], %)
|> yLine(-0.15, %)
|> line([-0.21, -0.02], %)
|> xLine(-0.15, %)
|> line([-0.02, 0.21], %)
|> line([-0.08, 0.05], %)`
const ast = assertParse(openSketch)
expect(ast.start).toEqual(0)
expect(ast.end).toEqual(227)
})
it('should find the location to add new lineTo', () => {
const openSketch = `sketch001 = startSketchOn('XZ')
|> startProfileAt([0.02, 0.22], %)
|> xLine(0.39, %)
|> line([0.02, -0.17], %)
|> yLine(-0.15, %)
|> line([-0.21, -0.02], %)
|> xLine(-0.15, %)
|> line([-0.02, 0.21], %)
|> line([-0.08, 0.05], %)`
const ast = assertParse(openSketch)
const sketchSnippet = `startProfileAt([0.02, 0.22], %)`
const sketchRange = topLevelRange(
openSketch.indexOf(sketchSnippet),
openSketch.indexOf(sketchSnippet) + sketchSnippet.length
)
const sketchPathToNode = getNodePathFromSourceRange(ast, sketchRange)
const modifiedAst = addCallExpressionsToPipe({
node: ast,
programMemory: ProgramMemory.empty(),
pathToNode: sketchPathToNode,
expressions: [
createCallExpressionStdLib(
'lineTo', // We are forcing lineTo!
[
createArrayExpression([
createCallExpressionStdLib('profileStartX', [
createPipeSubstitution(),
]),
createCallExpressionStdLib('profileStartY', [
createPipeSubstitution(),
]),
]),
createPipeSubstitution(),
]
),
],
})
if (err(modifiedAst)) throw modifiedAst
const recasted = recast(modifiedAst)
const expectedCode = `sketch001 = startSketchOn('XZ')
|> startProfileAt([0.02, 0.22], %)
|> xLine(0.39, %)
|> line([0.02, -0.17], %)
|> yLine(-0.15, %)
|> line([-0.21, -0.02], %)
|> xLine(-0.15, %)
|> line([-0.02, 0.21], %)
|> line([-0.08, 0.05], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
`
expect(recasted).toEqual(expectedCode)
})
it('it should find the location to add close', () => {
const openSketch = `sketch001 = startSketchOn('XZ')
|> startProfileAt([0.02, 0.22], %)
|> xLine(0.39, %)
|> line([0.02, -0.17], %)
|> yLine(-0.15, %)
|> line([-0.21, -0.02], %)
|> xLine(-0.15, %)
|> line([-0.02, 0.21], %)
|> line([-0.08, 0.05], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
`
const ast = assertParse(openSketch)
const sketchSnippet = `startProfileAt([0.02, 0.22], %)`
const sketchRange = topLevelRange(
openSketch.indexOf(sketchSnippet),
openSketch.indexOf(sketchSnippet) + sketchSnippet.length
)
const sketchPathToNode = getNodePathFromSourceRange(ast, sketchRange)
const modifiedAst = addCloseToPipe({
node: ast,
programMemory: ProgramMemory.empty(),
pathToNode: sketchPathToNode,
})
if (err(modifiedAst)) throw modifiedAst
const recasted = recast(modifiedAst)
const expectedCode = `sketch001 = startSketchOn('XZ')
|> startProfileAt([0.02, 0.22], %)
|> xLine(0.39, %)
|> line([0.02, -0.17], %)
|> yLine(-0.15, %)
|> line([-0.21, -0.02], %)
|> xLine(-0.15, %)
|> line([-0.02, 0.21], %)
|> line([-0.08, 0.05], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close()
`
expect(recasted).toEqual(expectedCode)
})
})

View File

@ -22,11 +22,12 @@ import {
topLevelRange,
VariableDeclaration,
VariableDeclarator,
recast,
} from './wasm'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { createIdentifier, splitPathAtLastIndex } from './modifyAst'
import { getSketchSegmentFromSourceRange } from './std/sketchConstraints'
import { getAngle } from '../lib/utils'
import { getAngle, isArray } from '../lib/utils'
import { ARG_TAG, getArgForEnd, getFirstArg } from './std/sketch'
import {
getConstraintLevelFromSourceRange,
@ -79,7 +80,28 @@ export function getNodeFromPath<T>(
deepPath: successfulPaths,
}
}
return new Error('not an object')
const stackTraceError = new Error()
const sourceCode = recast(node)
const levels = stackTraceError.stack?.split('\n')
const aFewFunctionNames: string[] = []
let tree = ''
levels?.forEach((val, index) => {
const fnName = val.trim().split(' ')[1]
const ending = index === levels.length - 1 ? ' ' : ' > '
tree += fnName + ending
if (index < 3) {
aFewFunctionNames.push(fnName)
}
})
const error = new Error(
`Failed to stopAt ${stopAt}, ${aFewFunctionNames
.filter((a) => a)
.join(' > ')}`
)
console.error(tree)
console.error(sourceCode)
console.error(error.stack)
return error
}
parent = currentNode
parentEdge = pathItem[0]
@ -90,7 +112,7 @@ export function getNodeFromPath<T>(
}
if (
typeof stopAt !== 'undefined' &&
(Array.isArray(stopAt)
(isArray(stopAt)
? stopAt.includes(currentNode.type)
: currentNode.type === stopAt)
) {
@ -145,6 +167,7 @@ export function getNodeFromPathCurry(
type KCLNode = Node<
| Expr
| ExpressionStatement
| ImportStatement
| VariableDeclaration
| VariableDeclarator
| ReturnStatement
@ -241,10 +264,14 @@ export function traverse(
// hmm this smell
_traverse(_node.object, [...pathToNode, ['object', 'MemberExpression']])
_traverse(_node.property, [...pathToNode, ['property', 'MemberExpression']])
} else if ('body' in _node && Array.isArray(_node.body)) {
_node.body.forEach((expression, index) =>
} else if (_node.type === 'ImportStatement') {
// Do nothing.
} else if ('body' in _node && isArray(_node.body)) {
// TODO: Program should have a type field, but it currently doesn't.
const program = node as Node<Program>
program.body.forEach((expression, index) => {
_traverse(expression, [...pathToNode, ['body', ''], [index, 'index']])
)
})
}
option?.leave?.(_node)
}

View File

@ -248,6 +248,8 @@ class EngineConnection extends EventTarget {
mediaStream?: MediaStream
idleMode: boolean = false
promise?: Promise<void>
sdpAnswer?: Models['RtcSessionDescription_type']
triggeredStart = false
onIceCandidate = function (
this: RTCPeerConnection,
@ -553,6 +555,7 @@ class EngineConnection extends EventTarget {
* did not establish.
*/
connect(reconnecting?: boolean): Promise<void> {
const that = this
return new Promise((resolve) => {
if (this.isConnecting() || this.isReady()) {
return
@ -583,8 +586,38 @@ class EngineConnection extends EventTarget {
},
}
const initiateConnectingExclusive = () => {
if (that.triggeredStart) return
that.triggeredStart = true
// Start connecting.
that.state = {
type: EngineConnectionStateType.Connecting,
value: {
type: ConnectingType.WebRTCConnecting,
},
}
// As soon as this is set, RTCPeerConnection tries to
// establish a connection.
// @ts-expect-error: Have to ignore because dom.ts doesn't have the right type
void that.pc?.setRemoteDescription(that.sdpAnswer)
that.state = {
type: EngineConnectionStateType.Connecting,
value: {
type: ConnectingType.SetRemoteDescription,
},
}
}
this.onIceCandidate = (event: RTCPeerConnectionIceEvent) => {
console.log('icecandidate', event.candidate)
// This is null when the ICE gathering state is done.
// Windows ONLY uses this to signal it's done!
if (event.candidate === null) {
initiateConnectingExclusive()
return
}
@ -595,7 +628,6 @@ class EngineConnection extends EventTarget {
},
}
// Request a candidate to use
this.send({
type: 'trickle_ice',
candidate: {
@ -605,8 +637,38 @@ class EngineConnection extends EventTarget {
usernameFragment: event.candidate.usernameFragment || undefined,
},
})
// Sometimes the remote end doesn't report the end of candidates.
// They have 3 seconds to.
setTimeout(() => {
initiateConnectingExclusive()
}, 3000)
}
this.pc?.addEventListener?.('icecandidate', this.onIceCandidate)
this.pc?.addEventListener?.(
'icegatheringstatechange',
function (_event) {
console.log('icegatheringstatechange', this.iceGatheringState)
if (this.iceGatheringState !== 'complete') return
initiateConnectingExclusive()
}
)
this.pc?.addEventListener?.(
'iceconnectionstatechange',
function (_event) {
console.log('iceconnectionstatechange', this.iceConnectionState)
console.log('iceconnectionstatechange', this.iceGatheringState)
}
)
this.pc?.addEventListener?.('negotiationneeded', function (_event) {
console.log('negotiationneeded', this.iceConnectionState)
console.log('negotiationneeded', this.iceGatheringState)
})
this.pc?.addEventListener?.('signalingstatechange', function (event) {
console.log('signalingstatechange', this.signalingState)
})
this.onIceCandidateError = (_event: Event) => {
const event = _event as RTCPeerConnectionIceErrorEvent
@ -634,6 +696,8 @@ class EngineConnection extends EventTarget {
})
)
break
case 'connecting':
break
case 'disconnected':
case 'failed':
this.pc?.removeEventListener('icecandidate', this.onIceCandidate)
@ -1126,25 +1190,8 @@ class EngineConnection extends EventTarget {
},
}
// As soon as this is set, RTCPeerConnection tries to
// establish a connection.
// @ts-ignore
// Have to ignore because dom.ts doesn't have the right type
void this.pc?.setRemoteDescription(answer)
this.sdpAnswer = answer
this.state = {
type: EngineConnectionStateType.Connecting,
value: {
type: ConnectingType.SetRemoteDescription,
},
}
this.state = {
type: EngineConnectionStateType.Connecting,
value: {
type: ConnectingType.WebRTCConnecting,
},
}
break
case 'trickle_ice':
@ -1235,6 +1282,7 @@ class EngineConnection extends EventTarget {
if (closedPc && closedUDC && closedWS) {
// Do not notify the rest of the program that we have cut off anything.
this.state = { type: EngineConnectionStateType.Disconnected }
this.triggeredStart = false
}
}
}
@ -1389,6 +1437,7 @@ export class EngineCommandManager extends EventTarget {
enableSSAO: true,
showScaleGrid: false,
cameraProjection: 'perspective',
cameraOrbit: 'spherical',
}
}
@ -1437,6 +1486,7 @@ export class EngineCommandManager extends EventTarget {
enableSSAO: true,
showScaleGrid: false,
cameraProjection: 'orthographic',
cameraOrbit: 'spherical',
},
// When passed, use a completely separate connecting code path that simply
// opens a websocket and this is a function that is called when connected.
@ -1999,7 +2049,7 @@ export class EngineCommandManager extends EventTarget {
.catch((e) => {
// TODO: Previously was never caught, we are not rejecting these pendingCommands but this needs to be handled at some point.
/*noop*/
return null
return e
})
}
/**

View File

@ -60,7 +60,7 @@ import {
mutateObjExpProp,
findUniqueName,
} from 'lang/modifyAst'
import { roundOff, getLength, getAngle } from 'lib/utils'
import { roundOff, getLength, getAngle, isArray } from 'lib/utils'
import { err } from 'lib/trap'
import { perpendicularDistance } from 'sketch-helpers'
import { TagDeclarator } from 'wasm-lib/kcl/bindings/TagDeclarator'
@ -96,7 +96,7 @@ export function createFirstArg(
sketchFn: ToolTip,
val: Expr | [Expr, Expr] | [Expr, Expr, Expr]
): Expr | Error {
if (Array.isArray(val)) {
if (isArray(val)) {
if (
[
'angledLine',

View File

@ -57,7 +57,7 @@ import {
getSketchSegmentFromPathToNode,
getSketchSegmentFromSourceRange,
} from './sketchConstraints'
import { getAngle, roundOff, normaliseAngle } from '../../lib/utils'
import { getAngle, roundOff, normaliseAngle, isArray } from '../../lib/utils'
import { Node } from 'wasm-lib/kcl/bindings/Node'
import { findKwArg, findKwArgAny } from 'lang/util'
@ -122,7 +122,7 @@ function createCallWrapper(
tag?: Expr,
valueUsedInTransform?: number
): CreatedSketchExprResult {
if (Array.isArray(val)) {
if (isArray(val)) {
if (tooltip === 'line') {
const labeledArgs = [createLabeledArg('end', createArrayExpression(val))]
if (tag) {
@ -1330,12 +1330,12 @@ export function getRemoveConstraintsTransform(
// check if the function has no constraints
const isTwoValFree =
Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
if (isTwoValFree) {
return false
}
const isOneValFree =
!Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
!isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
if (isOneValFree) {
return transformInfo
}
@ -1649,7 +1649,7 @@ export function getConstraintType(
// and for one val sketch functions that the arg is NOT locked down
// these conditions should have been checked previously.
// completely locked down or not locked down at all does not depend on the fnName so we can check that first
const isArr = Array.isArray(val)
const isArr = isArray(val)
if (!isArr) {
if (fnName === 'xLine') return 'yRelative'
if (fnName === 'yLine') return 'xRelative'
@ -2113,9 +2113,9 @@ export function getConstraintLevelFromSourceRange(
// check if the function has no constraints
const isTwoValFree =
Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
const isOneValFree =
!Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
!isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
if (isTwoValFree) return { level: 'free', range: range }
if (isOneValFree) return { level: 'partial', range: range }
@ -2128,7 +2128,7 @@ export function isLiteralArrayOrStatic(
): boolean {
if (!val) return false
if (Array.isArray(val)) {
if (isArray(val)) {
const a = val[0]
const b = val[1]
return isLiteralArrayOrStatic(a) && isLiteralArrayOrStatic(b)
@ -2142,7 +2142,7 @@ export function isLiteralArrayOrStatic(
export function isNotLiteralArrayOrStatic(
val: Expr | [Expr, Expr] | [Expr, Expr, Expr]
): boolean {
if (Array.isArray(val)) {
if (isArray(val)) {
const a = val[0]
const b = val[1]
return isNotLiteralArrayOrStatic(a) && isNotLiteralArrayOrStatic(b)

View File

@ -12,7 +12,7 @@ import {
NumericSuffix,
} from './wasm'
import { filterArtifacts } from 'lang/std/artifactGraph'
import { isOverlap } from 'lib/utils'
import { isArray, isOverlap } from 'lib/utils'
export function updatePathToNodeFromMap(
oldPath: PathToNode,
@ -40,8 +40,8 @@ export function isCursorInSketchCommandRange(
predicate: (artifact) => {
return selectionRanges.graphSelections.some(
(selection) =>
Array.isArray(selection?.codeRef?.range) &&
Array.isArray(artifact?.codeRef?.range) &&
isArray(selection?.codeRef?.range) &&
isArray(artifact?.codeRef?.range) &&
isOverlap(selection?.codeRef?.range, artifact.codeRef.range)
)
},

View File

@ -18,6 +18,7 @@ import {
default_project_settings,
base64_decode,
clear_scene_and_bust_cache,
change_kcl_settings,
reloadModule,
} from 'lib/wasm_lib_wrapper'
@ -56,6 +57,7 @@ import { ArtifactGraph as RustArtifactGraph } from 'wasm-lib/kcl/bindings/Artifa
import { Artifact } from './std/artifactGraph'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { NumericSuffix } from 'wasm-lib/kcl/bindings/NumericSuffix'
import { MetaSettings } from 'wasm-lib/kcl/bindings/MetaSettings'
export type { Artifact } from 'wasm-lib/kcl/bindings/Artifact'
export type { ArtifactCommand } from 'wasm-lib/kcl/bindings/Artifact'
@ -156,6 +158,12 @@ export function isTopLevelModule(range: SourceRange): boolean {
return range[2] === 0
}
function firstSourceRange(error: RustKclError): SourceRange {
return error.sourceRanges.length > 0
? sourceRangeFromRust(error.sourceRanges[0])
: defaultSourceRange()
}
export const wasmUrl = () => {
// For when we're in electron (file based) or web server (network based)
// For some reason relative paths don't work as expected. Otherwise we would
@ -253,7 +261,7 @@ export const parse = (code: string | Error): ParseResult | Error => {
return new KCLError(
parsed.kind,
parsed.msg,
sourceRangeFromRust(parsed.sourceRanges[0]),
firstSourceRange(parsed),
[],
[],
defaultArtifactGraph()
@ -620,7 +628,7 @@ export const executor = async (
const kclError = new KCLError(
parsed.error.kind,
parsed.error.msg,
sourceRangeFromRust(parsed.error.sourceRanges[0]),
firstSourceRange(parsed.error),
parsed.operations,
parsed.artifactCommands,
rustArtifactGraphToMap(parsed.artifactGraph)
@ -689,7 +697,7 @@ export const modifyAstForSketch = async (
const kclError = new KCLError(
parsed.kind,
parsed.msg,
sourceRangeFromRust(parsed.sourceRanges[0]),
firstSourceRange(parsed),
[],
[],
defaultArtifactGraph()
@ -760,7 +768,7 @@ export function programMemoryInit(): ProgramMemory | Error {
return new KCLError(
parsed.kind,
parsed.msg,
sourceRangeFromRust(parsed.sourceRanges[0]),
firstSourceRange(parsed),
[],
[],
defaultArtifactGraph()
@ -848,3 +856,17 @@ export function base64Decode(base64: string): ArrayBuffer | Error {
return new Error('Caught error decoding base64 string: ' + e)
}
}
/// Change the meta settings for the kcl file.
/// Returns the new kcl string with the updated settings.
export function changeKclSettings(
kcl: string,
settings: MetaSettings
): string | Error {
try {
return change_kcl_settings(kcl, JSON.stringify(settings))
} catch (e) {
console.error('Caught error changing kcl settings: ' + e)
return new Error('Caught error changing kcl settings: ' + e)
}
}

View File

@ -1,17 +1,14 @@
import { StateMachineCommandSetConfig } from 'lib/commandTypes'
import { authMachine } from 'machines/authMachine'
import { Command } from 'lib/commandTypes'
import { authActor } from 'machines/appMachine'
import { ACTOR_IDS } from 'machines/machineConstants'
type AuthCommandSchema = {}
export const authCommandBarConfig: StateMachineCommandSetConfig<
typeof authMachine,
AuthCommandSchema
> = {
'Log in': {
hide: 'both',
},
'Log out': {
args: [],
export const authCommands: Command[] = [
{
groupId: ACTOR_IDS.AUTH,
name: 'log-out',
displayName: 'Log out',
icon: 'arrowLeft',
needsReview: false,
onSubmit: () => authActor.send({ type: 'Log out' }),
},
}
]

View File

@ -308,7 +308,6 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
description:
'Create a 3D body by moving a sketch region along an arbitrary path.',
icon: 'sweep',
status: 'development',
needsReview: false,
args: {
target: {
@ -317,8 +316,6 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
required: true,
skip: true,
multiple: false,
warningMessage:
'The sweep workflow is new and under tested. Please break it and report issues.',
},
trajectory: {
inputType: 'selection',
@ -368,7 +365,6 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
Revolve: {
description: 'Create a 3D body by rotating a sketch region about an axis.',
icon: 'revolve',
status: 'development',
needsReview: true,
args: {
selection: {
@ -377,8 +373,6 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
multiple: false, // TODO: multiple selection
required: true,
skip: true,
warningMessage:
'The revolve workflow is new and under tested. Please break it and report issues.',
},
axisOrEdge: {
inputType: 'options',

View File

@ -0,0 +1,19 @@
import { parseEngineErrorMessage } from './validators'
describe('parseEngineErrorMessage', () => {
it('takes an engine error string and parses its json message', () => {
const engineError =
'engine error: [{"error_code":"internal_engine","message":"Trajectory curve must be G1 continuous (with continuous tangents)"}]'
const message = parseEngineErrorMessage(engineError)
expect(message).toEqual(
'Trajectory curve must be G1 continuous (with continuous tangents)'
)
})
it('retuns undefined on strings with different formats', () => {
const s1 = 'engine error: []'
const s2 = 'blabla'
expect(parseEngineErrorMessage(s1)).toBeUndefined()
expect(parseEngineErrorMessage(s2)).toBeUndefined()
})
})

View File

@ -3,6 +3,7 @@ import { engineCommandManager } from 'lib/singletons'
import { uuidv4 } from 'lib/utils'
import { CommandBarContext } from 'machines/commandBarMachine'
import { Selections } from 'lib/selections'
import { ApiError_type } from '@kittycad/lib/dist/types/src/models'
export const disableDryRunWithRetry = async (numberOfRetries = 3) => {
for (let tries = 0; tries < numberOfRetries; tries++) {
@ -46,6 +47,20 @@ function isSelections(selections: unknown): selections is Selections {
)
}
export function parseEngineErrorMessage(engineError: string) {
const parts = engineError.split('engine error: ')
if (parts.length < 2) {
return undefined
}
const errors = JSON.parse(parts[1]) as ApiError_type[]
if (!errors[0]) {
return undefined
}
return errors[0].message
}
export const revolveAxisValidator = async ({
data,
context,
@ -83,7 +98,7 @@ export const revolveAxisValidator = async ({
value: 360,
}
const revolveAboutEdgeCommand = async () => {
const command = async () => {
return await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
@ -92,17 +107,18 @@ export const revolveAxisValidator = async ({
angle: angleInDegrees,
edge_id: edgeSelection,
target: sketchSelection,
tolerance: 0.0001,
// Gotcha: Playwright will fail with larger tolerances, need to use a smaller one.
tolerance: 1e-7,
},
})
}
const attemptRevolve = await dryRunWrapper(revolveAboutEdgeCommand)
if (attemptRevolve?.success) {
const result = await dryRunWrapper(command)
if (result?.success) {
return true
} else {
// return error message for the toast
return 'Unable to revolve with selected edge'
}
const reason = parseEngineErrorMessage(result) || 'unknown'
return `Unable to revolve with the current selection. Reason: ${reason}`
}
export const loftValidator = async ({
@ -128,7 +144,7 @@ export const loftValidator = async ({
return 'Unable to loft, selection contains less than two solid2ds'
}
const loftCommand = async () => {
const command = async () => {
// TODO: check what to do with these
const DEFAULT_V_DEGREE = 2
const DEFAULT_TOLERANCE = 2
@ -145,13 +161,13 @@ export const loftValidator = async ({
},
})
}
const attempt = await dryRunWrapper(loftCommand)
if (attempt?.success) {
const result = await dryRunWrapper(command)
if (result?.success) {
return true
} else {
// return error message for the toast
return 'Unable to loft with selected sketches'
}
const reason = parseEngineErrorMessage(result) || 'unknown'
return `Unable to loft with the current selection. Reason: ${reason}`
}
export const shellValidator = async ({
@ -180,7 +196,7 @@ export const shellValidator = async ({
return "Unable to shell, couldn't find the solid"
}
const shellCommand = async () => {
const command = async () => {
// TODO: figure out something better than an arbitrarily small value
const DEFAULT_THICKNESS: Models['LengthUnit_type'] = 1e-9
const DEFAULT_HOLLOW = false
@ -200,12 +216,13 @@ export const shellValidator = async ({
})
}
const attemptShell = await dryRunWrapper(shellCommand)
if (attemptShell?.success) {
const result = await dryRunWrapper(command)
if (result?.success) {
return true
}
return 'Unable to shell with the provided selection'
const reason = parseEngineErrorMessage(result) || 'unknown'
return `Unable to shell with the current selection. Reason: ${reason}`
}
export const sweepValidator = async ({
@ -241,7 +258,7 @@ export const sweepValidator = async ({
}
const target = targetArtifact.pathId
const sweepCommand = async () => {
const command = async () => {
// TODO: second look on defaults here
const DEFAULT_TOLERANCE: Models['LengthUnit_type'] = 1e-7
const DEFAULT_SECTIONAL = false
@ -261,10 +278,11 @@ export const sweepValidator = async ({
})
}
const attemptSweep = await dryRunWrapper(sweepCommand)
if (attemptSweep?.success) {
const result = await dryRunWrapper(command)
if (result?.success) {
return true
}
return 'Unable to sweep with the provided selection'
const reason = parseEngineErrorMessage(result) || 'unknown'
return `Unable to sweep with the current selection. Reason: ${reason}`
}

View File

@ -26,7 +26,7 @@ export const FILE_EXT = '.kcl'
/** Default file to open when a project is opened */
export const PROJECT_ENTRYPOINT = `main${FILE_EXT}` as const
/** Thumbnail file name */
export const PROJECT_IMAGE_NAME = `main.jpg` as const
export const PROJECT_IMAGE_NAME = `thumbnail.png` as const
/** The localStorage key for last-opened projects */
export const FILE_PERSIST_KEY = `${PROJECT_FOLDER}-last-opened` as const
/** The default name given to new kcl files in a project */
@ -68,8 +68,6 @@ export const KCL_DEFAULT_DEGREE = `360`
/** localStorage key for the playwright test-specific app settings file */
export const TEST_SETTINGS_FILE_KEY = 'playwright-test-settings'
export const DEFAULT_HOST = 'https://api.zoo.dev'
export const PROD_APP_URL = 'https://app.zoo.dev'
export const SETTINGS_FILE_NAME = 'settings.toml'
export const TOKEN_FILE_NAME = 'token.txt'
export const PROJECT_SETTINGS_FILE_NAME = 'project.toml'
@ -145,7 +143,7 @@ export const VIEW_NAMES_SEMANTIC = {
export const SIDEBAR_BUTTON_SUFFIX = '-pane-button'
/** Custom URL protocol our desktop registers */
export const ZOO_STUDIO_PROTOCOL = 'zoo-studio:'
export const ZOO_STUDIO_PROTOCOL = 'zoo-studio'
/**
* A query parameter that triggers a modal

View File

@ -10,6 +10,7 @@ import {
import {
PROJECT_ENTRYPOINT,
PROJECT_FOLDER,
PROJECT_IMAGE_NAME,
PROJECT_SETTINGS_FILE_NAME,
SETTINGS_FILE_NAME,
TELEMETRY_FILE_NAME,
@ -625,3 +626,19 @@ export const getUser = async (
}
return Promise.reject(new Error('unreachable'))
}
export const writeProjectThumbnailFile = async (
dataUrl: string,
projectDirectoryPath: string
) => {
const filePath = window.electron.path.join(
projectDirectoryPath,
PROJECT_IMAGE_NAME
)
const data = atob(dataUrl.substring('data:image/png;base64,'.length))
const asArray = new Uint8Array(data.length)
for (let i = 0, len = data.length; i < len; ++i) {
asArray[i] = data.charCodeAt(i)
}
return window.electron.writeFile(filePath, asArray)
}

View File

@ -73,7 +73,7 @@ filletSketch = startSketchOn('XZ')
}, %)
// Sketch the bend
filletExtrude = extrude(-width, filletSketch)
filletExtrude = extrude(filletSketch, length = -width)
// Create a custom plane for the leg that sits on the wall
customPlane = {
@ -102,7 +102,7 @@ bracketLeg2Sketch = startSketchOn(customPlane)
}, %), %)
// Extrude the second leg
bracketLeg2Extrude = extrude(-thickness, bracketLeg2Sketch)
bracketLeg2Extrude = extrude(bracketLeg2Sketch, length = -thickness)
|> fillet({
radius = extFilletRadius,
tags = [

View File

@ -2,13 +2,12 @@ import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarnin
import { Command, CommandArgumentOption } from './commandTypes'
import { codeManager, kclManager } from './singletons'
import { isDesktop } from './isDesktop'
import { FILE_EXT, PROJECT_SETTINGS_FILE_NAME } from './constants'
import { FILE_EXT } from './constants'
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
import { parseProjectSettings } from 'lang/wasm'
import { err, reportRejection } from './trap'
import { projectConfigurationToSettingsPayload } from './settings/settingsUtils'
import { copyFileShareLink } from './links'
import { reportRejection } from './trap'
import { IndexLoaderData } from './types'
import { IS_NIGHTLY_OR_DEBUG } from 'routes/Settings'
import { copyFileShareLink } from './links'
interface OnSubmitProps {
sampleName: string
@ -68,23 +67,9 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] {
const sampleCodeUrl = `https://raw.githubusercontent.com/KittyCAD/kcl-samples/main/${encodeURIComponent(
projectPathPart
)}/${encodeURIComponent(primaryKclFile)}`
const sampleSettingsFileUrl = `https://raw.githubusercontent.com/KittyCAD/kcl-samples/main/${encodeURIComponent(
projectPathPart
)}/${PROJECT_SETTINGS_FILE_NAME}`
Promise.allSettled([fetch(sampleCodeUrl), fetch(sampleSettingsFileUrl)])
.then((results) => {
const a =
'value' in results[0] ? results[0].value : results[0].reason
const b =
'value' in results[1] ? results[1].value : results[1].reason
return [a, b]
})
.then(
async ([
codeResponse,
settingsResponse,
]): Promise<OnSubmitProps> => {
fetch(sampleCodeUrl)
.then(async (codeResponse): Promise<OnSubmitProps> => {
if (!codeResponse.ok) {
console.error(
'Failed to fetch sample code:',
@ -93,31 +78,12 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] {
return Promise.reject(new Error('Failed to fetch sample code'))
}
const code = await codeResponse.text()
// It's possible that a sample doesn't have a project.toml
// associated with it.
let projectSettingsPayload: ReturnType<
typeof projectConfigurationToSettingsPayload
> = {}
if (settingsResponse.ok) {
const parsedProjectSettings = parseProjectSettings(
await settingsResponse.text()
)
if (!err(parsedProjectSettings)) {
projectSettingsPayload =
projectConfigurationToSettingsPayload(parsedProjectSettings)
}
}
return {
sampleName: data.sample.split('/')[0] + FILE_EXT,
code,
method: data.method,
sampleUnits:
projectSettingsPayload.modeling?.defaultUnit || 'mm',
}
}
)
})
.then((props) => {
if (props?.code) {
commandProps.specialPropsForSampleCommand
@ -168,21 +134,22 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] {
},
},
},
// {
// name: 'share-file-link',
// displayName: 'Share file',
// description: 'Create a link that contains a copy of the current file.',
// groupId: 'code',
// needsReview: false,
// icon: 'link',
// onSubmit: () => {
// copyFileShareLink({
// token: commandProps.authToken,
// code: codeManager.code,
// name: commandProps.projectData.project?.name || '',
// units: commandProps.settings.defaultUnit,
// }).catch(reportRejection)
// },
// },
{
name: 'share-file-link',
displayName: 'Share file',
hide: IS_NIGHTLY_OR_DEBUG ? undefined : 'desktop',
description: 'Create a link that contains a copy of the current file.',
groupId: 'code',
needsReview: false,
icon: 'link',
onSubmit: () => {
copyFileShareLink({
token: commandProps.authToken,
code: codeManager.code,
name: commandProps.projectData.project?.name || '',
units: commandProps.settings.defaultUnit,
}).catch(reportRejection)
},
},
]
}

View File

@ -1,3 +1,4 @@
import { VITE_KC_SITE_APP_URL } from 'env'
import { createCreateFileUrl } from './links'
describe(`link creation tests`, () => {
@ -8,7 +9,7 @@ describe(`link creation tests`, () => {
// Converted with external online tools
const expectedEncodedCode = `ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg%3D%3D`
const expectedLink = `http://localhost:3000/?create-file=true&name=test&units=mm&code=${expectedEncodedCode}&ask-open-desktop=true`
const expectedLink = `${VITE_KC_SITE_APP_URL}/?create-file=true&name=test&units=mm&code=${expectedEncodedCode}&ask-open-desktop=true`
const result = createCreateFileUrl({ code, name, units })
expect(result.toString()).toBe(expectedLink)

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