Compare commits

..

132 Commits

Author SHA1 Message Date
0dab8d6f05 KCL: Improve error msg format when typechecking tag declarators 2025-01-31 11:09:52 -06:00
28b5a088d2 Merge branch 'main' into achalmers/kw-fn-sketches 2025-01-31 10:34:23 -06:00
057face2e2 Merge branch 'main' into achalmers/kw-fn-sketches 2025-01-31 09:39:21 -06:00
57f3cd1ba4 Merge branch 'nadro/gh-5157/center-rectangle-kcl-fix' into achalmers/kw-fn-sketches 2025-01-31 09:27:22 -06:00
32d2fb639c fix: adding a wait for execution to prevent clicking before lines are rendered 2025-01-31 08:44:34 -06:00
95ad629558 Add number with units formatting as KCL (#5195)
* Add number with units formatting as KCL

* Change type assertion helper to check what we need

* Fix rectangle unit test
2025-01-31 06:53:34 -06:00
ae9731b21a bump 2025-01-30 20:26:38 -06:00
c4ceaaf6b2 Merge branch 'nadro/gh-5157/center-rectangle-kcl-fix' of github.com:KittyCAD/modeling-app into nadro/gh-5157/center-rectangle-kcl-fix 2025-01-30 20:26:12 -06:00
60299c6520 bump 2025-01-30 20:26:05 -06:00
fcfdffaab8 A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) 2025-01-31 02:05:39 +00:00
fe3ab30627 fix: restore this... see if snapshots trigger again 2025-01-30 19:58:20 -06:00
259844ef11 fix: updating the type narrowing function 2025-01-30 19:55:04 -06:00
f7ab29047c why is this fallback here 2025-01-30 19:50:56 -06:00
82ad0ac8c7 A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) 2025-01-31 01:48:44 +00:00
4463833460 Merge branch 'nadro/gh-5157/center-rectangle-kcl-fix' of github.com:KittyCAD/modeling-app into nadro/gh-5157/center-rectangle-kcl-fix 2025-01-30 19:41:20 -06:00
f48c8fefd6 fix: implementing a safer dereference method until we update createLiteraly() 2025-01-30 19:41:13 -06:00
73a878b0aa A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) 2025-01-31 01:39:47 +00:00
d04ac2bbb4 fix: updating formatting 2025-01-30 19:33:05 -06:00
f5ec62ab27 Merge branch 'main' into nadro/gh-5157/center-rectangle-kcl-fix 2025-01-30 19:22:11 -06:00
2c28ad6866 Code review from Jon 2025-01-30 16:09:07 -06:00
989a2278cb A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) 2025-01-30 21:45:17 +00:00
01cb6df3b4 Fuck it, update all the snapshots 2025-01-30 15:33:46 -06:00
ab00c7bcc8 Merge branch 'main' into achalmers/kw-fn-sketches 2025-01-30 11:40:22 -06:00
1c589960bf void 2025-01-30 11:39:04 -06:00
a6b2d32b95 A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) 2025-01-29 23:07:40 +00:00
78e1bec0c7 void 2025-01-29 16:55:14 -06:00
c18c8f9e81 A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) 2025-01-29 21:57:40 +00:00
95b647676b Update snapshot with new KCL 2025-01-29 15:45:26 -06:00
b5182c89d0 Yarn fmt 2025-01-29 15:16:02 -06:00
069b1cb370 Fix up frontend unit tests 2025-01-29 15:12:36 -06:00
012da726ec WIP: Fix up artifact graph tests 2025-01-29 14:13:50 -06:00
7b3cfb1c2f Ensure no failing simulation tests are suddenly passing 2025-01-29 13:48:21 -06:00
5f3cf61d0f Fix sample KCL, extrude needs to use kw args 2025-01-29 13:20:20 -06:00
1f3455e840 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.
2025-01-29 12:01:25 -06:00
260ec9fe45 Fix broken code test 2025-01-29 10:49:12 -06:00
1ae55c2946 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.
2025-01-28 17:49:52 -06:00
0400f33fc3 Merge branch 'main' into achalmers/kw-fn-sketches 2025-01-28 17:11:09 -06:00
734e590e09 Merge branch 'main' into achalmers/kw-fn-sketches 2025-01-28 16:37:09 -06:00
2a5433a2a7 Fix edge treatment tests 2025-01-28 15:19:03 -06:00
519041c8fa fix: updated the LiteralValue dereferencing and added some type narrowing helpers 2025-01-28 15:15:48 -06:00
c6d563f08f fix: new Literal data structure update 2025-01-28 13:04:52 -06:00
82d6446e5e Empty commit to unstick CI 2025-01-27 17:00:33 +13:00
e34a4db077 A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) 2025-01-27 17:00:33 +13:00
665ec8a6b5 simple test fixups
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-01-27 17:00:33 +13:00
ef8131f33d Changing stdlib changed the order of autocompletes
There's no longer a `lineTo`, so there's only one item before `xLine`
not two like previously. So, the instructions for how to select the
autocompletion for xLine need to be updated (one fewer down arrow key
press is needed)
2025-01-27 08:58:47 +13:00
b7ec0eeb6e Fix test expectation 2025-01-27 08:58:47 +13:00
43c12e523b Fix: Update expected hover text for 'line' fn 2025-01-27 08:58:47 +13:00
b74216378d Fix more missing tags from bad regex 2025-01-27 08:58:47 +13:00
7031b6f0a7 Update sweep test KCL 2025-01-27 08:58:47 +13:00
e1ce83a72e Fix: Restore more tags which my bad regex mistakenly erased
Only 18 failing now
2025-01-27 08:58:47 +13:00
f6c335181e Fix pathToNode for new extrude nodes
This means there's only 26 failing (locally on my Macbook)
2025-01-27 08:58:47 +13:00
25d9c19763 Migrate Extrude AST mod from XState action to actor 2025-01-27 08:57:36 +13:00
871890415c Rectangle tool should use default unlabeled, fix broken regex find-replace 2025-01-27 08:57:36 +13:00
7f9526bb81 Fix revolve 2025-01-27 08:57:36 +13:00
aafa1a398d Fix shell 2025-01-27 08:57:36 +13:00
fda7acf0ff 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.
2025-01-27 08:57:36 +13:00
bf306e774d 34 failing 2025-01-27 08:57:36 +13:00
2abd516db4 Same as previous 2025-01-27 08:57:36 +13:00
e26d7e741f Same as previous 2025-01-27 08:57:36 +13:00
91df6b9511 Style fix: skip % when creating new line calls
The unlabeled arg is automatically set to % anyway.
2025-01-27 08:57:36 +13:00
3cecb99aa1 Fix: mutateKwArg was appending not overwriting args 2025-01-27 08:57:36 +13:00
e8961d4190 Fix 'remove constraint' on kw arg calls 2025-01-27 08:57:36 +13:00
da3788afba Fix: make Toolbar components support CallExpressionKw 2025-01-27 08:57:36 +13:00
12ed315244 A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) 2025-01-27 08:57:33 +13:00
9c35c06e58 Rebasing fixups
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-01-27 08:56:37 +13:00
a69da088cd Some line fixes
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-01-27 08:56:37 +13:00
38e59df85c Fix some uses of close and lineTo
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-01-27 08:56:37 +13:00
8bac097743 Fix the 'constrain length' test 2025-01-27 08:56:37 +13:00
09df8a734a Fix some uses of extrude
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-01-27 08:56:37 +13:00
da2072e005 Fix drafting lines 2025-01-27 08:56:37 +13:00
cb4a47705c Fix point-and-click sketch mode to use kwargs 2025-01-27 08:56:37 +13:00
1227b38e12 Update e2e test code with new syntax 2025-01-27 08:56:37 +13:00
8e343a254f Remove expect from application code 2025-01-27 08:56:37 +13:00
2559309f85 Update snapshots 2025-01-27 08:56:37 +13:00
26f01bce76 fmt 2025-01-27 08:56:37 +13:00
729a34efed Fixed more e2e tests 2025-01-27 08:56:37 +13:00
5191013db2 Correct sourceRange expectations 2025-01-27 08:56:37 +13:00
d653b852dd Add back a missing import 2025-01-27 08:56:37 +13:00
f3a2922ba0 Remove .only and fix resulting tests 2025-01-27 08:56:37 +13:00
ce8aeab508 Start fixing playwright tests 2025-01-27 08:56:37 +13:00
ee6014d0cf Update helix docs 2025-01-27 08:56:37 +13:00
bd329de3e3 Fix the last graph test 2025-01-27 08:56:37 +13:00
5d1df40de5 WIP 2025-01-27 08:56:37 +13:00
21ea1d785a Support kwargs in addEdgeTreatment 2025-01-27 08:56:37 +13:00
987152e166 Fix some graph tests 2025-01-27 08:56:37 +13:00
6b37bf1809 Start using kwargs branch of kcl-samples 2025-01-27 08:56:37 +13:00
617dcfee34 Fix git merge conflicts from Helix and Cache work 2025-01-27 08:56:37 +13:00
10c446151b 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.
2025-01-27 08:56:37 +13:00
99866b5f9a 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.
2025-01-27 08:56:37 +13:00
cd022fb087 close is now a kw function 2025-01-27 08:56:37 +13:00
fda4aeb505 Bugfix: don't duplicate tag 2025-01-27 08:56:37 +13:00
d9fe9bafba Fix another test 2025-01-27 08:56:37 +13:00
9dc06f6b92 Fix another test, allow it to use callExpressionKw 2025-01-27 08:56:37 +13:00
9e778280b3 Fix another test, addTagKw
addTag helper was not aware of kw args.
2025-01-27 08:56:37 +13:00
141c38c92b Convert old test KCL to kwargs 2025-01-27 08:56:37 +13:00
fc7e6993ca Update sourceRanges 2025-01-27 08:56:37 +13:00
ad8336ef57 Call the KW helper, not just positional 2025-01-27 08:56:37 +13:00
a819d16798 Only 13 failing 2025-01-27 08:56:37 +13:00
1c8cc54c9d Update test to expect kwargs syntax 2025-01-27 08:56:37 +13:00
70766cef69 Fix tests
Assume the line() kwargs calls are being generated in a |>, so they should
not set an unlabeled arg (it will default to % which it was previously setting)
2025-01-27 08:56:37 +13:00
a69ccf960f Refactor: move some strings into named constants 2025-01-27 08:56:37 +13:00
308b7121fb Fix more queryAst code that wasn't aware of CallExpressionKw
Down to 15 tests failing!!!!!!!!!!

YEAH BABYYYY!!!
2025-01-27 08:56:37 +13:00
6522f1cbe7 Fix another queryAst test 2025-01-27 08:56:37 +13:00
2943ea1fd6 Fix test: traverse was not handling CallExpressionKw 2025-01-27 08:56:36 +13:00
ae0860a775 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
2025-01-27 08:56:36 +13:00
ff71250d5d Fix test 'should recognize that sketch001 has been extruded' 2025-01-27 08:56:36 +13:00
81e2fd201d Remove .only calls 2025-01-27 08:56:36 +13:00
de2dfc24dc Fix more tests: getConstraintType calls weren't checking if the
'line' fn was absolute or relative.
2025-01-27 08:56:36 +13:00
84cd2c0c88 Test now uses callExpressionKw not positional 2025-01-27 08:56:36 +13:00
440173d045 Fix lints, fix one test sourcerange assertions 2025-01-27 08:56:36 +13:00
ddafbf059c 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.
2025-01-27 08:56:36 +13:00
e86a5fa614 Use new transformMap lookup even if line is unconstrained 2025-01-27 08:56:36 +13:00
d386c92c8e Fix minor tests + lints 2025-01-27 08:56:36 +13:00
248b2d2a2b 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.
2025-01-27 08:56:36 +13:00
72f84c3a8f Fix constraint checking in line 2025-01-27 08:56:36 +13:00
ecec739632 Fix test 'yRelative to horizontal distance'
Fixes:
 - Make a lineTo helper
 - Fix pathToNode to go through the labeled arg .arg property
2025-01-27 08:56:36 +13:00
cd28a4a9a1 yarn fmt 2025-01-27 08:56:36 +13:00
18b1d84e5c Fix up Rust changes 2025-01-27 08:56:36 +13:00
fb59996c54 Fix lints 2025-01-27 08:56:36 +13:00
d66c7cfe33 Fixed test 'Should reorder when user selects last-to-first' 2025-01-27 08:56:36 +13:00
e917eb9af6 Update test KCL 2025-01-27 08:56:36 +13:00
cc9a14ddd9 Fix some tests 2025-01-27 08:56:36 +13:00
235e526e2d WIP: Start fixing frontend 2025-01-27 08:56:36 +13:00
725ffd952c JS: Rectangle and extrude buttons now generate keyword args 2025-01-27 08:56:36 +13:00
2848d6c77c bugfix: Use = not : in autocomplete 2025-01-27 08:56:36 +13:00
989ca83971 Update more test KCL 2025-01-27 08:56:36 +13:00
bdeda51078 LSP: Update examples to use new KCL stdlib 2025-01-27 08:56:36 +13:00
b079af20da More autocomplete work 2025-01-27 08:56:36 +13:00
641efb0750 Change arg_docs to args 2025-01-27 08:56:36 +13:00
69f036e61b Move get_autocomplete_snippet_from_schema into the stdlib fn arg struct 2025-01-27 08:56:36 +13:00
57ca6d52d5 WIP: Autocomplete 2025-01-27 08:56:36 +13:00
a186517bd0 KCL: Line and Extrude use kw args 2025-01-27 08:56:36 +13:00
253 changed files with 7107 additions and 10601 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,8 +1,5 @@
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,13 +29,6 @@
{
"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,6 +134,8 @@ 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: |
@ -163,8 +165,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, and points to dev infra
run: yarn tronb:package:dev
# this is just not doing any codesign or release yml generation
run: yarn electron-builder --config
- name: Build the app (release)
if: ${{ env.IS_RELEASE == 'true' || env.IS_NIGHTLY == 'true' }}
@ -183,7 +185,7 @@ jobs:
with:
timeout_minutes: 10
max_attempts: 3
command: yarn tronb:package:prod
command: yarn electron-builder --config --publish always
- name: List artifacts in out/
run: ls -R out
@ -244,7 +246,7 @@ jobs:
with:
timeout_minutes: 10
max_attempts: 3
command: yarn tronb:package:prod
command: yarn electron-builder --config --publish always
- uses: actions/upload-artifact@v4
if: ${{ env.IS_RELEASE == 'true' }}
@ -388,19 +390,19 @@ jobs:
- name: Authenticate to Google Cloud
if: ${{ env.IS_NIGHTLY == 'true' }}
uses: 'google-github-actions/auth@v2.1.8'
uses: 'google-github-actions/auth@v2.1.7'
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.4
uses: google-github-actions/setup-gcloud@v2.1.2
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.2
uses: google-github-actions/upload-cloud-storage@v2.2.1
with:
path: out
glob: '*'

View File

@ -46,15 +46,7 @@ jobs:
runs-on: ${{ matrix.os }}
needs: check-rust-changes
steps:
- uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ secrets.MODELING_APP_GH_APP_ID }}
private-key: ${{ secrets.MODELING_APP_GH_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- uses: actions/checkout@v4
with:
token: ${{ steps.app-token.outputs.token }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@ -131,23 +123,23 @@ jobs:
if: steps.download-wasm.outcome == 'failure'
shell: bash
run: yarn build:wasm
- name: build web
- name: build electron
shell: bash
run: yarn tronb:vite:dev
- 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 }}
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:
@ -170,20 +162,20 @@ jobs:
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 }}
# - 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'

View File

@ -108,17 +108,17 @@ jobs:
run: yarn files:set-notes
- name: Authenticate to Google Cloud
uses: 'google-github-actions/auth@v2.1.8'
uses: 'google-github-actions/auth@v2.1.7'
with:
credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}'
- name: Set up Google Cloud SDK
uses: google-github-actions/setup-gcloud@v2.1.4
uses: google-github-actions/setup-gcloud@v2.1.2
with:
project_id: ${{ env.GOOGLE_CLOUD_PROJECT_ID }}
- name: Upload release files to public bucket
uses: google-github-actions/upload-cloud-storage@v2.2.2
uses: google-github-actions/upload-cloud-storage@v2.2.1
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 with electron-builder, run `yarn tronb:package:dev` (or `yarn tronb:package:prod` to point to the .env.production variables)
To build, run `yarn tron:package`.
## Checking out commits / Bisecting

View File

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

View File

@ -4,125 +4,112 @@ 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',
{ tag: ['@skipWin'] },
() => {
const sketchOnPlaneAndBackSideTest = async (
page: Page,
homePage: HomePageFixture,
plane: string,
clickCoords: { x: number; y: number }
) => {
const u = await getUtils(page)
const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setBodyDimensions({ width: 1200, height: 500 })
test.describe('Can create sketches on all planes and their back sides', () => {
const sketchOnPlaneAndBackSideTest = async (
page: Page,
homePage: HomePageFixture,
plane: string,
clickCoords: { x: number; y: number }
) => {
const u = await getUtils(page)
const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await u.openDebugPanel()
await homePage.goToModelingScene()
await u.openDebugPanel()
const coord =
plane === '-XY' || plane === '-YZ' || plane === 'XZ' ? -100 : 100
const camCommand: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center: { x: 0, y: 0, z: 0 },
vantage: { x: coord, y: coord, z: coord },
up: { x: 0, y: 0, z: 1 },
},
}
const updateCamCommand: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
}
const coord =
plane === '-XY' || plane === '-YZ' || plane === 'XZ' ? -100 : 100
const camCommand: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center: { x: 0, y: 0, z: 0 },
vantage: { x: coord, y: coord, z: coord },
up: { x: 0, y: 0, z: 1 },
},
}
const updateCamCommand: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
}
const code = `sketch001 = startSketchOn('${plane}')
const code = `sketch001 = startSketchOn('${plane}')
|> startProfileAt([0.9, -1.22], %)`
await u.openDebugPanel()
await u.openDebugPanel()
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await u.sendCustomCmd(camCommand)
await page.waitForTimeout(100)
await u.sendCustomCmd(updateCamCommand)
await u.sendCustomCmd(camCommand)
await page.waitForTimeout(100)
await u.sendCustomCmd(updateCamCommand)
await u.closeDebugPanel()
await page.mouse.click(clickCoords.x, clickCoords.y)
await page.waitForTimeout(300) // wait for animation
await u.closeDebugPanel()
await page.mouse.click(clickCoords.x, clickCoords.y)
await page.waitForTimeout(300) // wait for animation
await expect(
page.getByRole('button', { name: 'line Line', exact: true })
).toBeVisible()
await expect(
page.getByRole('button', { name: 'line Line', exact: true })
).toBeVisible()
// draw a line
const startXPx = 600
// draw a line
const startXPx = 600
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await expect(page.locator('.cm-content')).toHaveText(code)
await expect(page.locator('.cm-content')).toHaveText(code)
await page
.getByRole('button', { name: 'line Line', exact: true })
.first()
.click()
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.expectCmdLog('[data-message-type="execution-done"]')
await page
.getByRole('button', { name: 'line Line', exact: true })
.first()
.click()
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.clearCommandLogs()
await u.removeCurrentCode()
}
test('XY', async ({ page, homePage }) => {
await sketchOnPlaneAndBackSideTest(
page,
homePage,
'XY',
{ x: 600, y: 388 } // red plane
// { x: 600, y: 400 }, // red plane // clicks grid helper and that causes problems, should fix so that these coords work too.
)
})
test('YZ', async ({ page, homePage }) => {
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
})
test('-XY', async ({ page, homePage }) => {
await sketchOnPlaneAndBackSideTest(page, homePage, '-XY', {
x: 600,
y: 118,
}) // back of red plane
})
test('-YZ', async ({ page, homePage }) => {
await sketchOnPlaneAndBackSideTest(page, homePage, '-YZ', {
x: 700,
y: 219,
}) // back of green plan
})
test('-XZ', async ({ page, homePage }) => {
await sketchOnPlaneAndBackSideTest(page, homePage, 'XZ', {
x: 700,
y: 427,
}) // back of blue plane
})
await u.clearCommandLogs()
await u.removeCurrentCode()
}
)
test('XY', async ({ page, homePage }) => {
await sketchOnPlaneAndBackSideTest(
page,
homePage,
'XY',
{ x: 600, y: 388 } // red plane
// { x: 600, y: 400 }, // red plane // clicks grid helper and that causes problems, should fix so that these coords work too.
)
})
test('YZ', async ({ page, homePage }) => {
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
})
test('-XY', async ({ page, homePage }) => {
await sketchOnPlaneAndBackSideTest(page, homePage, '-XY', {
x: 600,
y: 118,
}) // back of red plane
})
test('-YZ', async ({ page, homePage }) => {
await sketchOnPlaneAndBackSideTest(page, homePage, '-YZ', {
x: 700,
y: 219,
}) // back of green plan
})
test('-XZ', async ({ page, homePage }) => {
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', { tag: ['@skipWin'] }, () => {
test.describe('Code pane and errors', () => {
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', { tag: ['@skipWin'] }, () => {
test.describe('Command bar tests', () => {
test('Extrude from command bar selects extrude line after', async ({
page,
homePage,
@ -167,54 +167,54 @@ test.describe('Command bar tests', { tag: ['@skipWin'] }, () => {
await expect(commandLevelArgButton).toHaveText('level: project')
})
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()
test('Command bar keybinding works from code editor and can change a setting', async ({
page,
homePage,
}) => {
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
// Put the cursor in the code editor
await page.locator('.cm-content').click()
// Put the cursor in the code editor
await page.locator('.cm-content').click()
// Now try the same, but with the keyboard shortcut, check focus
await page.keyboard.press('ControlOrMeta+K')
// Now try the same, but with the keyboard shortcut, check focus
await page.keyboard.press('ControlOrMeta+K')
let cmdSearchBar = page.getByPlaceholder('Search commands')
await expect(cmdSearchBar).toBeVisible()
await expect(cmdSearchBar).toBeFocused()
let cmdSearchBar = page.getByPlaceholder('Search commands')
await expect(cmdSearchBar).toBeVisible()
await expect(cmdSearchBar).toBeFocused()
// Try typing in the command bar
await cmdSearchBar.fill('theme')
const themeOption = page.getByRole('option', {
name: 'Settings · app · theme',
})
await expect(themeOption).toBeVisible()
await themeOption.click()
const themeInput = page.getByPlaceholder('dark')
await expect(themeInput).toBeVisible()
await expect(themeInput).toBeFocused()
// Select dark theme
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 page.keyboard.press('Enter')
// Try typing in the command bar
await cmdSearchBar.fill('theme')
const themeOption = page.getByRole('option', {
name: 'Settings · app · theme',
})
await expect(themeOption).toBeVisible()
await themeOption.click()
const themeInput = page.getByPlaceholder('dark')
await expect(themeInput).toBeVisible()
await expect(themeInput).toBeFocused()
// Select dark theme
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 page.keyboard.press('Enter')
// Check the toast appeared
await expect(
page.getByText(`Set theme to "system" as a user default`)
).toBeVisible()
// Check that the theme changed
await expect(page.locator('body')).not.toHaveClass(`body-bg dark`)
}
)
// Check the toast appeared
await expect(
page.getByText(`Set theme to "system" as a user default`)
).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', { tag: ['@skipWin'] }, () => {
test.describe('Editor tests', () => {
test('can comment out code with ctrl+/', async ({ page, homePage }) => {
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1000, height: 500 })
@ -966,106 +966,106 @@ test.describe('Editor tests', { tag: ['@skipWin'] }, () => {
|> close()`)
})
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(
'persistCode',
`sketch001 = startSketchOn('XZ')
test('Can undo a sketch modification with ctrl+z', async ({
page,
homePage,
}) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`sketch001 = startSketchOn('XZ')
|> startProfileAt([4.61, -10.01], %)
|> line(end = [12.73, -0.09])
|> tangentialArcTo([24.95, -0.38], %)
|> close()
|> extrude(length = 5)`
)
})
)
})
await page.setBodyDimensions({ width: 1200, height: 500 })
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await homePage.goToModelingScene()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await page.waitForTimeout(100)
await u.openAndClearDebugPanel()
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
vantage: { x: 0, y: -1250, z: 580 },
center: { x: 0, y: 0, z: 0 },
up: { x: 0, y: 0, z: 1 },
},
})
await page.waitForTimeout(100)
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
await page.waitForTimeout(100)
await page.waitForTimeout(100)
await u.openAndClearDebugPanel()
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
vantage: { x: 0, y: -1250, z: 580 },
center: { x: 0, y: 0, z: 0 },
up: { x: 0, y: 0, z: 1 },
},
})
await page.waitForTimeout(100)
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
await page.waitForTimeout(100)
const startPX = [1200 / 2, 500 / 2]
const startPX = [1200 / 2, 500 / 2]
const dragPX = 40
const dragPX = 40
await page.getByText('startProfileAt([4.61, -10.01], %)').click()
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).toBeVisible()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(400)
let prevContent = await page.locator('.cm-content').innerText()
await page.getByText('startProfileAt([4.61, -10.01], %)').click()
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).toBeVisible()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(400)
let prevContent = await page.locator('.cm-content').innerText()
await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
// drag startProfileAt handle
await page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x: startPX[0] + 68, y: startPX[1] + 147 },
targetPosition: { x: startPX[0] + dragPX, y: startPX[1] + dragPX },
})
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
// drag startProfileAt handle
await page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x: startPX[0] + 68, y: startPX[1] + 147 },
targetPosition: { x: startPX[0] + dragPX, y: startPX[1] + dragPX },
})
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
// drag line handle
// we wait so it saves the code
await page.waitForTimeout(800)
// drag line handle
// we wait so it saves the code
await page.waitForTimeout(800)
const lineEnd = await u.getBoundingBox('[data-overlay-index="0"]')
await page.waitForTimeout(100)
await page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x: lineEnd.x - 5, y: lineEnd.y },
targetPosition: { x: lineEnd.x + dragPX, y: lineEnd.y + dragPX },
})
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
const lineEnd = await u.getBoundingBox('[data-overlay-index="0"]')
await page.waitForTimeout(100)
await page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x: lineEnd.x - 5, y: lineEnd.y },
targetPosition: { x: lineEnd.x + dragPX, y: lineEnd.y + dragPX },
})
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
// we wait so it saves the code
await page.waitForTimeout(800)
// we wait so it saves the code
await page.waitForTimeout(800)
// drag tangentialArcTo handle
const tangentEnd = await u.getBoundingBox('[data-overlay-index="1"]')
await page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x: tangentEnd.x + 10, y: tangentEnd.y - 5 },
targetPosition: {
x: tangentEnd.x + dragPX,
y: tangentEnd.y + dragPX,
},
})
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
// drag tangentialArcTo handle
const tangentEnd = await u.getBoundingBox('[data-overlay-index="1"]')
await page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x: tangentEnd.x + 10, y: tangentEnd.y - 5 },
targetPosition: {
x: tangentEnd.x + dragPX,
y: tangentEnd.y + dragPX,
},
})
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
// expect the code to have changed
await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ')
// expect the code to have changed
await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt([2.71, -2.71], %)
|> line(end = [15.4, -2.78])
|> tangentialArcTo([27.6, -3.05], %)
@ -1073,26 +1073,26 @@ test.describe('Editor tests', { tag: ['@skipWin'] }, () => {
|> extrude(length = 5)
`)
// Hit undo
await page.keyboard.down('Control')
await page.keyboard.press('KeyZ')
await page.keyboard.up('Control')
// Hit undo
await page.keyboard.down('Control')
await page.keyboard.press('KeyZ')
await page.keyboard.up('Control')
await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ')
await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt([2.71, -2.71], %)
|> line(end = [15.4, -2.78])
|> tangentialArcTo([24.95, -0.38], %)
|> close()
|> extrude(length = 5)`)
// Hit undo again.
await page.keyboard.down('Control')
await page.keyboard.press('KeyZ')
await page.keyboard.up('Control')
// Hit undo again.
await page.keyboard.down('Control')
await page.keyboard.press('KeyZ')
await page.keyboard.up('Control')
await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ')
await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt([2.71, -2.71], %)
|> line(end = [12.73, -0.09])
|> tangentialArcTo([24.95, -0.38], %)
@ -1100,21 +1100,20 @@ test.describe('Editor tests', { tag: ['@skipWin'] }, () => {
|> extrude(length = 5)
`)
// Hit undo again.
await page.keyboard.down('Control')
await page.keyboard.press('KeyZ')
await page.keyboard.up('Control')
// Hit undo again.
await page.keyboard.down('Control')
await page.keyboard.press('KeyZ')
await page.keyboard.up('Control')
await page.waitForTimeout(100)
await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ')
await page.waitForTimeout(100)
await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt([4.61, -10.01], %)
|> line(end = [12.73, -0.09])
|> 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(726, 272)
const [clickObj] = await scene.makeMouseHelpers(600, 300)
await test.step('setup test', async () => {
await homePage.expectState({
@ -61,7 +61,6 @@ 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({
@ -1186,56 +1185,4 @@ 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,11 +89,18 @@ export class HomePageFixture {
* Maybe there a good sanity check we can do each time?
*/
expectState = async (expectedState: HomePageState) => {
await expect.poll(this._serialiseSortBy).toEqual(expectedState.sortBy)
for (const projectCard of expectedState.projectCards) {
await expect.poll(this._serialiseProjectCards).toContainEqual(projectCard)
}
await expect
.poll(async () => {
const [projectCards, sortBy] = await Promise.all([
this._serialiseProjectCards(),
this._serialiseSortBy(),
])
return {
projectCards,
sortBy,
}
})
.toEqual(expectedState)
}
createAndGoToProject = async (projectTitle = 'project-$nnn') => {

View File

@ -18,7 +18,6 @@ export class ToolbarFixture {
filletButton!: Locator
chamferButton!: Locator
shellButton!: Locator
revolveButton!: Locator
offsetPlaneButton!: Locator
startSketchBtn!: Locator
lineBtn!: Locator
@ -48,7 +47,6 @@ 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')
@ -62,9 +60,7 @@ 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-receive-reliable'
)
this.exeIndicator = page.getByTestId('model-state-indicator-execution-done')
}
get logoLink() {

File diff suppressed because it is too large Load Diff

View File

@ -185,7 +185,7 @@ test(
// error text on hover
await page.hover('.cm-lint-marker-error')
const crypticErrorText = `Expected a tag declarator`
const crypticErrorText = `Expected a tag identifier`
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: 600 })
await page.setBodyDimensions({ width: 1200, height: 500 })
page.on('console', console.log)
@ -1527,32 +1527,34 @@ test(
{ tag: '@electron' },
async ({ context, page, cmdBar, homePage }, testInfo) => {
await context.folderSetupFn(async (dir) => {
await fsp.mkdir(path.join(dir, 'router-template-slate'), {
recursive: true,
})
await fsp.copyFile(
path.join(
'src',
'wasm-lib',
'tests',
'executor',
'inputs',
'router-template-slate.kcl'
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(
path.join(
'src',
'wasm-lib',
'tests',
'executor',
'inputs',
'router-template-slate.kcl'
),
path.join(dir, 'router-template-slate', 'main.kcl')
),
path.join(dir, 'router-template-slate', 'main.kcl')
)
await fsp.mkdir(path.join(dir, 'bracket'), { recursive: true })
await fsp.copyFile(
path.join(
'src',
'wasm-lib',
'tests',
'executor',
'inputs',
'focusrite_scarlett_mounting_braket.kcl'
fsp.copyFile(
path.join(
'src',
'wasm-lib',
'tests',
'executor',
'inputs',
'focusrite_scarlett_mounting_braket.kcl'
),
path.join(dir, 'bracket', 'main.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.fixme('Check the happy path, for basic changing color', () => {
test.describe('Check the happy path, for basic changing color', () => {
const cases = [
{
desc: 'User accepts change',
@ -134,7 +134,7 @@ test.fixme('Check the happy path, for basic changing color', () => {
}
})
test.describe('bad path', { tag: ['@skipWin'] }, () => {
test.describe('bad path', () => {
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', { tag: ['@skipWin'] }, () => {
test.describe('Regression tests', () => {
// bugs we found that don't fit neatly into other categories
test('bad model has inline error #3251', async ({
context,
@ -239,6 +239,12 @@ 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 })
@ -419,6 +425,13 @@ 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(
@ -547,6 +560,8 @@ 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', { tag: ['@skipWin'] }, () => {
test.describe('Sketch tests', () => {
test('multi-sketch file shows multiple Edit Sketch buttons', async ({
page,
context,
@ -312,40 +312,32 @@ test.describe('Sketch tests', { tag: ['@skipWin'] }, () => {
|> line(end = [1.97, 2.06])
|> close()`)
}
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(
'store',
JSON.stringify({
state: {
openPanes: ['code'],
},
version: 0,
})
)
})
await doEditSegmentsByDraggingHandle(page, homePage, ['code'])
}
)
test('code pane open at start-handles', async ({ page, homePage }) => {
// Load the app with the code panes
await page.addInitScript(async () => {
localStorage.setItem(
'store',
JSON.stringify({
state: {
openPanes: ['code'],
},
version: 0,
})
)
})
await doEditSegmentsByDraggingHandle(page, homePage, ['code'])
})
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(
persistModelingContext,
JSON.stringify({ openPanes: [] })
)
}, PERSIST_MODELING_CONTEXT)
await doEditSegmentsByDraggingHandle(page, homePage, [])
}
)
test('code pane closed at start-handles', async ({ page, homePage }) => {
// Load the app with the code panes
await page.addInitScript(async (persistModelingContext) => {
localStorage.setItem(
persistModelingContext,
JSON.stringify({ openPanes: [] })
)
}, PERSIST_MODELING_CONTEXT)
await doEditSegmentsByDraggingHandle(page, homePage, [])
})
})
test('Can edit a circle center and radius by dragging its handles', async ({
@ -644,6 +636,8 @@ test.describe('Sketch tests', { tag: ['@skipWin'] }, () => {
|> 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 }
@ -841,6 +835,8 @@ test.describe('Sketch tests', { tag: ['@skipWin'] }, () => {
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(
@ -890,7 +886,7 @@ test.describe('Sketch tests', { tag: ['@skipWin'] }, () => {
// 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 segment', exact: false })
page.getByRole('button', { name: 'selection : 1 face', exact: false })
).toBeVisible({
timeout: 10_000,
})
@ -1431,7 +1427,7 @@ test.describe('Redirecting to home page and back to the original file should cle
'persistCode',
` sketch001 = startSketchOn('XZ')
|> startProfileAt([256.85, 14.41], %)
|> line(endAbsolute = [0, 211.07])
|> lineTo([0, 211.07], %)
`
)
})

View File

@ -55,6 +55,13 @@ 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: 48 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: 48 KiB

After

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -552,16 +552,6 @@ 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,8 +3,13 @@ import { EngineCommand } from 'lang/std/artifactGraph'
import { uuidv4 } from 'lib/utils'
import { getUtils } from './test-utils'
test.describe('Testing Camera Movement', { tag: ['@skipWin'] }, () => {
test.describe('Testing Camera Movement', () => {
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 })
@ -342,6 +347,8 @@ test.describe('Testing Camera Movement', { tag: ['@skipWin'] }, () => {
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', { tag: ['@skipWin'] }, () => {
test.describe('Testing constraints', () => {
test('Can constrain line length', async ({ page, homePage }) => {
await page.addInitScript(async () => {
localStorage.setItem(
@ -129,6 +129,8 @@ test.describe('Testing constraints', { tag: ['@skipWin'] }, () => {
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',
@ -249,6 +251,8 @@ test.describe('Testing constraints', { tag: ['@skipWin'] }, () => {
}
})
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',
@ -468,6 +472,8 @@ test.describe('Testing constraints', { tag: ['@skipWin'] }, () => {
}
})
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',
@ -658,6 +664,8 @@ test.describe('Testing constraints', { tag: ['@skipWin'] }, () => {
}
})
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',
@ -843,6 +851,8 @@ 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,7 +3,9 @@ import { getUtils } from './test-utils'
import { uuidv4 } from 'lib/utils'
import { TEST_CODE_GIZMO } from './storageStates'
test.describe('Testing Gizmo', { tag: ['@skipWin'] }, () => {
test.describe('Testing Gizmo', () => {
// TODO: fix this test on windows after the electron migration
test.skip(process.platform === 'win32', 'Skip on windows')
const cases = [
{
testDescription: 'top view',

View File

@ -69,6 +69,7 @@ test.describe('Testing in-app sample loading', () => {
await confirmButton.click()
await editor.expectEditor.toContain('// ' + newSample.title)
await expect(unitsToast('in')).toBeVisible()
})
})
@ -157,6 +158,7 @@ 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 () => {
@ -186,6 +188,7 @@ 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()
})
}
)

File diff suppressed because it is too large Load Diff

View File

@ -5,248 +5,256 @@ import { Coords2d } from 'lang/std/sketch'
import { KCL_DEFAULT_LENGTH } from 'lib/constants'
import { uuidv4 } from 'lib/utils'
test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
test.describe('Testing selections', () => {
test.setTimeout(90_000)
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
const u = await getUtils(page)
const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setBodyDimensions({ width: 1200, height: 500 })
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')
await homePage.goToModelingScene()
await u.openDebugPanel()
// 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
const u = await getUtils(page)
const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setBodyDimensions({ width: 1200, height: 500 })
const yAxisClick = () =>
test.step('Click on Y axis', async () => {
await page.mouse.move(600, 200, { steps: 5 })
await page.mouse.click(600, 200)
await page.waitForTimeout(100)
})
const xAxisClickAfterExitingSketch = () =>
test.step(`Click on X axis after exiting sketch, which shifts it at the moment`, async () => {
await page.mouse.click(639, 278)
await page.waitForTimeout(100)
})
const emptySpaceHover = () =>
test.step('Hover over empty space', async () => {
await page.mouse.move(700, 143, { steps: 5 })
await expect(page.locator('.hover-highlight')).not.toBeVisible()
})
const emptySpaceClick = () =>
test.step(`Click in empty space`, async () => {
await page.mouse.click(700, 143)
await expect(page.locator('.cm-line').last()).toHaveClass(
/cm-activeLine/
)
})
const topHorzSegmentClick = () =>
page.mouse
.click(startXPx, 500 - PUR * 20)
.then(() => page.waitForTimeout(100))
const bottomHorzSegmentClick = () =>
page.mouse
.click(startXPx + PUR * 10, 500 - PUR * 10)
.then(() => page.waitForTimeout(100))
await homePage.goToModelingScene()
await u.openDebugPanel()
await u.clearCommandLogs()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await page.getByRole('button', { name: 'Start Sketch' }).click()
const yAxisClick = () =>
test.step('Click on Y axis', async () => {
await page.mouse.move(600, 200, { steps: 5 })
await page.mouse.click(600, 200)
await page.waitForTimeout(100)
})
const xAxisClickAfterExitingSketch = () =>
test.step(`Click on X axis after exiting sketch, which shifts it at the moment`, async () => {
await page.mouse.click(639, 278)
await page.waitForTimeout(100)
})
const emptySpaceHover = () =>
test.step('Hover over empty space', async () => {
await page.mouse.move(700, 143, { steps: 5 })
await expect(page.locator('.hover-highlight')).not.toBeVisible()
})
const emptySpaceClick = () =>
test.step(`Click in empty space`, async () => {
await page.mouse.click(700, 143)
await expect(page.locator('.cm-line').last()).toHaveClass(
/cm-activeLine/
)
})
const topHorzSegmentClick = () =>
page.mouse
.click(startXPx, 500 - PUR * 20)
.then(() => page.waitForTimeout(100))
const bottomHorzSegmentClick = () =>
page.mouse
.click(startXPx + PUR * 10, 500 - PUR * 10)
.then(() => page.waitForTimeout(100))
// select a plane
await page.mouse.click(700, 200)
await page.waitForTimeout(700) // wait for animation
await u.clearCommandLogs()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await page.getByRole('button', { name: 'Start Sketch' }).click()
const startXPx = 600
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ')
// select a plane
await page.mouse.click(700, 200)
await page.waitForTimeout(700) // wait for animation
const startXPx = 600
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %)`)
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ')
await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %)
|> xLine(${commonPoints.num1}, %)`)
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ')
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %)
|> xLine(${commonPoints.num1}, %)
|> yLine(${commonPoints.num1 + 0.01}, %)`)
await page.waitForTimeout(100)
await page.mouse.click(startXPx, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ')
await page.waitForTimeout(100)
await page.mouse.click(startXPx, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %)
|> xLine(${commonPoints.num1}, %)
|> yLine(${commonPoints.num1 + 0.01}, %)
|> xLine(${commonPoints.num2 * -1}, %)`)
// deselect line tool
await page.getByRole('button', { name: 'line Line', exact: true }).click()
// deselect line tool
await page.getByRole('button', { name: 'line Line', exact: true }).click()
await u.closeDebugPanel()
const selectionSequence = async () => {
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
await u.closeDebugPanel()
const selectionSequence = async () => {
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
await page.waitForTimeout(100)
await page.mouse.move(startXPx + PUR * 15, 500 - PUR * 10)
await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
// bg-yellow-300/70 is more brittle than hover-highlight, but is closer to the user experience
// and will be an easy fix if it breaks because we change the colour
await expect(page.locator('.bg-yellow-300\\/70')).toBeVisible()
// check mousing off, than mousing onto another line
await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 15) // mouse off
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 20) // mouse onto another line
await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
// now check clicking works including axis
// click a segment hold shift and click an axis, see that a relevant constraint is enabled
const constrainButton = page.getByRole('button', {
name: 'Length: open menu',
})
const absXButton = page.getByRole('button', { name: 'Absolute X' })
await test.step(`Select a segment and an axis, see that a relevant constraint is enabled`, async () => {
await topHorzSegmentClick()
await page.keyboard.down('Shift')
await constrainButton.click()
await expect(absXButton).toBeDisabled()
await page.waitForTimeout(100)
await yAxisClick()
await page.keyboard.up('Shift')
await constrainButton.click()
await absXButton.and(page.locator(':not([disabled])')).waitFor()
await expect(absXButton).not.toBeDisabled()
})
await page.mouse.move(startXPx + PUR * 15, 500 - PUR * 10)
await emptySpaceClick()
await page.waitForTimeout(100)
await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
// bg-yellow-300/70 is more brittle than hover-highlight, but is closer to the user experience
// and will be an easy fix if it breaks because we change the colour
await expect(page.locator('.bg-yellow-300\\/70')).toBeVisible()
// check mousing off, than mousing onto another line
await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 15) // mouse off
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 20) // mouse onto another line
await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
await test.step(`Same selection but click the axis first`, async () => {
await yAxisClick()
await constrainButton.click()
await expect(absXButton).toBeDisabled()
await page.keyboard.down('Shift')
await page.waitForTimeout(100)
await topHorzSegmentClick()
// now check clicking works including axis
// click a segment hold shift and click an axis, see that a relevant constraint is enabled
const constrainButton = page.getByRole('button', {
name: 'Length: open menu',
})
const absXButton = page.getByRole('button', { name: 'Absolute X' })
await test.step(`Select a segment and an axis, see that a relevant constraint is enabled`, async () => {
await topHorzSegmentClick()
await page.keyboard.down('Shift')
await constrainButton.click()
await expect(absXButton).toBeDisabled()
await page.waitForTimeout(100)
await yAxisClick()
await page.keyboard.up('Shift')
await constrainButton.click()
await absXButton.and(page.locator(':not([disabled])')).waitFor()
await expect(absXButton).not.toBeDisabled()
})
await emptySpaceClick()
await page.waitForTimeout(100)
await page.keyboard.up('Shift')
await constrainButton.click()
await expect(absXButton).not.toBeDisabled()
})
await test.step(`Same selection but click the axis first`, async () => {
await yAxisClick()
await constrainButton.click()
await expect(absXButton).toBeDisabled()
await page.keyboard.down('Shift')
await page.waitForTimeout(100)
await topHorzSegmentClick()
await page.waitForTimeout(100)
// clear selection by clicking on nothing
await emptySpaceClick()
await page.keyboard.up('Shift')
await constrainButton.click()
await expect(absXButton).not.toBeDisabled()
})
// 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 () => {
// clear selection by clicking on nothing
await emptySpaceClick()
// 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.keyboard.down('Shift')
await constrainButton.click()
await expect(absXButton).toBeDisabled()
await page.waitForTimeout(100)
await yAxisClick()
await page.keyboard.up('Shift')
await constrainButton.click()
await expect(absXButton).not.toBeDisabled()
})
// clear selection by clicking on nothing
await emptySpaceClick()
// select segment in editor than another segment in scene and check there are two cursors
// TODO change this back to shift click in the scene, not cmd click in the editor
await bottomHorzSegmentClick()
await expect(page.locator('.cm-cursor')).toHaveCount(1)
await page.keyboard.down(
process.platform === 'linux' ? 'Control' : 'Meta'
)
await page.waitForTimeout(100)
await page.getByText(` |> xLine(${commonPoints.num2 * -1}, %)`).click()
await page.keyboard.down('Shift')
await constrainButton.click()
await expect(absXButton).toBeDisabled()
await page.waitForTimeout(100)
await yAxisClick()
await page.keyboard.up('Shift')
await constrainButton.click()
await expect(absXButton).not.toBeDisabled()
await expect(page.locator('.cm-cursor')).toHaveCount(2)
await page.waitForTimeout(500)
await page.keyboard.up(
process.platform === 'linux' ? 'Control' : 'Meta'
)
// clear selection by clicking on nothing
await emptySpaceClick()
}
await test.step(`Test hovering and selecting on fresh sketch`, async () => {
await selectionSequence()
})
// clear selection by clicking on nothing
await emptySpaceClick()
// hovering in fresh sketch worked, lets try exiting and re-entering
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await page.waitForTimeout(200)
// wait for execution done
// select segment in editor than another segment in scene and check there are two cursors
// TODO change this back to shift click in the scene, not cmd click in the editor
await bottomHorzSegmentClick()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
await expect(page.locator('.cm-cursor')).toHaveCount(1)
await page.keyboard.down(
process.platform === 'linux' ? 'Control' : 'Meta'
)
// select a line, this verifies that sketches in the scene can be selected outside of sketch mode
await topHorzSegmentClick()
await xAxisClickAfterExitingSketch()
await page.waitForTimeout(100)
await page.getByText(` |> xLine(${commonPoints.num2 * -1}, %)`).click()
await emptySpaceHover()
await expect(page.locator('.cm-cursor')).toHaveCount(2)
await page.waitForTimeout(500)
await page.keyboard.up(process.platform === 'linux' ? 'Control' : 'Meta')
// enter sketch again
await u.doAndWaitForCmd(
() => page.getByRole('button', { name: 'Edit Sketch' }).click(),
'default_camera_get_settings'
)
await page.waitForTimeout(450) // wait for animation
await u.openAndClearDebugPanel()
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center: { x: 0, y: 0, z: 0 },
vantage: { x: 0, y: -1378.01, z: 0 },
up: { x: 0, y: 0, z: 1 },
},
})
await page.waitForTimeout(100)
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
await page.waitForTimeout(100)
// clear selection by clicking on nothing
await emptySpaceClick()
await u.closeDebugPanel()
await test.step(`Test hovering and selecting on edited sketch`, async () => {
await selectionSequence()
})
}
await test.step(`Test hovering and selecting on fresh sketch`, async () => {
await selectionSequence()
})
// hovering in fresh sketch worked, lets try exiting and re-entering
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await page.waitForTimeout(200)
// wait for execution done
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// select a line, this verifies that sketches in the scene can be selected outside of sketch mode
await topHorzSegmentClick()
await xAxisClickAfterExitingSketch()
await page.waitForTimeout(100)
await emptySpaceHover()
// enter sketch again
await u.doAndWaitForCmd(
() => page.getByRole('button', { name: 'Edit Sketch' }).click(),
'default_camera_get_settings'
)
await page.waitForTimeout(450) // wait for animation
await u.openAndClearDebugPanel()
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center: { x: 0, y: 0, z: 0 },
vantage: { x: 0, y: -1378.01, z: 0 },
up: { x: 0, y: 0, z: 1 },
},
})
await page.waitForTimeout(100)
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
await page.waitForTimeout(100)
await emptySpaceClick()
await u.closeDebugPanel()
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)
@ -484,6 +492,8 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
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,6 +435,11 @@ 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,6 +75,3 @@ publish:
channel: latest
releaseInfo:
releaseNotesFile: release-notes.md
protocols:
- name: Zoo Studio
schemes: ['zoo-studio']

View File

@ -9,8 +9,23 @@ 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,7 +32,6 @@ 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,
@ -66,7 +65,6 @@ 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'",
"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",
"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",
"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 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: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: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.1",
"vitest": "^1.6.0",
"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.9"
"vitest": "^2.1.8"
},
"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.9":
version "2.1.9"
resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-2.1.9.tgz#b566ea20d58ea6578d8dc37040d6c1a47ebe5ff8"
integrity sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==
"@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==
dependencies:
"@vitest/spy" "2.1.9"
"@vitest/utils" "2.1.9"
"@vitest/spy" "2.1.8"
"@vitest/utils" "2.1.8"
chai "^5.1.2"
tinyrainbow "^1.2.0"
"@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==
"@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==
dependencies:
"@vitest/spy" "2.1.9"
"@vitest/spy" "2.1.8"
estree-walker "^3.0.3"
magic-string "^0.30.12"
"@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==
"@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==
dependencies:
tinyrainbow "^1.2.0"
"@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==
"@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==
dependencies:
"@vitest/utils" "2.1.9"
"@vitest/utils" "2.1.8"
pathe "^1.1.2"
"@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==
"@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==
dependencies:
"@vitest/pretty-format" "2.1.9"
"@vitest/pretty-format" "2.1.8"
magic-string "^0.30.12"
pathe "^1.1.2"
"@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==
"@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==
dependencies:
tinyspy "^3.0.2"
"@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==
"@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==
dependencies:
"@vitest/pretty-format" "2.1.9"
"@vitest/pretty-format" "2.1.8"
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.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==
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==
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.9:
version "2.1.9"
resolved "https://registry.yarnpkg.com/vitest/-/vitest-2.1.9.tgz#7d01ffd07a553a51c87170b5e80fea3da7fb41e7"
integrity sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==
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==
dependencies:
"@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"
"@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"
chai "^5.1.2"
debug "^4.3.7"
expect-type "^1.1.0"
@ -716,7 +716,7 @@ vitest@^2.1.9:
tinypool "^1.0.1"
tinyrainbow "^1.2.0"
vite "^5.0.0"
vite-node "2.1.9"
vite-node "2.1.8"
why-is-node-running "^2.3.0"
w3c-keyname@^2.2.4:

View File

@ -1,7 +0,0 @@
/**
* 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,7 +2,6 @@ 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) {
@ -46,7 +45,7 @@ export function offsetToPos(doc: Text, offset: number) {
export function formatMarkdownContents(
contents: LSP.MarkupContent | LSP.MarkedString | LSP.MarkedString[]
): string {
if (isArray(contents)) {
if (Array.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, useState } from 'react'
import { useEffect, useMemo, useRef } from 'react'
import { useHotKeyListener } from './hooks/useHotKeyListener'
import { Stream } from './components/Stream'
import { AppHeader } from './components/AppHeader'
@ -24,12 +24,7 @@ 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(() => {})
@ -59,20 +54,14 @@ 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 { settings } = useSettingsAuthContext()
const token = useToken()
const { auth, settings } = useSettingsAuthContext()
const token = auth?.context?.token
const coreDumpManager = useMemo(
() => new CoreDumpManager(engineCommandManager, codeManager, token),
@ -102,32 +91,6 @@ 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 authState = useAuthState()
const isLoggingIn = authState.matches('checkIfLoggedIn')
const { auth } = useSettingsAuthContext()
const isLoggingIn = auth?.state.matches('checkIfLoggedIn')
return isLoggingIn ? (
<Loading>

View File

@ -37,6 +37,7 @@ 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'
@ -46,7 +47,6 @@ 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,7 +203,8 @@ export const Router = () => {
}
function CoreDump() {
const token = useToken()
const { auth } = useSettingsAuthContext()
const token = auth?.context?.token
const coreDumpManager = useMemo(
() => new CoreDumpManager(engineCommandManager, codeManager, token),
[]

View File

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

View File

@ -29,7 +29,6 @@ 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
@ -407,7 +406,7 @@ export class CameraControls {
.sub(this.mouseDownPosition)
this.mouseDownPosition.copy(this.mouseNewPosition)
let interaction = this.getInteractionType(event)
const interaction = this.getInteractionType(event)
if (interaction === 'none') return
// If there's a valid interaction and the mouse is moving,
@ -754,6 +753,8 @@ export class CameraControls {
didChange = true
}
this.safeLookAtTarget(this.camera.up)
// Update the camera's matrices
this.camera.updateMatrixWorld()
if (didChange || forceUpdate) {
@ -1188,24 +1189,14 @@ export class CameraControls {
this.deferReactUpdate(this.reactCameraProperties)
Object.values(this._camChangeCallbacks).forEach((cb) => cb())
}
getInteractionType = (
event: MouseEvent
): CameraDragInteractionType_type | 'none' => {
const initialInteractionType = _getInteractionType(
getInteractionType = (event: MouseEvent) =>
_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,7 +24,8 @@ export const AppHeader = ({
style,
enableMenu = false,
}: AppHeaderProps) => {
const user = useUser()
const { auth } = useSettingsAuthContext()
const user = auth?.context?.user
return (
<header

View File

@ -7,7 +7,6 @@ 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()
@ -167,12 +166,12 @@ function DisplayObj({
{Object.entries(obj).map(([key, value]) => {
if (filterKeys.includes(key)) {
return null
} else if (isArray(value)) {
} else if (Array.isArray(value)) {
return (
<li key={key}>
{`${key}: [`}
<DisplayBody
body={value as any}
body={value}
filterKeys={filterKeys}
node={node}
/>

View File

@ -30,7 +30,6 @@ 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>
@ -48,8 +47,7 @@ export const FileMachineProvider = ({
children: React.ReactNode
}) => {
const navigate = useNavigate()
const { settings } = useSettingsAuthContext()
const token = useToken()
const { settings, auth } = useSettingsAuthContext()
const projectData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const { project, file } = projectData
const [kclSamples, setKclSamples] = React.useState<KclSamplesManifestItem[]>(
@ -124,43 +122,22 @@ export const FileMachineProvider = ({
let createdName = input.name.trim() || DEFAULT_FILE_NAME
let createdPath: string
if (
(input.targetPathToClone &&
(await window.electron.statIsDirectory(
input.targetPathToClone
))) ||
input.makeDir
) {
if (input.makeDir) {
let { name, path } = getNextDirName({
entryName: input.targetPathToClone
? window.electron.path.basename(input.targetPathToClone)
: createdName,
baseDir: input.targetPathToClone
? window.electron.path.dirname(input.targetPathToClone)
: input.selectedDirectory.path,
entryName: createdName,
baseDir: input.selectedDirectory.path,
})
createdName = name
createdPath = path
await window.electron.mkdir(createdPath)
} else {
const { name, path } = getNextFileName({
entryName: input.targetPathToClone
? window.electron.path.basename(input.targetPathToClone)
: createdName,
baseDir: input.targetPathToClone
? window.electron.path.dirname(input.targetPathToClone)
: input.selectedDirectory.path,
entryName: createdName,
baseDir: 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 ?? '')
}
await window.electron.writeFile(createdPath, input.content ?? '')
}
return {
@ -320,7 +297,7 @@ export const FileMachineProvider = ({
const kclCommandMemo = useMemo(
() =>
kclCommands({
authToken: token ?? '',
authToken: auth?.context?.token ?? '',
projectData,
settings: {
defaultUnit: settings?.context?.modeling.defaultUnit.current ?? 'mm',

View File

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

View File

@ -27,7 +27,6 @@ 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 []
@ -70,7 +69,8 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const [isKclLspReady, setIsKclLspReady] = useState(false)
const [isCopilotLspReady, setIsCopilotLspReady] = useState(false)
const token = useToken()
const { auth } = useSettingsAuthContext()
const token = auth?.context.token
const navigate = useNavigate()
// So this is a bit weird, we need to initialize the lsp server and client.

View File

@ -1,8 +1,10 @@
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,7 +89,6 @@ 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>
@ -111,6 +110,7 @@ export const ModelingMachineProvider = ({
children: React.ReactNode
}) => {
const {
auth,
settings: {
context: {
app: { theme, enableSSAO, allowOrbitInSketchMode },
@ -119,7 +119,6 @@ export const ModelingMachineProvider = ({
cameraProjection,
highlightEdges,
showScaleGrid,
cameraOrbit,
},
},
},
@ -128,7 +127,7 @@ export const ModelingMachineProvider = ({
const navigate = useNavigate()
const { context, send: fileMachineSend } = useFileContext()
const { file } = useLoaderData() as IndexLoaderData
const token = useToken()
const token = auth?.context?.token
const streamRef = useRef<HTMLDivElement>(null)
const persistedContext = useMemo(() => getPersistedContext(), [])
@ -1155,7 +1154,6 @@ export const ModelingMachineProvider = ({
enableSSAO: enableSSAO.current,
showScaleGrid: showScaleGrid.current,
cameraProjection: cameraProjection.current,
cameraOrbit: cameraOrbit.current,
},
token
)
@ -1185,13 +1183,6 @@ 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,7 +14,6 @@ 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 = () => {
@ -87,18 +86,6 @@ 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,
@ -116,7 +103,7 @@ export function useCodeMirror(props: UseCodeMirror) {
const isFirstRender = useFirstRender()
const targetExtensions = useMemo(() => {
let exts = isExtensionArray(extensions) ? extensions : []
let exts = Array.isArray(extensions) ? extensions : []
if (theme === 'dark') {
exts = [...exts, oneDark]
} else if (theme === 'light') {

View File

@ -122,8 +122,7 @@ export const sidebarPanes: SidebarPane[] = [
icon: 'folder',
sidebarName: 'Project Files',
Content: (props: { id: SidebarType; onClose: () => void }) => {
const { createFile, createFolder, cloneFileOrDir, newTreeEntry } =
useFileTreeOperations()
const { createFile, createFolder, newTreeEntry } = useFileTreeOperations()
return (
<>
@ -144,7 +143,6 @@ 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, PROJECT_IMAGE_NAME } from 'lib/constants'
import { FILE_EXT } 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,21 +53,18 @@ function ProjectCard({
setNumberOfFolders(project.directory_count)
}
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)
}
}
// 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)
// }
// }
void getNumberOfFiles()
void setupImageUrl()
// void setupImageUrl()
}, [project.kcl_file_count, project.directory_count])
useEffect(() => {
@ -87,7 +84,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}
@ -95,7 +92,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,8 +19,7 @@ import { commandBarActor } from 'machines/commandBarMachine'
import { useSelector } from '@xstate/react'
import { copyFileShareLink } from 'lib/links'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { IS_NIGHTLY_OR_DEBUG } from 'routes/Settings'
import { useToken } from 'machines/appMachine'
import { DEV } from 'env'
const ProjectSidebarMenu = ({
project,
@ -104,15 +103,13 @@ function ProjectMenuPopover({
const location = useLocation()
const navigate = useNavigate()
const filePath = useAbsoluteFilePath()
const { settings } = useSettingsAuthContext()
const token = useToken()
const { settings, auth } = useSettingsAuthContext()
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)
@ -194,10 +191,10 @@ function ProjectMenuPopover({
id: 'share-link',
Element: 'button',
children: 'Share link to file',
disabled: IS_NIGHTLY_OR_DEBUG || !findCommand(shareCommandInfo),
disabled: !DEV,
onClick: async () => {
await copyFileShareLink({
token: token ?? '',
token: auth?.context.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 token = useToken()
const { auth } = useSettingsAuthContext()
const token = auth?.context?.token
const coreDumpManager = useMemo(
() => new CoreDumpManager(engineCommandManager, codeManager, token),
[]

View File

@ -2,12 +2,10 @@ 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,7 +2,10 @@ 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 {
@ -13,6 +16,7 @@ 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,
@ -46,6 +50,7 @@ type MachineContext<T extends AnyStateMachine> = {
}
type SettingsAuthContextType = {
auth: MachineContext<typeof authMachine>
settings: MachineContext<typeof settingsMachine>
}
@ -365,9 +370,40 @@ 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,
@ -381,3 +417,12 @@ 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 = authActor.send
const send = useSettingsAuthContext()?.auth?.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 dark:bg-blue-800/50',
class: 'bg-yellow-300/70',
attributes: { 'data-testid': 'hover-highlight' },
})
const matchDeco2 = Decoration.mark({
class: 'bg-yellow-200/40 dark:bg-blue-700/50',
class: 'bg-yellow-200/40',
attributes: { 'data-testid': 'hover-highlight' },
})

View File

@ -9,7 +9,6 @@ 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
@ -80,7 +79,7 @@ function discoverColorsInKCL(
)
if (maybeWidgetOptions) {
if (isArray(maybeWidgetOptions)) {
if (Array.isArray(maybeWidgetOptions)) {
console.error('Unexpected nested overlays')
ret.push(...maybeWidgetOptions)
} else {
@ -151,7 +150,7 @@ function colorPickersDecorations(
return
}
if (!isArray(maybeWidgetOptions)) {
if (!Array.isArray(maybeWidgetOptions)) {
widgets.push(
Decoration.widget({
widget: new ColorPickerWidget(maybeWidgetOptions),

View File

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

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

View File

@ -57,7 +57,6 @@ 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>,
@ -1468,8 +1467,6 @@ 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,19 +21,13 @@ 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 {
codeManager,
editorManager,
engineCommandManager,
kclManager,
} from 'lib/singletons'
import { engineCommandManager, kclManager } from 'lib/singletons'
import { VITE_KC_DEV_TOKEN } from 'env'
import { isOverlap } from 'lib/utils'
import { codeRefFromRange } from 'lang/std/artifactGraph'
@ -61,13 +55,6 @@ afterAll(() => {
engineCommandManager.tearDown()
})
const dependencies = {
kclManager,
engineCommandManager,
editorManager,
codeManager,
}
const runGetPathToExtrudeForSegmentSelectionTest = async (
code: string,
selectedSegmentSnippet: string,
@ -146,8 +133,7 @@ const runGetPathToExtrudeForSegmentSelectionTest = async (
const pathResult = getPathToExtrudeForSegmentSelection(
ast,
selection,
artifactGraph,
dependencies
artifactGraph
)
if (err(pathResult)) return pathResult
const { pathToExtrudeNode } = pathResult
@ -304,13 +290,8 @@ const runModifyAstCloneWithEdgeTreatmentAndTag = async (
otherSelections: [],
}
// apply edge treatment to selection
const result = modifyAstWithEdgeTreatmentAndTag(
ast,
selection,
parameters,
dependencies
)
// apply edge treatment to seleciton
const result = modifyAstWithEdgeTreatmentAndTag(ast, selection, parameters)
if (err(result)) {
return result
}
@ -320,46 +301,6 @@ 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: {
@ -636,191 +577,6 @@ 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,7 +6,6 @@ import {
Identifier,
ObjectExpression,
PathToNode,
PipeExpression,
Program,
VariableDeclaration,
VariableDeclarator,
@ -36,14 +35,15 @@ 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,38 +65,21 @@ export type EdgeTreatmentParameters = ChamferParameters | FilletParameters
export async function applyEdgeTreatmentToSelection(
ast: Node<Program>,
selection: Selections,
parameters: EdgeTreatmentParameters,
dependencies: {
kclManager: KclManager
engineCommandManager: EngineCommandManager
editorManager: EditorManager
codeManager: CodeManager
}
parameters: EdgeTreatmentParameters
): Promise<void | Error> {
// 1. clone and modify with edge treatment and tag
const result = modifyAstWithEdgeTreatmentAndTag(
ast,
selection,
parameters,
dependencies
)
const result = modifyAstWithEdgeTreatmentAndTag(ast, selection, parameters)
if (err(result)) return result
const { modifiedAst, pathToEdgeTreatmentNode } = result
// 2. update ast
await updateAstAndFocus(modifiedAst, pathToEdgeTreatmentNode, dependencies)
await updateAstAndFocus(modifiedAst, pathToEdgeTreatmentNode)
}
export function modifyAstWithEdgeTreatmentAndTag(
ast: Node<Program>,
selections: Selections,
parameters: EdgeTreatmentParameters,
dependencies: {
kclManager: KclManager
engineCommandManager: EngineCommandManager
editorManager: EditorManager
codeManager: CodeManager
}
parameters: EdgeTreatmentParameters
):
| { modifiedAst: Node<Program>; pathToEdgeTreatmentNode: Array<PathToNode> }
| Error {
@ -106,7 +89,7 @@ export function modifyAstWithEdgeTreatmentAndTag(
const astResult = insertParametersIntoAst(clonedAst, parameters)
if (err(astResult)) return astResult
const artifactGraph = dependencies.engineCommandManager.artifactGraph
const artifactGraph = engineCommandManager.artifactGraph
// Step 1: modify ast with tags and group them by extrude nodes (bodies)
const extrudeToTagsMap: Map<
@ -119,8 +102,7 @@ export function modifyAstWithEdgeTreatmentAndTag(
const result = getPathToExtrudeForSegmentSelection(
clonedAstForGetExtrude,
selection,
artifactGraph,
dependencies
artifactGraph
)
if (err(result)) return result
const { pathToSegmentNode, pathToExtrudeNode } = result
@ -276,13 +258,7 @@ function insertParametersIntoAst(
export function getPathToExtrudeForSegmentSelection(
ast: Program,
selection: Selection,
artifactGraph: ArtifactGraph,
dependencies: {
kclManager: KclManager
engineCommandManager: EngineCommandManager
editorManager: EditorManager
codeManager: CodeManager
}
artifactGraph: ArtifactGraph
): { pathToSegmentNode: PathToNode; pathToExtrudeNode: PathToNode } | Error {
const pathToSegmentNode = getNodePathFromSourceRange(
ast,
@ -298,7 +274,7 @@ export function getPathToExtrudeForSegmentSelection(
const sketchVar = varDecNode.node.declaration.id.name
const sketch = sketchFromKclValue(
dependencies.kclManager.programMemory.get(sketchVar),
kclManager.programMemory.get(sketchVar),
sketchVar
)
if (trap(sketch)) return sketch
@ -317,28 +293,16 @@ export function getPathToExtrudeForSegmentSelection(
async function updateAstAndFocus(
modifiedAst: Node<Program>,
pathToEdgeTreatmentNode: Array<PathToNode>,
dependencies: {
kclManager: KclManager
engineCommandManager: EngineCommandManager
editorManager: EditorManager
codeManager: CodeManager
}
pathToEdgeTreatmentNode: Array<PathToNode>
): Promise<void> {
const updatedAst = await dependencies.kclManager.updateAst(
modifiedAst,
true,
{
focusPath: pathToEdgeTreatmentNode,
}
)
const updatedAst = await kclManager.updateAst(modifiedAst, true, {
focusPath: pathToEdgeTreatmentNode,
})
await dependencies.codeManager.updateEditorWithAstAndWriteToFile(
updatedAst.newAst
)
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
if (updatedAst?.selections) {
dependencies.editorManager.selectRange(updatedAst?.selections)
editorManager.selectRange(updatedAst?.selections)
}
}
@ -818,142 +782,3 @@ 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,28 +19,17 @@ 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)
@ -53,8 +42,7 @@ export function addShell({
const extrudeLookupResult = getPathToExtrudeForSegmentSelection(
clonedAstForGetExtrude,
graphSelection,
artifactGraph,
dependencies
artifactGraph
)
if (err(extrudeLookupResult)) {
return new Error("Couldn't find extrude")

View File

@ -5,11 +5,7 @@ import {
PathToNode,
Identifier,
topLevelRange,
PipeExpression,
CallExpression,
VariableDeclarator,
} from './wasm'
import { ProgramMemory } from 'lang/wasm'
import {
findAllPreviousVariables,
isNodeSafeToReplace,
@ -29,11 +25,9 @@ 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
@ -686,115 +680,3 @@ 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,12 +22,11 @@ import {
topLevelRange,
VariableDeclaration,
VariableDeclarator,
recast,
} from './wasm'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { createIdentifier, splitPathAtLastIndex } from './modifyAst'
import { getSketchSegmentFromSourceRange } from './std/sketchConstraints'
import { getAngle, isArray } from '../lib/utils'
import { getAngle } from '../lib/utils'
import { ARG_TAG, getArgForEnd, getFirstArg } from './std/sketch'
import {
getConstraintLevelFromSourceRange,
@ -80,28 +79,7 @@ export function getNodeFromPath<T>(
deepPath: successfulPaths,
}
}
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
return new Error('not an object')
}
parent = currentNode
parentEdge = pathItem[0]
@ -112,7 +90,7 @@ export function getNodeFromPath<T>(
}
if (
typeof stopAt !== 'undefined' &&
(isArray(stopAt)
(Array.isArray(stopAt)
? stopAt.includes(currentNode.type)
: currentNode.type === stopAt)
) {
@ -167,7 +145,6 @@ export function getNodeFromPathCurry(
type KCLNode = Node<
| Expr
| ExpressionStatement
| ImportStatement
| VariableDeclaration
| VariableDeclarator
| ReturnStatement
@ -264,14 +241,10 @@ export function traverse(
// hmm this smell
_traverse(_node.object, [...pathToNode, ['object', 'MemberExpression']])
_traverse(_node.property, [...pathToNode, ['property', 'MemberExpression']])
} 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) => {
} else if ('body' in _node && Array.isArray(_node.body)) {
_node.body.forEach((expression, index) =>
_traverse(expression, [...pathToNode, ['body', ''], [index, 'index']])
})
)
}
option?.leave?.(_node)
}

View File

@ -248,8 +248,6 @@ class EngineConnection extends EventTarget {
mediaStream?: MediaStream
idleMode: boolean = false
promise?: Promise<void>
sdpAnswer?: Models['RtcSessionDescription_type']
triggeredStart = false
onIceCandidate = function (
this: RTCPeerConnection,
@ -555,7 +553,6 @@ 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
@ -586,38 +583,8 @@ 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
}
@ -628,6 +595,7 @@ class EngineConnection extends EventTarget {
},
}
// Request a candidate to use
this.send({
type: 'trickle_ice',
candidate: {
@ -637,38 +605,8 @@ 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
@ -696,8 +634,6 @@ class EngineConnection extends EventTarget {
})
)
break
case 'connecting':
break
case 'disconnected':
case 'failed':
this.pc?.removeEventListener('icecandidate', this.onIceCandidate)
@ -1190,8 +1126,25 @@ class EngineConnection extends EventTarget {
},
}
this.sdpAnswer = answer
// 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.state = {
type: EngineConnectionStateType.Connecting,
value: {
type: ConnectingType.SetRemoteDescription,
},
}
this.state = {
type: EngineConnectionStateType.Connecting,
value: {
type: ConnectingType.WebRTCConnecting,
},
}
break
case 'trickle_ice':
@ -1282,7 +1235,6 @@ 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
}
}
}
@ -1437,7 +1389,6 @@ export class EngineCommandManager extends EventTarget {
enableSSAO: true,
showScaleGrid: false,
cameraProjection: 'perspective',
cameraOrbit: 'spherical',
}
}
@ -1486,7 +1437,6 @@ 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.
@ -2049,7 +1999,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 e
return null
})
}
/**

View File

@ -60,7 +60,7 @@ import {
mutateObjExpProp,
findUniqueName,
} from 'lang/modifyAst'
import { roundOff, getLength, getAngle, isArray } from 'lib/utils'
import { roundOff, getLength, getAngle } 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 (isArray(val)) {
if (Array.isArray(val)) {
if (
[
'angledLine',

View File

@ -57,7 +57,7 @@ import {
getSketchSegmentFromPathToNode,
getSketchSegmentFromSourceRange,
} from './sketchConstraints'
import { getAngle, roundOff, normaliseAngle, isArray } from '../../lib/utils'
import { getAngle, roundOff, normaliseAngle } 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 (isArray(val)) {
if (Array.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 =
isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
if (isTwoValFree) {
return false
}
const isOneValFree =
!isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
!Array.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 = isArray(val)
const isArr = Array.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 =
isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
const isOneValFree =
!isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
!Array.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 (isArray(val)) {
if (Array.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 (isArray(val)) {
if (Array.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 { isArray, isOverlap } from 'lib/utils'
import { isOverlap } from 'lib/utils'
export function updatePathToNodeFromMap(
oldPath: PathToNode,
@ -40,8 +40,8 @@ export function isCursorInSketchCommandRange(
predicate: (artifact) => {
return selectionRanges.graphSelections.some(
(selection) =>
isArray(selection?.codeRef?.range) &&
isArray(artifact?.codeRef?.range) &&
Array.isArray(selection?.codeRef?.range) &&
Array.isArray(artifact?.codeRef?.range) &&
isOverlap(selection?.codeRef?.range, artifact.codeRef.range)
)
},

View File

@ -18,7 +18,6 @@ import {
default_project_settings,
base64_decode,
clear_scene_and_bust_cache,
change_kcl_settings,
reloadModule,
} from 'lib/wasm_lib_wrapper'
@ -57,7 +56,6 @@ 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'
@ -158,12 +156,6 @@ 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
@ -261,7 +253,7 @@ export const parse = (code: string | Error): ParseResult | Error => {
return new KCLError(
parsed.kind,
parsed.msg,
firstSourceRange(parsed),
sourceRangeFromRust(parsed.sourceRanges[0]),
[],
[],
defaultArtifactGraph()
@ -628,7 +620,7 @@ export const executor = async (
const kclError = new KCLError(
parsed.error.kind,
parsed.error.msg,
firstSourceRange(parsed.error),
sourceRangeFromRust(parsed.error.sourceRanges[0]),
parsed.operations,
parsed.artifactCommands,
rustArtifactGraphToMap(parsed.artifactGraph)
@ -697,7 +689,7 @@ export const modifyAstForSketch = async (
const kclError = new KCLError(
parsed.kind,
parsed.msg,
firstSourceRange(parsed),
sourceRangeFromRust(parsed.sourceRanges[0]),
[],
[],
defaultArtifactGraph()
@ -768,7 +760,7 @@ export function programMemoryInit(): ProgramMemory | Error {
return new KCLError(
parsed.kind,
parsed.msg,
firstSourceRange(parsed),
sourceRangeFromRust(parsed.sourceRanges[0]),
[],
[],
defaultArtifactGraph()
@ -856,17 +848,3 @@ 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,14 +1,17 @@
import { Command } from 'lib/commandTypes'
import { authActor } from 'machines/appMachine'
import { ACTOR_IDS } from 'machines/machineConstants'
import { StateMachineCommandSetConfig } from 'lib/commandTypes'
import { authMachine } from 'machines/authMachine'
export const authCommands: Command[] = [
{
groupId: ACTOR_IDS.AUTH,
name: 'log-out',
displayName: 'Log out',
icon: 'arrowLeft',
needsReview: false,
onSubmit: () => authActor.send({ type: 'Log out' }),
type AuthCommandSchema = {}
export const authCommandBarConfig: StateMachineCommandSetConfig<
typeof authMachine,
AuthCommandSchema
> = {
'Log in': {
hide: 'both',
},
]
'Log out': {
args: [],
icon: 'arrowLeft',
},
}

View File

@ -308,6 +308,7 @@ 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: {
@ -316,6 +317,8 @@ 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',
@ -365,6 +368,7 @@ 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: {
@ -373,6 +377,8 @@ 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

@ -1,19 +0,0 @@
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,7 +3,6 @@ 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++) {
@ -47,20 +46,6 @@ 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,
@ -98,7 +83,7 @@ export const revolveAxisValidator = async ({
value: 360,
}
const command = async () => {
const revolveAboutEdgeCommand = async () => {
return await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
@ -107,18 +92,17 @@ export const revolveAxisValidator = async ({
angle: angleInDegrees,
edge_id: edgeSelection,
target: sketchSelection,
// Gotcha: Playwright will fail with larger tolerances, need to use a smaller one.
tolerance: 1e-7,
tolerance: 0.0001,
},
})
}
const result = await dryRunWrapper(command)
if (result?.success) {
const attemptRevolve = await dryRunWrapper(revolveAboutEdgeCommand)
if (attemptRevolve?.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 ({
@ -144,7 +128,7 @@ export const loftValidator = async ({
return 'Unable to loft, selection contains less than two solid2ds'
}
const command = async () => {
const loftCommand = async () => {
// TODO: check what to do with these
const DEFAULT_V_DEGREE = 2
const DEFAULT_TOLERANCE = 2
@ -161,13 +145,13 @@ export const loftValidator = async ({
},
})
}
const result = await dryRunWrapper(command)
if (result?.success) {
const attempt = await dryRunWrapper(loftCommand)
if (attempt?.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 ({
@ -196,7 +180,7 @@ export const shellValidator = async ({
return "Unable to shell, couldn't find the solid"
}
const command = async () => {
const shellCommand = async () => {
// TODO: figure out something better than an arbitrarily small value
const DEFAULT_THICKNESS: Models['LengthUnit_type'] = 1e-9
const DEFAULT_HOLLOW = false
@ -216,13 +200,12 @@ export const shellValidator = async ({
})
}
const result = await dryRunWrapper(command)
if (result?.success) {
const attemptShell = await dryRunWrapper(shellCommand)
if (attemptShell?.success) {
return true
}
const reason = parseEngineErrorMessage(result) || 'unknown'
return `Unable to shell with the current selection. Reason: ${reason}`
return 'Unable to shell with the provided selection'
}
export const sweepValidator = async ({
@ -258,7 +241,7 @@ export const sweepValidator = async ({
}
const target = targetArtifact.pathId
const command = async () => {
const sweepCommand = async () => {
// TODO: second look on defaults here
const DEFAULT_TOLERANCE: Models['LengthUnit_type'] = 1e-7
const DEFAULT_SECTIONAL = false
@ -278,11 +261,10 @@ export const sweepValidator = async ({
})
}
const result = await dryRunWrapper(command)
if (result?.success) {
const attemptSweep = await dryRunWrapper(sweepCommand)
if (attemptSweep?.success) {
return true
}
const reason = parseEngineErrorMessage(result) || 'unknown'
return `Unable to sweep with the current selection. Reason: ${reason}`
return 'Unable to sweep with the provided selection'
}

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 = `thumbnail.png` as const
export const PROJECT_IMAGE_NAME = `main.jpg` 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,6 +68,8 @@ 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'
@ -143,7 +145,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,7 +10,6 @@ import {
import {
PROJECT_ENTRYPOINT,
PROJECT_FOLDER,
PROJECT_IMAGE_NAME,
PROJECT_SETTINGS_FILE_NAME,
SETTINGS_FILE_NAME,
TELEMETRY_FILE_NAME,
@ -626,19 +625,3 @@ 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(filletSketch, length = -width)
filletExtrude = extrude(-width, filletSketch)
// 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(bracketLeg2Sketch, length = -thickness)
bracketLeg2Extrude = extrude(-thickness, bracketLeg2Sketch)
|> fillet({
radius = extFilletRadius,
tags = [

View File

@ -2,12 +2,13 @@ import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarnin
import { Command, CommandArgumentOption } from './commandTypes'
import { codeManager, kclManager } from './singletons'
import { isDesktop } from './isDesktop'
import { FILE_EXT } from './constants'
import { FILE_EXT, PROJECT_SETTINGS_FILE_NAME } from './constants'
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
import { reportRejection } from './trap'
import { IndexLoaderData } from './types'
import { IS_NIGHTLY_OR_DEBUG } from 'routes/Settings'
import { parseProjectSettings } from 'lang/wasm'
import { err, reportRejection } from './trap'
import { projectConfigurationToSettingsPayload } from './settings/settingsUtils'
import { copyFileShareLink } from './links'
import { IndexLoaderData } from './types'
interface OnSubmitProps {
sampleName: string
@ -67,23 +68,56 @@ 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}`
fetch(sampleCodeUrl)
.then(async (codeResponse): Promise<OnSubmitProps> => {
if (!codeResponse.ok) {
console.error(
'Failed to fetch sample code:',
codeResponse.statusText
)
return Promise.reject(new Error('Failed to fetch sample code'))
}
const code = await codeResponse.text()
return {
sampleName: data.sample.split('/')[0] + FILE_EXT,
code,
method: data.method,
}
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> => {
if (!codeResponse.ok) {
console.error(
'Failed to fetch sample code:',
codeResponse.statusText
)
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
@ -134,22 +168,21 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] {
},
},
},
{
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)
},
},
// {
// 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)
// },
// },
]
}

View File

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

View File

@ -1,7 +1,11 @@
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
import { ASK_TO_OPEN_QUERY_PARAM, CREATE_FILE_URL_PARAM } from './constants'
import {
ASK_TO_OPEN_QUERY_PARAM,
CREATE_FILE_URL_PARAM,
PROD_APP_URL,
} from './constants'
import { stringToBase64 } from './base64'
import { VITE_KC_API_BASE_URL, VITE_KC_SITE_APP_URL } from 'env'
import { DEV, VITE_KC_API_BASE_URL } from 'env'
import toast from 'react-hot-toast'
import { err } from './trap'
export interface FileLinkParams {
@ -47,7 +51,8 @@ export async function copyFileShareLink(
* open the URL in the desktop app.
*/
export function createCreateFileUrl({ code, name, units }: FileLinkParams) {
let origin = VITE_KC_SITE_APP_URL
// Use the dev server if we are in development mode
let origin = DEV ? 'http://localhost:3000' : PROD_APP_URL
const searchParams = new URLSearchParams({
[CREATE_FILE_URL_PARAM]: String(true),
name,

View File

@ -29,8 +29,8 @@ describe('library rectangleTool helper functions', () => {
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %, $rectangleSegmentC001)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
`
// Create ast
const _ast = assertParse(sourceCode)
@ -82,8 +82,8 @@ segAng(rectangleSegmentA001),
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %, $rectangleSegmentC001)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
`
const recasted = recast(ast)
expect(recasted).toEqual(expectedSourceCode)

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