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 DEV=true
VITE_KC_API_WS_MODELING_URL=wss://api.dev.zoo.dev/ws/modeling/commands VITE_KC_API_WS_MODELING_URL=wss://api.dev.zoo.dev/ws/modeling/commands
VITE_KC_API_BASE_URL=https://api.dev.zoo.dev 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_BASE_URL=https://dev.zoo.dev
VITE_KC_SITE_APP_URL=https://app.dev.zoo.dev
VITE_KC_SKIP_AUTH=false VITE_KC_SKIP_AUTH=false
VITE_KC_CONNECTION_TIMEOUT_MS=5000 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! # 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_WS_MODELING_URL=wss://api.zoo.dev/ws/modeling/commands
VITE_KC_API_BASE_URL=https://api.zoo.dev VITE_KC_API_BASE_URL=https://api.zoo.dev
VITE_KC_SITE_BASE_URL=https://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_SKIP_AUTH=false
VITE_KC_CONNECTION_TIMEOUT_MS=15000 VITE_KC_CONNECTION_TIMEOUT_MS=15000

View File

@ -29,13 +29,6 @@
{ {
"name": "isNaN", "name": "isNaN",
"message": "Use Number.isNaN() instead." "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": [ "semi": [

View File

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

View File

@ -46,15 +46,7 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
needs: check-rust-changes needs: check-rust-changes
steps: 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 - uses: actions/checkout@v4
with:
token: ${{ steps.app-token.outputs.token }}
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
@ -131,23 +123,23 @@ jobs:
if: steps.download-wasm.outcome == 'failure' if: steps.download-wasm.outcome == 'failure'
shell: bash shell: bash
run: yarn build:wasm run: yarn build:wasm
- name: build web - name: build electron
shell: bash shell: bash
run: yarn tronb:vite:dev run: yarn tron:package
- name: Run ubuntu/chrome snapshots # - name: Run ubuntu/chrome snapshots
if: ${{ matrix.os == 'namespace-profile-ubuntu-8-cores' && matrix.shardIndex == 1 }} # if: ${{ matrix.os == 'namespace-profile-ubuntu-8-cores' && matrix.shardIndex == 1 }}
shell: bash # shell: bash
# TODO: break this in its own job, for now it's not slowing down the overall execution as ubuntu is the quickest, # # 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. # # but we could do better. This forces a large 1/1 shard of all 20 snapshot tests that runs in about 3 minutes.
run: | # run: |
PLATFORM=web yarn playwright test --config=playwright.config.ts --retries="3" --update-snapshots --grep=@snapshot --shard=1/1 # PLATFORM=web yarn playwright test --config=playwright.config.ts --retries="3" --update-snapshots --grep=@snapshot --shard=1/1
env: # env:
CI: true # CI: true
NODE_ENV: development # NODE_ENV: development
VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} # VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
VITE_KC_SKIP_AUTH: true # VITE_KC_SKIP_AUTH: true
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} # token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
snapshottoken: ${{ secrets.KITTYCAD_API_TOKEN }} # snapshottoken: ${{ secrets.KITTYCAD_API_TOKEN }}
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: ${{ !cancelled() && (success() || failure()) }} if: ${{ !cancelled() && (success() || failure()) }}
with: with:
@ -170,20 +162,20 @@ jobs:
then echo "modified=true" >> $GITHUB_OUTPUT then echo "modified=true" >> $GITHUB_OUTPUT
else echo "modified=false" >> $GITHUB_OUTPUT else echo "modified=false" >> $GITHUB_OUTPUT
fi fi
- name: Commit changes, if any # - name: Commit changes, if any
if: steps.git-check.outputs.modified == 'true' # if: steps.git-check.outputs.modified == 'true'
shell: bash # shell: bash
run: | # run: |
git add . # git add .
git config --local user.email "github-actions[bot]@users.noreply.github.com" # git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]" # 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 remote set-url origin https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
git fetch origin # git fetch origin
echo ${{ github.head_ref }} # echo ${{ github.head_ref }}
git checkout ${{ github.head_ref }} # git checkout ${{ github.head_ref }}
git commit -am "A snapshot a day keeps the bugs away! 📷🐛 (OS: ${{matrix.os}})" || true # git commit -am "A snapshot a day keeps the bugs away! 📷🐛 (OS: ${{matrix.os}})" || true
git push # git push
git push origin ${{ github.head_ref }} # git push origin ${{ github.head_ref }}
# only upload artifacts if there's actually changes # only upload artifacts if there's actually changes
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: steps.git-check.outputs.modified == 'true' if: steps.git-check.outputs.modified == 'true'

View File

@ -108,17 +108,17 @@ jobs:
run: yarn files:set-notes run: yarn files:set-notes
- name: Authenticate to Google Cloud - name: Authenticate to Google Cloud
uses: 'google-github-actions/auth@v2.1.8' uses: 'google-github-actions/auth@v2.1.7'
with: with:
credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}' credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}'
- name: Set up Google Cloud SDK - 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: with:
project_id: ${{ env.GOOGLE_CLOUD_PROJECT_ID }} project_id: ${{ env.GOOGLE_CLOUD_PROJECT_ID }}
- name: Upload release files to public bucket - 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: with:
path: out path: out
glob: '*' 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). 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 ## Checking out commits / Bisecting

View File

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

View File

@ -4,10 +4,7 @@ import { getUtils } from './test-utils'
import { EngineCommand } from 'lang/std/artifactGraph' import { EngineCommand } from 'lang/std/artifactGraph'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
test.describe( test.describe('Can create sketches on all planes and their back sides', () => {
'Can create sketches on all planes and their back sides',
{ tag: ['@skipWin'] },
() => {
const sketchOnPlaneAndBackSideTest = async ( const sketchOnPlaneAndBackSideTest = async (
page: Page, page: Page,
homePage: HomePageFixture, homePage: HomePageFixture,
@ -91,17 +88,11 @@ test.describe(
}) })
test('YZ', async ({ page, homePage }) => { test('YZ', async ({ page, homePage }) => {
await sketchOnPlaneAndBackSideTest(page, homePage, 'YZ', { await sketchOnPlaneAndBackSideTest(page, homePage, 'YZ', { x: 700, y: 250 }) // green plane
x: 700,
y: 250,
}) // green plane
}) })
test('XZ', async ({ page, homePage }) => { test('XZ', async ({ page, homePage }) => {
await sketchOnPlaneAndBackSideTest(page, homePage, '-XZ', { await sketchOnPlaneAndBackSideTest(page, homePage, '-XZ', { x: 700, y: 80 }) // blue plane
x: 700,
y: 80,
}) // blue plane
}) })
test('-XY', async ({ page, homePage }) => { test('-XY', async ({ page, homePage }) => {
@ -119,10 +110,6 @@ test.describe(
}) })
test('-XZ', async ({ page, homePage }) => { test('-XZ', async ({ page, homePage }) => {
await sketchOnPlaneAndBackSideTest(page, homePage, 'XZ', { await sketchOnPlaneAndBackSideTest(page, homePage, 'XZ', { x: 700, y: 427 }) // back of blue plane
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 { TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW } from './storageStates'
import fsp from 'fs/promises' 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 ({ test('Typing KCL errors induces a badge on the code pane button', async ({
page, page,
homePage, homePage,

View File

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

View File

@ -10,7 +10,7 @@ import {
import { join } from 'path' import { join } from 'path'
test.describe('Editor tests', { tag: ['@skipWin'] }, () => { test.describe('Editor tests', () => {
test('can comment out code with ctrl+/', async ({ page, homePage }) => { test('can comment out code with ctrl+/', async ({ page, homePage }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
@ -966,10 +966,10 @@ test.describe('Editor tests', { tag: ['@skipWin'] }, () => {
|> close()`) |> close()`)
}) })
test( test('Can undo a sketch modification with ctrl+z', async ({
'Can undo a sketch modification with ctrl+z', page,
{ tag: ['@skipWin'] }, homePage,
async ({ page, homePage }) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
@ -1113,8 +1113,7 @@ test.describe('Editor tests', { tag: ['@skipWin'] }, () => {
|> tangentialArcTo([24.95, -0.38], %) |> tangentialArcTo([24.95, -0.38], %)
|> close() |> close()
|> extrude(length = 5)`) |> extrude(length = 5)`)
} })
)
test.fixme( test.fixme(
`Can use the import stdlib function on a local OBJ file`, `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 test.step('setup test', async () => {
await homePage.expectState({ await homePage.expectState({
@ -61,7 +61,6 @@ test.describe('integrations tests', () => {
}) })
await test.step('setup for next assertion', async () => { await test.step('setup for next assertion', async () => {
await toolbar.openFile('main.kcl') await toolbar.openFile('main.kcl')
await scene.waitForExecutionDone()
await clickObj() await clickObj()
await scene.moveNoWhere() await scene.moveNoWhere()
await editor.expectState({ 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? * Maybe there a good sanity check we can do each time?
*/ */
expectState = async (expectedState: HomePageState) => { expectState = async (expectedState: HomePageState) => {
await expect.poll(this._serialiseSortBy).toEqual(expectedState.sortBy) await expect
.poll(async () => {
for (const projectCard of expectedState.projectCards) { const [projectCards, sortBy] = await Promise.all([
await expect.poll(this._serialiseProjectCards).toContainEqual(projectCard) this._serialiseProjectCards(),
this._serialiseSortBy(),
])
return {
projectCards,
sortBy,
} }
})
.toEqual(expectedState)
} }
createAndGoToProject = async (projectTitle = 'project-$nnn') => { createAndGoToProject = async (projectTitle = 'project-$nnn') => {

View File

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

View File

@ -8,7 +8,6 @@ import { getUtils } from './test-utils'
// test file is for testing point an click code gen functionality that's not sketch mode related // test file is for testing point an click code gen functionality that's not sketch mode related
test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
test('verify extruding circle works', async ({ test('verify extruding circle works', async ({
context, context,
homePage, homePage,
@ -17,6 +16,8 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
toolbar, toolbar,
scene, scene,
}) => { }) => {
// TODO: fix this test on windows after the electron migration
test.skip(process.platform === 'win32', 'Skip on windows')
const file = await fs.readFile( const file = await fs.readFile(
path.resolve( path.resolve(
__dirname, __dirname,
@ -96,6 +97,8 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
}) })
test.describe('verify sketch on chamfer works', () => { test.describe('verify sketch on chamfer works', () => {
// TODO: fix this test on windows after the electron migration
test.skip(process.platform === 'win32', 'Skip on windows')
const _sketchOnAChamfer = const _sketchOnAChamfer =
( (
page: Page, page: Page,
@ -214,8 +217,7 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
getOppositeEdge(seg01) getOppositeEdge(seg01)
]}, %)`, ]}, %)`,
afterChamferSelectSnippet: afterChamferSelectSnippet: 'sketch002 = startSketchOn(extrude001, seg03)',
'sketch002 = startSketchOn(extrude001, seg03)',
afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)', afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)',
afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002) afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002)
|> angledLine([ |> angledLine([
@ -246,8 +248,7 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
] ]
}, %)`, }, %)`,
afterChamferSelectSnippet: afterChamferSelectSnippet: 'sketch003 = startSketchOn(extrude001, seg04)',
'sketch003 = startSketchOn(extrude001, seg04)',
afterRectangle1stClickSnippet: 'startProfileAt([-209.64, 255.28], %)', afterRectangle1stClickSnippet: 'startProfileAt([-209.64, 255.28], %)',
afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003) afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003)
|> angledLine([ |> angledLine([
@ -272,8 +273,7 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
getNextAdjacentEdge(seg02) getNextAdjacentEdge(seg02)
] ]
}, %)`, }, %)`,
afterChamferSelectSnippet: afterChamferSelectSnippet: 'sketch003 = startSketchOn(extrude001, seg04)',
'sketch003 = startSketchOn(extrude001, seg04)',
afterRectangle1stClickSnippet: 'startProfileAt([75.8, 317.2], %)', afterRectangle1stClickSnippet: 'startProfileAt([75.8, 317.2], %)',
afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003) afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003)
|> angledLine([ |> angledLine([
@ -296,8 +296,7 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
length = 30, length = 30,
tags = [getNextAdjacentEdge(yo)] tags = [getNextAdjacentEdge(yo)]
}, %)`, }, %)`,
afterChamferSelectSnippet: afterChamferSelectSnippet: 'sketch005 = startSketchOn(extrude001, seg06)',
'sketch005 = startSketchOn(extrude001, seg06)',
afterRectangle1stClickSnippet: 'startProfileAt([-23.43, 19.69], %)', afterRectangle1stClickSnippet: 'startProfileAt([-23.43, 19.69], %)',
afterRectangle2ndClickSnippet: `angledLine([0, 9.1], %, $rectangleSegmentA005) afterRectangle2ndClickSnippet: `angledLine([0, 9.1], %, $rectangleSegmentA005)
@ -437,8 +436,7 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
getOppositeEdge(seg01) getOppositeEdge(seg01)
]}, extrude001)`, ]}, extrude001)`,
beforeChamferSnippetEnd: '}, extrude001)', beforeChamferSnippetEnd: '}, extrude001)',
afterChamferSelectSnippet: afterChamferSelectSnippet: 'sketch002 = startSketchOn(extrude001, seg03)',
'sketch002 = startSketchOn(extrude001, seg03)',
afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)', afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)',
afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002) afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002)
|> angledLine([ |> angledLine([
@ -692,11 +690,7 @@ openSketch = startSketchOn('XY')
await test.step(`Double-click on the open sketch`, async () => { await test.step(`Double-click on the open sketch`, async () => {
await moveToOpenPath() await moveToOpenPath()
await scene.expectPixelColor( await scene.expectPixelColor([250, 250, 250], pointOnPathAfterSketching, 15)
[250, 250, 250],
pointOnPathAfterSketching,
15
)
// There is a full execution after exiting sketch that clears the scene. // There is a full execution after exiting sketch that clears the scene.
await page.waitForTimeout(500) await page.waitForTimeout(500)
await dblClickOpenPath() await dblClickOpenPath()
@ -891,10 +885,7 @@ loft001 = loft([sketch001, sketch002])
// One dumb hardcoded screen pixel value // One dumb hardcoded screen pixel value
const testPoint = { x: 575, y: 200 } const testPoint = { x: 575, y: 200 }
const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y) const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const [clickOnSketch2] = scene.makeMouseHelpers( const [clickOnSketch2] = scene.makeMouseHelpers(testPoint.x, testPoint.y + 80)
testPoint.x,
testPoint.y + 80
)
await test.step(`Delete loft`, async () => { await test.step(`Delete loft`, async () => {
// Check for loft // Check for loft
@ -960,10 +951,7 @@ sketch002 = startSketchOn('XZ')
// One dumb hardcoded screen pixel value // One dumb hardcoded screen pixel value
const testPoint = { x: 700, y: 250 } const testPoint = { x: 700, y: 250 }
const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y) const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const [clickOnSketch2] = scene.makeMouseHelpers( const [clickOnSketch2] = scene.makeMouseHelpers(testPoint.x - 50, testPoint.y)
testPoint.x - 50,
testPoint.y
)
const sweepDeclaration = 'sweep001 = sweep({ path = sketch002 }, sketch001)' const sweepDeclaration = 'sweep001 = sweep({ path = sketch002 }, sketch001)'
await test.step(`Look for sketch001`, async () => { await test.step(`Look for sketch001`, async () => {
@ -1054,10 +1042,7 @@ sketch002 = startSketchOn('XZ')
// One dumb hardcoded screen pixel value // One dumb hardcoded screen pixel value
const testPoint = { x: 700, y: 250 } const testPoint = { x: 700, y: 250 }
const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y) const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const [clickOnSketch2] = scene.makeMouseHelpers( const [clickOnSketch2] = scene.makeMouseHelpers(testPoint.x - 50, testPoint.y)
testPoint.x - 50,
testPoint.y
)
await test.step(`Look for sketch001`, async () => { await test.step(`Look for sketch001`, async () => {
await toolbar.closePane('code') await toolbar.closePane('code')
@ -1093,7 +1078,7 @@ sketch002 = startSketchOn('XZ')
await page.waitForTimeout(500) await page.waitForTimeout(500)
await cmdBar.progressCmdBar() await cmdBar.progressCmdBar()
await expect( await expect(
page.getByText('Unable to sweep with the current selection. Reason:') page.getByText('Unable to sweep with the provided selection')
).toBeVisible() ).toBeVisible()
}) })
}) })
@ -1115,7 +1100,7 @@ sketch002 = startSketchOn('XZ')
|> line(end = [0, -12]) |> line(end = [0, -12])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
extrude001 = extrude(sketch001, length = -12) extrude001 = extrude(-12, sketch001)
` `
const firstFilletDeclaration = 'fillet({ radius = 5, tags = [seg01] }, %)' const firstFilletDeclaration = 'fillet({ radius = 5, tags = [seg01] }, %)'
const secondFilletDeclaration = const secondFilletDeclaration =
@ -1198,7 +1183,7 @@ extrude001 = extrude(sketch001, length = -12)
currentArgKey: 'radius', currentArgKey: 'radius',
currentArgValue: '5', currentArgValue: '5',
headerArguments: { headerArguments: {
Selection: '1 segment', Selection: '1 face',
Radius: '', Radius: '',
}, },
stage: 'arguments', stage: 'arguments',
@ -1207,7 +1192,7 @@ extrude001 = extrude(sketch001, length = -12)
await cmdBar.expectState({ await cmdBar.expectState({
commandName: 'Fillet', commandName: 'Fillet',
headerArguments: { headerArguments: {
Selection: '1 segment', Selection: '1 face',
Radius: '5', Radius: '5',
}, },
stage: 'review', stage: 'review',
@ -1311,170 +1296,6 @@ extrude001 = extrude(sketch001, length = -12)
lowTolerance lowTolerance
) )
}) })
// Test 3: Delete fillets
await test.step('Delete fillet via feature tree selection', async () => {
await test.step('Open Feature Tree Pane', async () => {
await toolbar.openPane('feature-tree')
await page.waitForTimeout(500)
})
await test.step('Delete fillet via feature tree selection', async () => {
await editor.expectEditor.toContain(secondFilletDeclaration)
const operationButton = await toolbar.getFeatureTreeOperation(
'Fillet',
1
)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Backspace')
await page.waitForTimeout(500)
await scene.expectPixelColor(edgeColorWhite, secondEdgeLocation, 15) // deleted
await editor.expectEditor.not.toContain(secondFilletDeclaration)
await scene.expectPixelColor(filletColor, firstEdgeLocation, 15) // stayed
})
})
})
test(`Fillet point-and-click delete`, async ({
context,
page,
homePage,
scene,
editor,
toolbar,
}) => {
// Code samples
const initialCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-12, -6], %)
|> line(end = [0, 12])
|> line(end = [24, 0], tag = $seg02)
|> line(end = [0, -12])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg01)
|> close()
extrude001 = extrude(sketch001, length = -12)
|> fillet({ radius = 5, tags = [seg01] }, %) // fillet01
|> fillet({ radius = 5, tags = [seg02] }, %) // fillet02
fillet03 = fillet({ radius = 5, tags = [getOppositeEdge(seg01)]}, extrude001)
fillet04 = fillet({ radius = 5, tags = [getOppositeEdge(seg02)]}, extrude001)
`
const pipedFilletDeclaration = 'fillet({ radius = 5, tags = [seg01] }, %)'
const secondPipedFilletDeclaration =
'fillet({ radius = 5, tags = [seg02] }, %)'
const standaloneFilletDeclaration =
'fillet03 = fillet({ radius = 5, tags = [getOppositeEdge(seg01)]}, extrude001)'
const secondStandaloneFilletDeclaration =
'fillet04 = fillet({ radius = 5, tags = [getOppositeEdge(seg02)]}, extrude001)'
// Locators
const pipedFilletEdgeLocation = { x: 600, y: 193 }
const standaloneFilletEdgeLocation = { x: 600, y: 383 }
const bodyLocation = { x: 630, y: 290 }
// Colors
const edgeColorWhite: [number, number, number] = [248, 248, 248]
const bodyColor: [number, number, number] = [155, 155, 155]
const filletColor: [number, number, number] = [127, 127, 127]
const backgroundColor: [number, number, number] = [30, 30, 30]
const lowTolerance = 20
const highTolerance = 40
// Setup
await test.step(`Initial test setup`, async () => {
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
// verify modeling scene is loaded
await scene.expectPixelColor(
backgroundColor,
standaloneFilletEdgeLocation,
lowTolerance
)
// wait for stream to load
await scene.expectPixelColor(bodyColor, bodyLocation, highTolerance)
})
// Test
await test.step('Delete fillet via feature tree selection', async () => {
await test.step('Open Feature Tree Pane', async () => {
await toolbar.openPane('feature-tree')
await page.waitForTimeout(500)
})
await test.step('Delete piped fillet via feature tree selection', async () => {
await test.step('Verify all fillets are present in the editor', async () => {
await editor.expectEditor.toContain(pipedFilletDeclaration)
await editor.expectEditor.toContain(secondPipedFilletDeclaration)
await editor.expectEditor.toContain(standaloneFilletDeclaration)
await editor.expectEditor.toContain(secondStandaloneFilletDeclaration)
})
await test.step('Verify test fillets are present in the scene', async () => {
await scene.expectPixelColor(
filletColor,
pipedFilletEdgeLocation,
lowTolerance
)
await scene.expectPixelColor(
backgroundColor,
standaloneFilletEdgeLocation,
lowTolerance
)
})
await test.step('Delete piped fillet', async () => {
const operationButton = await toolbar.getFeatureTreeOperation(
'Fillet',
0
)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Backspace')
await page.waitForTimeout(500)
})
await test.step('Verify piped fillet is deleted but other fillets are not (in the editor)', async () => {
await editor.expectEditor.not.toContain(pipedFilletDeclaration)
await editor.expectEditor.toContain(secondPipedFilletDeclaration)
await editor.expectEditor.toContain(standaloneFilletDeclaration)
await editor.expectEditor.toContain(secondStandaloneFilletDeclaration)
})
await test.step('Verify piped fillet is deleted but non-piped is not (in the scene)', async () => {
await scene.expectPixelColor(
edgeColorWhite, // you see edge because fillet is deleted
pipedFilletEdgeLocation,
lowTolerance
)
await scene.expectPixelColor(
backgroundColor, // you see background because fillet is not deleted
standaloneFilletEdgeLocation,
lowTolerance
)
})
})
await test.step('Delete non-piped fillet via feature tree selection', async () => {
await test.step('Delete non-piped fillet', async () => {
const operationButton = await toolbar.getFeatureTreeOperation(
'Fillet',
1
)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Backspace')
await page.waitForTimeout(500)
})
await test.step('Verify non-piped fillet is deleted but other two fillets are not (in the editor)', async () => {
await editor.expectEditor.toContain(secondPipedFilletDeclaration)
await editor.expectEditor.not.toContain(standaloneFilletDeclaration)
await editor.expectEditor.toContain(secondStandaloneFilletDeclaration)
})
await test.step('Verify non-piped fillet is deleted but piped is not (in the scene)', async () => {
await scene.expectPixelColor(
edgeColorWhite,
standaloneFilletEdgeLocation,
lowTolerance
)
})
})
})
}) })
test(`Chamfer point-and-click`, async ({ test(`Chamfer point-and-click`, async ({
@ -1577,7 +1398,7 @@ extrude001 = extrude(sketch001, length = -12)
currentArgKey: 'length', currentArgKey: 'length',
currentArgValue: '5', currentArgValue: '5',
headerArguments: { headerArguments: {
Selection: '1 segment', Selection: '1 face',
Length: '', Length: '',
}, },
stage: 'arguments', stage: 'arguments',
@ -1586,7 +1407,7 @@ extrude001 = extrude(sketch001, length = -12)
await cmdBar.expectState({ await cmdBar.expectState({
commandName: 'Chamfer', commandName: 'Chamfer',
headerArguments: { headerArguments: {
Selection: '1 segment', Selection: '1 face',
Length: '5', Length: '5',
}, },
stage: 'review', stage: 'review',
@ -1604,11 +1425,7 @@ extrude001 = extrude(sketch001, length = -12)
}) })
await test.step(`Confirm scene has changed`, async () => { await test.step(`Confirm scene has changed`, async () => {
await scene.expectPixelColor( await scene.expectPixelColor(chamferColor, firstEdgeLocation, lowTolerance)
chamferColor,
firstEdgeLocation,
lowTolerance
)
}) })
// Test 2: Command bar flow without preselected edges // Test 2: Command bar flow without preselected edges
@ -1694,172 +1511,6 @@ extrude001 = extrude(sketch001, length = -12)
lowTolerance lowTolerance
) )
}) })
// Test 3: Delete chamfer via feature tree selection
await test.step('Open Feature Tree Pane', async () => {
await toolbar.openPane('feature-tree')
await page.waitForTimeout(500)
})
await test.step('Delete chamfer via feature tree selection', async () => {
const operationButton = await toolbar.getFeatureTreeOperation(
'Chamfer',
1
)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Backspace')
await page.waitForTimeout(500)
await scene.expectPixelColor(edgeColorWhite, secondEdgeLocation, 15) // deleted
await scene.expectPixelColor(chamferColor, firstEdgeLocation, 15) // stayed
})
})
test(`Chamfer point-and-click delete`, async ({
context,
page,
homePage,
scene,
editor,
toolbar,
}) => {
// Code samples
const initialCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-12, -6], %)
|> line(end = [0, 12])
|> line(end = [24, 0], tag = $seg02)
|> line(end = [0, -12])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg01)
|> close()
extrude001 = extrude(sketch001, length = -12)
|> chamfer({ length = 5, tags = [seg01] }, %) // chamfer01
|> chamfer({ length = 5, tags = [seg02] }, %) // chamfer02
chamfer03 = chamfer({ length = 5, tags = [getOppositeEdge(seg01)]}, extrude001)
chamfer04 = chamfer({ length = 5, tags = [getOppositeEdge(seg02)]}, extrude001)
`
const pipedChamferDeclaration = 'chamfer({ length = 5, tags = [seg01] }, %)'
const secondPipedChamferDeclaration =
'chamfer({ length = 5, tags = [seg02] }, %)'
const standaloneChamferDeclaration =
'chamfer03 = chamfer({ length = 5, tags = [getOppositeEdge(seg01)]}, extrude001)'
const secondStandaloneChamferDeclaration =
'chamfer04 = chamfer({ length = 5, tags = [getOppositeEdge(seg02)]}, extrude001)'
// Locators
const pipedChamferEdgeLocation = { x: 600, y: 193 }
const standaloneChamferEdgeLocation = { x: 600, y: 383 }
const bodyLocation = { x: 630, y: 290 }
// Colors
const edgeColorWhite: [number, number, number] = [248, 248, 248]
const bodyColor: [number, number, number] = [155, 155, 155]
const chamferColor: [number, number, number] = [168, 168, 168]
const backgroundColor: [number, number, number] = [30, 30, 30]
const lowTolerance = 20
const highTolerance = 40
// Setup
await test.step(`Initial test setup`, async () => {
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
// verify modeling scene is loaded
await scene.expectPixelColor(
backgroundColor,
standaloneChamferEdgeLocation,
lowTolerance
)
// wait for stream to load
await scene.expectPixelColor(bodyColor, bodyLocation, highTolerance)
})
// Test
await test.step('Delete chamfer via feature tree selection', async () => {
await test.step('Open Feature Tree Pane', async () => {
await toolbar.openPane('feature-tree')
await page.waitForTimeout(500)
})
await test.step('Delete piped chamfer via feature tree selection', async () => {
await test.step('Verify all chamfers are present in the editor', async () => {
await editor.expectEditor.toContain(pipedChamferDeclaration)
await editor.expectEditor.toContain(secondPipedChamferDeclaration)
await editor.expectEditor.toContain(standaloneChamferDeclaration)
await editor.expectEditor.toContain(
secondStandaloneChamferDeclaration
)
})
await test.step('Verify test chamfers are present in the scene', async () => {
await scene.expectPixelColor(
chamferColor,
pipedChamferEdgeLocation,
lowTolerance
)
await scene.expectPixelColor(
backgroundColor,
standaloneChamferEdgeLocation,
lowTolerance
)
})
await test.step('Delete piped chamfer', async () => {
const operationButton = await toolbar.getFeatureTreeOperation(
'Chamfer',
0
)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Backspace')
await page.waitForTimeout(500)
})
await test.step('Verify piped chamfer is deleted but other chamfers are not (in the editor)', async () => {
await editor.expectEditor.not.toContain(pipedChamferDeclaration)
await editor.expectEditor.toContain(secondPipedChamferDeclaration)
await editor.expectEditor.toContain(standaloneChamferDeclaration)
await editor.expectEditor.toContain(
secondStandaloneChamferDeclaration
)
})
await test.step('Verify piped chamfer is deleted but non-piped is not (in the scene)', async () => {
await scene.expectPixelColor(
edgeColorWhite, // you see edge color because chamfer is deleted
pipedChamferEdgeLocation,
lowTolerance
)
await scene.expectPixelColor(
backgroundColor, // you see background color instead of edge because it's chamfered
standaloneChamferEdgeLocation,
lowTolerance
)
})
})
await test.step('Delete non-piped chamfer via feature tree selection', async () => {
await test.step('Delete non-piped chamfer', async () => {
const operationButton = await toolbar.getFeatureTreeOperation(
'Chamfer',
1
)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Backspace')
await page.waitForTimeout(500)
})
await test.step('Verify non-piped chamfer is deleted but other two chamfers are not (in the editor)', async () => {
await editor.expectEditor.toContain(secondPipedChamferDeclaration)
await editor.expectEditor.not.toContain(standaloneChamferDeclaration)
await editor.expectEditor.toContain(
secondStandaloneChamferDeclaration
)
})
await test.step('Verify non-piped chamfer is deleted but piped is not (in the scene)', async () => {
await scene.expectPixelColor(
edgeColorWhite,
standaloneChamferEdgeLocation,
lowTolerance
)
})
})
})
}) })
const shellPointAndClickCapCases = [ const shellPointAndClickCapCases = [
@ -1876,6 +1527,8 @@ chamfer04 = chamfer({ length = 5, tags = [getOppositeEdge(seg02)]}, extrude001
toolbar, toolbar,
cmdBar, cmdBar,
}) => { }) => {
// TODO: fix this test on windows after the electron migration
test.skip(process.platform === 'win32', 'Skip on windows')
const initialCode = `sketch001 = startSketchOn('XZ') const initialCode = `sketch001 = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 30 }, %) |> circle({ center = [0, 0], radius = 30 }, %)
extrude001 = extrude(sketch001, length = 30) extrude001 = extrude(sketch001, length = 30)
@ -2193,169 +1846,8 @@ sweep001 = sweep({ path = sketch002 }, sketch001)
await page.waitForTimeout(500) await page.waitForTimeout(500)
await cmdBar.progressCmdBar() await cmdBar.progressCmdBar()
await expect( await expect(
page.getByText('Unable to shell with the current selection. Reason:') page.getByText('Unable to shell with the provided selection')
).toBeVisible() ).toBeVisible()
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
}) })
}) })
test.describe('Revolve point and click workflows', () => {
test('Base case workflow, auto spam continue in command bar', async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
const initialCode = `
sketch001 = startSketchOn('XZ')
|> startProfileAt([-100.0, 100.0], %)
|> angledLine([0, 200.0], %, $rectangleSegmentA001)
|> angledLine([segAng(rectangleSegmentA001) - 90, 200], %, $rectangleSegmentB001)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %, $rectangleSegmentC001)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(sketch001, length = 200)
sketch002 = startSketchOn(extrude001, rectangleSegmentA001)
|> startProfileAt([-66.77, 84.81], %)
|> angledLine([180, 27.08], %, $rectangleSegmentA002)
|> angledLine([
segAng(rectangleSegmentA002) - 90,
27.8
], %, $rectangleSegmentB002)
|> angledLine([
segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002)
], %, $rectangleSegmentC002)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
`
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
// select line of code
const codeToSelecton = `segAng(rectangleSegmentA002) - 90,`
// revolve
await page.getByText(codeToSelecton).click()
await toolbar.revolveButton.click()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
const newCodeToFind = `revolve001 = revolve({ angle = 360, axis = 'X' }, sketch002)`
expect(editor.expectEditor.toContain(newCodeToFind)).toBeTruthy()
})
test('revolve surface around edge from an extruded solid2d', async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
const initialCode = `
sketch001 = startSketchOn('XZ')
|> startProfileAt([-102.57, 101.72], %)
|> angledLine([0, 202.6], %, $rectangleSegmentA001)
|> angledLine([
segAng(rectangleSegmentA001) - 90,
202.6
], %, $rectangleSegmentB001)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %, $rectangleSegmentC001)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(sketch001, length = 50)
sketch002 = startSketchOn(extrude001, rectangleSegmentA001)
|> circle({
center = [-11.34, 10.0],
radius = 8.69
}, %)
`
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
// select line of code
const codeToSelecton = `center = [-11.34, 10.0]`
// revolve
await page.getByText(codeToSelecton).click()
await toolbar.revolveButton.click()
await page.getByText('Edge', { exact: true }).click()
const lineCodeToSelection = `|> angledLine([0, 202.6], %, $rectangleSegmentA001)`
await page.getByText(lineCodeToSelection).click()
await cmdBar.progressCmdBar()
const newCodeToFind = `revolve001 = revolve({angle = 360, axis = getOppositeEdge(rectangleSegmentA001)}, sketch002) `
expect(editor.expectEditor.toContain(newCodeToFind)).toBeTruthy()
})
test('revolve sketch circle around line segment from startProfileAt sketch', async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
const initialCode = `
sketch002 = startSketchOn('XY')
|> startProfileAt([-2.02, 1.79], %)
|> xLine(2.6, %)
sketch001 = startSketchOn('-XY')
|> startProfileAt([-0.48, 1.25], %)
|> angledLine([0, 2.38], %, $rectangleSegmentA001)
|> angledLine([segAng(rectangleSegmentA001) - 90, 2.4], %, $rectangleSegmentB001)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %, $rectangleSegmentC001)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(sketch001, length = 5)
sketch003 = startSketchOn(extrude001, 'START')
|> circle({
center = [-0.69, 0.56],
radius = 0.28
}, %)
`
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
// select line of code
const codeToSelecton = `center = [-0.69, 0.56]`
// revolve
await page.getByText(codeToSelecton).click()
await toolbar.revolveButton.click()
await page.getByText('Edge', { exact: true }).click()
const lineCodeToSelection = `|> xLine(2.6, %)`
await page.getByText(lineCodeToSelection).click()
await cmdBar.progressCmdBar()
const newCodeToFind = `revolve001 = revolve({ angle = 360, axis = seg01 }, sketch003)`
expect(editor.expectEditor.toContain(newCodeToFind)).toBeTruthy()
})
})
})

View File

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

View File

@ -35,7 +35,7 @@ sketch003 = startSketchOn('XY')
extrude003 = extrude(sketch003, length = 20) 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 = [ const cases = [
{ {
desc: 'User accepts change', 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 ({ test(`bad edit prompt`, async ({
context, context,
homePage, homePage,

View File

@ -5,7 +5,7 @@ import { getUtils, executorInputPath } from './test-utils'
import { TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR } from './storageStates' import { TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR } from './storageStates'
import { bracket } from 'lib/exampleKcl' 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 // bugs we found that don't fit neatly into other categories
test('bad model has inline error #3251', async ({ test('bad model has inline error #3251', async ({
context, context,
@ -239,6 +239,12 @@ extrude001 = extrude(sketch001, length = 50)
'Position _ Is Out Of Range... regression test', 'Position _ Is Out Of Range... regression test',
{ tag: ['@skipWin'] }, { tag: ['@skipWin'] },
async ({ context, page, homePage }) => { 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 u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio // const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setBodyDimensions({ width: 1200, height: 500 }) 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', 'ensure you can not export while an export is already going',
{ tag: ['@skipLinux', '@skipWin'] }, { tag: ['@skipLinux', '@skipWin'] },
async ({ page, homePage }) => { 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) const u = await getUtils(page)
await test.step('Set up the code and durations', async () => { await test.step('Set up the code and durations', async () => {
await page.addInitScript( await page.addInitScript(
@ -547,6 +560,8 @@ extrude001 = extrude(sketch001, length = 50)
page, page,
homePage, 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 u = await getUtils(page)
// Constants and locators // Constants and locators

View File

@ -10,7 +10,7 @@ import {
} from './test-utils' } from './test-utils'
import { uuidv4, roundOff } from 'lib/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 ({ test('multi-sketch file shows multiple Edit Sketch buttons', async ({
page, page,
context, context,
@ -312,10 +312,7 @@ test.describe('Sketch tests', { tag: ['@skipWin'] }, () => {
|> line(end = [1.97, 2.06]) |> line(end = [1.97, 2.06])
|> close()`) |> close()`)
} }
test( test('code pane open at start-handles', async ({ page, homePage }) => {
'code pane open at start-handles',
{ tag: ['@skipWin'] },
async ({ page, homePage }) => {
// Load the app with the code panes // Load the app with the code panes
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
@ -329,13 +326,9 @@ test.describe('Sketch tests', { tag: ['@skipWin'] }, () => {
) )
}) })
await doEditSegmentsByDraggingHandle(page, homePage, ['code']) await doEditSegmentsByDraggingHandle(page, homePage, ['code'])
} })
)
test( test('code pane closed at start-handles', async ({ page, homePage }) => {
'code pane closed at start-handles',
{ tag: ['@skipWin'] },
async ({ page, homePage }) => {
// Load the app with the code panes // Load the app with the code panes
await page.addInitScript(async (persistModelingContext) => { await page.addInitScript(async (persistModelingContext) => {
localStorage.setItem( localStorage.setItem(
@ -344,8 +337,7 @@ test.describe('Sketch tests', { tag: ['@skipWin'] }, () => {
) )
}, PERSIST_MODELING_CONTEXT) }, PERSIST_MODELING_CONTEXT)
await doEditSegmentsByDraggingHandle(page, homePage, []) await doEditSegmentsByDraggingHandle(page, homePage, [])
} })
)
}) })
test('Can edit a circle center and radius by dragging its handles', async ({ 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" }, %)`) |> revolve({ axis = "X" }, %)`)
}) })
test('Can add multiple sketches', async ({ page, homePage }) => { 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 u = await getUtils(page)
const viewportSize = { width: 1200, height: 500 } const viewportSize = { width: 1200, height: 500 }
@ -841,6 +835,8 @@ test.describe('Sketch tests', { tag: ['@skipWin'] }, () => {
page, page,
homePage, 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 // this was a regression https://github.com/KittyCAD/modeling-app/issues/2832
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( 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 // 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. // otherwise the cmdbar would be waiting for a selection.
await expect( await expect(
page.getByRole('button', { name: 'selection : 1 segment', exact: false }) page.getByRole('button', { name: 'selection : 1 face', exact: false })
).toBeVisible({ ).toBeVisible({
timeout: 10_000, timeout: 10_000,
}) })
@ -1431,7 +1427,7 @@ test.describe('Redirecting to home page and back to the original file should cle
'persistCode', 'persistCode',
` sketch001 = startSketchOn('XZ') ` sketch001 = startSketchOn('XZ')
|> startProfileAt([256.85, 14.41], %) |> 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', 'exports of each format should work',
{ tag: ['@snapshot', '@skipWin', '@skipMacos'] }, { tag: ['@snapshot', '@skipWin', '@skipMacos'] },
async ({ page, context }) => { 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 // FYI this test doesn't work with only engine running locally
// And you will need to have the KittyCAD CLI installed // And you will need to have the KittyCAD CLI installed
const u = await getUtils(page) 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) => { selectFile: async (name: string) => {
return test?.step(`Select ${name}`, async () => { return test?.step(`Select ${name}`, async () => {
await page await page

View File

@ -3,8 +3,13 @@ import { EngineCommand } from 'lang/std/artifactGraph'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
import { getUtils } from './test-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 }) => { 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) const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
@ -342,6 +347,8 @@ test.describe('Testing Camera Movement', { tag: ['@skipWin'] }, () => {
homePage, homePage,
page, 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, * Currently we only allow zooming by scroll when no other camera movement is happening,
* set within cameraMouseDragGuards in cameraControls.ts, * set within cameraMouseDragGuards in cameraControls.ts,

View File

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

View File

@ -3,7 +3,9 @@ import { getUtils } from './test-utils'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
import { TEST_CODE_GIZMO } from './storageStates' 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 = [ const cases = [
{ {
testDescription: 'top view', testDescription: 'top view',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,8 +9,23 @@ const rootDir = process.cwd()
const config: ForgeConfig = { const config: ForgeConfig = {
packagerConfig: { packagerConfig: {
asar: true, 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', executableName: 'zoo-modeling-app',
icon: path.resolve(rootDir, 'assets', 'icon'), icon: path.resolve(rootDir, 'assets', 'icon'),
protocols: [
{
name: 'Zoo Studio',
schemes: ['zoo-studio'],
},
],
extendInfo: 'Info.plist', // Information for file associations.
}, },
rebuildConfig: {}, rebuildConfig: {},
makers: [], makers: [],

2
interface.d.ts vendored
View File

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

View File

@ -103,11 +103,11 @@
"make:dev": "make dev", "make:dev": "make dev",
"generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts", "generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts",
"tron:start": "electron-forge start", "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'", "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", "tron:test": "NODE_ENV=development yarn playwright test --config=playwright.electron.config.ts --grep-invert='@snapshot'",
"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:vite": "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": "electron-builder --config electron-builder.yml",
"tronb:package:prod": "yarn tronb:vite:prod && electron-builder --config electron-builder.yml --publish always",
"test-setup": "yarn install && yarn build:wasm", "test-setup": "yarn install && yarn build:wasm",
"test": "vitest --mode development", "test": "vitest --mode development",
"test:unit": "vitest run --mode development --exclude **/kclSamples.test.ts", "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: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: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: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:local": "yarn tron:package && 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: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 tronb:vite:dev && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@skipMacos|@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 tronb:vite:dev && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@skipLinux|@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: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" "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": "^5.4.12",
"vite-plugin-package-version": "^1.1.0", "vite-plugin-package-version": "^1.1.0",
"vite-tsconfig-paths": "^4.3.2", "vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.6.1", "vitest": "^1.6.0",
"vitest-webgl-canvas-mock": "^1.1.0", "vitest-webgl-canvas-mock": "^1.1.0",
"wasm-pack": "^0.13.1", "wasm-pack": "^0.13.1",
"ws": "^8.17.0", "ws": "^8.17.0",

View File

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

View File

@ -313,62 +313,62 @@
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==
"@vitest/expect@2.1.9": "@vitest/expect@2.1.8":
version "2.1.9" version "2.1.8"
resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-2.1.9.tgz#b566ea20d58ea6578d8dc37040d6c1a47ebe5ff8" resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-2.1.8.tgz#13fad0e8d5a0bf0feb675dcf1d1f1a36a1773bc1"
integrity sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw== integrity sha512-8ytZ/fFHq2g4PJVAtDX57mayemKgDR6X3Oa2Foro+EygiOJHUXhCqBAAKQYYajZpFoIfvBCF1j6R6IYRSIUFuw==
dependencies: dependencies:
"@vitest/spy" "2.1.9" "@vitest/spy" "2.1.8"
"@vitest/utils" "2.1.9" "@vitest/utils" "2.1.8"
chai "^5.1.2" chai "^5.1.2"
tinyrainbow "^1.2.0" tinyrainbow "^1.2.0"
"@vitest/mocker@2.1.9": "@vitest/mocker@2.1.8":
version "2.1.9" version "2.1.8"
resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-2.1.9.tgz#36243b27351ca8f4d0bbc4ef91594ffd2dc25ef5" resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-2.1.8.tgz#51dec42ac244e949d20009249e033e274e323f73"
integrity sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg== integrity sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA==
dependencies: dependencies:
"@vitest/spy" "2.1.9" "@vitest/spy" "2.1.8"
estree-walker "^3.0.3" estree-walker "^3.0.3"
magic-string "^0.30.12" magic-string "^0.30.12"
"@vitest/pretty-format@2.1.9", "@vitest/pretty-format@^2.1.9": "@vitest/pretty-format@2.1.8", "@vitest/pretty-format@^2.1.8":
version "2.1.9" version "2.1.8"
resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-2.1.9.tgz#434ff2f7611689f9ce70cd7d567eceb883653fdf" resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-2.1.8.tgz#88f47726e5d0cf4ba873d50c135b02e4395e2bca"
integrity sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ== integrity sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==
dependencies: dependencies:
tinyrainbow "^1.2.0" tinyrainbow "^1.2.0"
"@vitest/runner@2.1.9": "@vitest/runner@2.1.8":
version "2.1.9" version "2.1.8"
resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-2.1.9.tgz#cc18148d2d797fd1fd5908d1f1851d01459be2f6" resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-2.1.8.tgz#b0e2dd29ca49c25e9323ea2a45a5125d8729759f"
integrity sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g== integrity sha512-17ub8vQstRnRlIU5k50bG+QOMLHRhYPAna5tw8tYbj+jzjcspnwnwtPtiOlkuKC4+ixDPTuLZiqiWWQ2PSXHVg==
dependencies: dependencies:
"@vitest/utils" "2.1.9" "@vitest/utils" "2.1.8"
pathe "^1.1.2" pathe "^1.1.2"
"@vitest/snapshot@2.1.9": "@vitest/snapshot@2.1.8":
version "2.1.9" version "2.1.8"
resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-2.1.9.tgz#24260b93f798afb102e2dcbd7e61c6dfa118df91" resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-2.1.8.tgz#d5dc204f4b95dc8b5e468b455dfc99000047d2de"
integrity sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ== integrity sha512-20T7xRFbmnkfcmgVEz+z3AU/3b0cEzZOt/zmnvZEctg64/QZbSDJEVm9fLnnlSi74KibmRsO9/Qabi+t0vCRPg==
dependencies: dependencies:
"@vitest/pretty-format" "2.1.9" "@vitest/pretty-format" "2.1.8"
magic-string "^0.30.12" magic-string "^0.30.12"
pathe "^1.1.2" pathe "^1.1.2"
"@vitest/spy@2.1.9": "@vitest/spy@2.1.8":
version "2.1.9" version "2.1.8"
resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-2.1.9.tgz#cb28538c5039d09818b8bfa8edb4043c94727c60" resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-2.1.8.tgz#bc41af3e1e6a41ae3b67e51f09724136b88fa447"
integrity sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ== integrity sha512-5swjf2q95gXeYPevtW0BLk6H8+bPlMb4Vw/9Em4hFxDcaOxS+e0LOX4yqNxoHzMR2akEB2xfpnWUzkZokmgWDg==
dependencies: dependencies:
tinyspy "^3.0.2" tinyspy "^3.0.2"
"@vitest/utils@2.1.9": "@vitest/utils@2.1.8":
version "2.1.9" version "2.1.8"
resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-2.1.9.tgz#4f2486de8a54acf7ecbf2c5c24ad7994a680a6c1" resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-2.1.8.tgz#f8ef85525f3362ebd37fd25d268745108d6ae388"
integrity sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ== integrity sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==
dependencies: dependencies:
"@vitest/pretty-format" "2.1.9" "@vitest/pretty-format" "2.1.8"
loupe "^3.1.2" loupe "^3.1.2"
tinyrainbow "^1.2.0" tinyrainbow "^1.2.0"
@ -662,10 +662,10 @@ typescript@^5.7.2:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6"
integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg== integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==
vite-node@2.1.9: vite-node@2.1.8:
version "2.1.9" version "2.1.8"
resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-2.1.9.tgz#549710f76a643f1c39ef34bdb5493a944e4f895f" resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-2.1.8.tgz#9495ca17652f6f7f95ca7c4b568a235e0c8dbac5"
integrity sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA== integrity sha512-uPAwSr57kYjAUux+8E2j0q0Fxpn8M9VoyfGiRI8Kfktz9NcYMCenwY5RnZxnF1WTu3TGiYipirIzacLL3VVGFg==
dependencies: dependencies:
cac "^6.7.14" cac "^6.7.14"
debug "^4.3.7" debug "^4.3.7"
@ -693,18 +693,18 @@ vite@^5.0.0:
optionalDependencies: optionalDependencies:
fsevents "~2.3.3" fsevents "~2.3.3"
vitest@^2.1.9: vitest@^2.1.8:
version "2.1.9" version "2.1.8"
resolved "https://registry.yarnpkg.com/vitest/-/vitest-2.1.9.tgz#7d01ffd07a553a51c87170b5e80fea3da7fb41e7" resolved "https://registry.yarnpkg.com/vitest/-/vitest-2.1.8.tgz#2e6a00bc24833574d535c96d6602fb64163092fa"
integrity sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q== integrity sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ==
dependencies: dependencies:
"@vitest/expect" "2.1.9" "@vitest/expect" "2.1.8"
"@vitest/mocker" "2.1.9" "@vitest/mocker" "2.1.8"
"@vitest/pretty-format" "^2.1.9" "@vitest/pretty-format" "^2.1.8"
"@vitest/runner" "2.1.9" "@vitest/runner" "2.1.8"
"@vitest/snapshot" "2.1.9" "@vitest/snapshot" "2.1.8"
"@vitest/spy" "2.1.9" "@vitest/spy" "2.1.8"
"@vitest/utils" "2.1.9" "@vitest/utils" "2.1.8"
chai "^5.1.2" chai "^5.1.2"
debug "^4.3.7" debug "^4.3.7"
expect-type "^1.1.0" expect-type "^1.1.0"
@ -716,7 +716,7 @@ vitest@^2.1.9:
tinypool "^1.0.1" tinypool "^1.0.1"
tinyrainbow "^1.2.0" tinyrainbow "^1.2.0"
vite "^5.0.0" vite "^5.0.0"
vite-node "2.1.9" vite-node "2.1.8"
why-is-node-running "^2.3.0" why-is-node-running "^2.3.0"
w3c-keyname@^2.2.4: 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 { Marked } from '@ts-stack/markdown'
import type * as LSP from 'vscode-languageserver-protocol' 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 // 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) { export function deferExecution<T>(func: (args: T) => any, wait: number) {
@ -46,7 +45,7 @@ export function offsetToPos(doc: Text, offset: number) {
export function formatMarkdownContents( export function formatMarkdownContents(
contents: LSP.MarkupContent | LSP.MarkedString | LSP.MarkedString[] contents: LSP.MarkupContent | LSP.MarkedString | LSP.MarkedString[]
): string { ): string {
if (isArray(contents)) { if (Array.isArray(contents)) {
return contents.map((c) => formatMarkdownContents(c) + '\n\n').join('') return contents.map((c) => formatMarkdownContents(c) + '\n\n').join('')
} else if (typeof contents === 'string') { } else if (typeof contents === 'string') {
return Marked.parse(contents) 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 { useHotKeyListener } from './hooks/useHotKeyListener'
import { Stream } from './components/Stream' import { Stream } from './components/Stream'
import { AppHeader } from './components/AppHeader' import { AppHeader } from './components/AppHeader'
@ -24,12 +24,7 @@ import { UnitsMenu } from 'components/UnitsMenu'
import { CameraProjectionToggle } from 'components/CameraProjectionToggle' import { CameraProjectionToggle } from 'components/CameraProjectionToggle'
import { useCreateFileLinkQuery } from 'hooks/useCreateFileLinkQueryWatcher' import { useCreateFileLinkQuery } from 'hooks/useCreateFileLinkQueryWatcher'
import { maybeWriteToDisk } from 'lib/telemetry' 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 { commandBarActor } from 'machines/commandBarMachine'
import { useToken } from 'machines/appMachine'
maybeWriteToDisk() maybeWriteToDisk()
.then(() => {}) .then(() => {})
.catch(() => {}) .catch(() => {})
@ -59,20 +54,14 @@ export function App() {
const projectName = project?.name || null const projectName = project?.name || null
const projectPath = project?.path || 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(() => { useEffect(() => {
onProjectOpen({ name: projectName, path: projectPath }, file || null) onProjectOpen({ name: projectName, path: projectPath }, file || null)
}, [projectName, projectPath]) }, [projectName, projectPath])
useHotKeyListener() useHotKeyListener()
const { settings } = useSettingsAuthContext() const { auth, settings } = useSettingsAuthContext()
const token = useToken() const token = auth?.context?.token
const coreDumpManager = useMemo( const coreDumpManager = useMemo(
() => new CoreDumpManager(engineCommandManager, codeManager, token), () => new CoreDumpManager(engineCommandManager, codeManager, token),
@ -102,32 +91,6 @@ export function App() {
useEngineConnectionSubscriptions() 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 ( return (
<div className="relative h-full flex flex-col" ref={ref}> <div className="relative h-full flex flex-col" ref={ref}>
<AppHeader <AppHeader

View File

@ -1,10 +1,10 @@
import { useAuthState } from 'machines/appMachine'
import Loading from './components/Loading' import Loading from './components/Loading'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
// Wrapper around protected routes, used in src/Router.tsx // Wrapper around protected routes, used in src/Router.tsx
export const Auth = ({ children }: React.PropsWithChildren) => { export const Auth = ({ children }: React.PropsWithChildren) => {
const authState = useAuthState() const { auth } = useSettingsAuthContext()
const isLoggingIn = authState.matches('checkIfLoggedIn') const isLoggingIn = auth?.state.matches('checkIfLoggedIn')
return isLoggingIn ? ( return isLoggingIn ? (
<Loading> <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 { ASK_TO_OPEN_QUERY_PARAM, BROWSER_PROJECT_NAME } from 'lib/constants'
import { CoreDumpManager } from 'lib/coredump' import { CoreDumpManager } from 'lib/coredump'
import { codeManager, engineCommandManager } from 'lib/singletons' import { codeManager, engineCommandManager } from 'lib/singletons'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import useHotkeyWrapper from 'lib/hotkeyWrapper' import useHotkeyWrapper from 'lib/hotkeyWrapper'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { coreDump } from 'lang/wasm' import { coreDump } from 'lang/wasm'
@ -46,7 +47,6 @@ import { reportRejection } from 'lib/trap'
import { RouteProvider } from 'components/RouteProvider' import { RouteProvider } from 'components/RouteProvider'
import { ProjectsContextProvider } from 'components/ProjectsContextProvider' import { ProjectsContextProvider } from 'components/ProjectsContextProvider'
import { OpenInDesktopAppHandler } from 'components/OpenInDesktopAppHandler' import { OpenInDesktopAppHandler } from 'components/OpenInDesktopAppHandler'
import { useToken } from 'machines/appMachine'
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
@ -203,7 +203,8 @@ export const Router = () => {
} }
function CoreDump() { function CoreDump() {
const token = useToken() const { auth } = useSettingsAuthContext()
const token = auth?.context?.token
const coreDumpManager = useMemo( const coreDumpManager = useMemo(
() => new CoreDumpManager(engineCommandManager, codeManager, token), () => new CoreDumpManager(engineCommandManager, codeManager, token),
[] []

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -27,7 +27,6 @@ import { PROJECT_ENTRYPOINT } from 'lib/constants'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
import { codeManager } from 'lib/singletons' import { codeManager } from 'lib/singletons'
import { useToken } from 'machines/appMachine'
function getWorkspaceFolders(): LSP.WorkspaceFolder[] { function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
return [] return []
@ -70,7 +69,8 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const [isKclLspReady, setIsKclLspReady] = useState(false) const [isKclLspReady, setIsKclLspReady] = useState(false)
const [isCopilotLspReady, setIsCopilotLspReady] = useState(false) const [isCopilotLspReady, setIsCopilotLspReady] = useState(false)
const token = useToken() const { auth } = useSettingsAuthContext()
const token = auth?.context.token
const navigate = useNavigate() const navigate = useNavigate()
// So this is a bit weird, we need to initialize the lsp server and client. // 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 { useEngineCommands } from './EngineCommands'
import { Spinner } from './Spinner' import { Spinner } from './Spinner'
import { CustomIcon } from './CustomIcon' import { CustomIcon } from './CustomIcon'
export const ModelStateIndicator = () => { export const ModelStateIndicator = () => {
const [commands] = useEngineCommands() const [commands] = useEngineCommands()
const lastCommandType = commands[commands.length - 1]?.type const lastCommandType = commands[commands.length - 1]?.type
let className = 'w-6 h-6 ' 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 { promptToEditFlow } from 'lib/promptToEdit'
import { kclEditorActor } from 'machines/kclEditorMachine' import { kclEditorActor } from 'machines/kclEditorMachine'
import { commandBarActor } from 'machines/commandBarMachine' import { commandBarActor } from 'machines/commandBarMachine'
import { useToken } from 'machines/appMachine'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -111,6 +110,7 @@ export const ModelingMachineProvider = ({
children: React.ReactNode children: React.ReactNode
}) => { }) => {
const { const {
auth,
settings: { settings: {
context: { context: {
app: { theme, enableSSAO, allowOrbitInSketchMode }, app: { theme, enableSSAO, allowOrbitInSketchMode },
@ -119,7 +119,6 @@ export const ModelingMachineProvider = ({
cameraProjection, cameraProjection,
highlightEdges, highlightEdges,
showScaleGrid, showScaleGrid,
cameraOrbit,
}, },
}, },
}, },
@ -128,7 +127,7 @@ export const ModelingMachineProvider = ({
const navigate = useNavigate() const navigate = useNavigate()
const { context, send: fileMachineSend } = useFileContext() const { context, send: fileMachineSend } = useFileContext()
const { file } = useLoaderData() as IndexLoaderData const { file } = useLoaderData() as IndexLoaderData
const token = useToken() const token = auth?.context?.token
const streamRef = useRef<HTMLDivElement>(null) const streamRef = useRef<HTMLDivElement>(null)
const persistedContext = useMemo(() => getPersistedContext(), []) const persistedContext = useMemo(() => getPersistedContext(), [])
@ -1155,7 +1154,6 @@ export const ModelingMachineProvider = ({
enableSSAO: enableSSAO.current, enableSSAO: enableSSAO.current,
showScaleGrid: showScaleGrid.current, showScaleGrid: showScaleGrid.current,
cameraProjection: cameraProjection.current, cameraProjection: cameraProjection.current,
cameraOrbit: cameraOrbit.current,
}, },
token token
) )
@ -1185,13 +1183,6 @@ export const ModelingMachineProvider = ({
editorManager.selectionRanges = modelingState.context.selectionRanges editorManager.selectionRanges = modelingState.context.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(() => { useEffect(() => {
const onConnectionStateChanged = ({ detail }: CustomEvent) => { const onConnectionStateChanged = ({ detail }: CustomEvent) => {
// If we are in sketch mode we need to exit it. // If we are in sketch mode we need to exit it.

View File

@ -14,7 +14,6 @@ import {
} from '@codemirror/state' } from '@codemirror/state'
import { EditorView } from '@codemirror/view' import { EditorView } from '@codemirror/view'
import { oneDark } from '@codemirror/theme-one-dark' 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 //reference: https://github.com/sachinraja/rodemirror/blob/main/src/use-first-render.ts
const useFirstRender = () => { const useFirstRender = () => {
@ -87,18 +86,6 @@ const CodeEditor = forwardRef<CodeEditorRef, CodeEditorProps>((props, ref) => {
return <div ref={editor}></div> 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) { export function useCodeMirror(props: UseCodeMirror) {
const { const {
onCreateEditor, onCreateEditor,
@ -116,7 +103,7 @@ export function useCodeMirror(props: UseCodeMirror) {
const isFirstRender = useFirstRender() const isFirstRender = useFirstRender()
const targetExtensions = useMemo(() => { const targetExtensions = useMemo(() => {
let exts = isExtensionArray(extensions) ? extensions : [] let exts = Array.isArray(extensions) ? extensions : []
if (theme === 'dark') { if (theme === 'dark') {
exts = [...exts, oneDark] exts = [...exts, oneDark]
} else if (theme === 'light') { } else if (theme === 'light') {

View File

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

View File

@ -33,7 +33,7 @@ export const OpenInDesktopAppHandler = (props: React.PropsWithChildren) => {
function onOpenInDesktopApp() { function onOpenInDesktopApp() {
const newSearchParams = new URLSearchParams(globalThis.location.search) const newSearchParams = new URLSearchParams(globalThis.location.search)
newSearchParams.delete(ASK_TO_OPEN_QUERY_PARAM) 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()}` : ''}` )}${searchParams.size > 0 ? `?${newSearchParams.toString()}` : ''}`

View File

@ -2,7 +2,7 @@ import { FormEvent, useEffect, useRef, useState } from 'react'
import { PATHS } from 'lib/paths' import { PATHS } from 'lib/paths'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { ActionButton } from '../ActionButton' 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 { useHotkeys } from 'react-hotkeys-hook'
import Tooltip from '../Tooltip' import Tooltip from '../Tooltip'
import { DeleteConfirmationDialog } from './DeleteProjectDialog' import { DeleteConfirmationDialog } from './DeleteProjectDialog'
@ -29,7 +29,7 @@ function ProjectCard({
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false) const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
const [numberOfFiles, setNumberOfFiles] = useState(1) const [numberOfFiles, setNumberOfFiles] = useState(1)
const [numberOfFolders, setNumberOfFolders] = useState(0) const [numberOfFolders, setNumberOfFolders] = useState(0)
const [imageUrl, setImageUrl] = useState('') // const [imageUrl, setImageUrl] = useState('')
let inputRef = useRef<HTMLInputElement>(null) let inputRef = useRef<HTMLInputElement>(null)
@ -53,21 +53,18 @@ function ProjectCard({
setNumberOfFolders(project.directory_count) setNumberOfFolders(project.directory_count)
} }
async function setupImageUrl() { // async function setupImageUrl() {
const projectImagePath = window.electron.path.join( // const projectImagePath = await join(project.file.path, PROJECT_IMAGE_NAME)
project.path, // if (await exists(projectImagePath)) {
PROJECT_IMAGE_NAME // const imageData = await readFile(projectImagePath)
) // const blob = new Blob([imageData], { type: 'image/jpg' })
if (await window.electron.exists(projectImagePath)) { // const imageUrl = URL.createObjectURL(blob)
const imageData = await window.electron.readFile(projectImagePath) // setImageUrl(imageUrl)
const blob = new Blob([imageData], { type: 'image/png' }) // }
const imageUrl = URL.createObjectURL(blob) // }
setImageUrl(imageUrl)
}
}
void getNumberOfFiles() void getNumberOfFiles()
void setupImageUrl() // void setupImageUrl()
}, [project.kcl_file_count, project.directory_count]) }, [project.kcl_file_count, project.directory_count])
useEffect(() => { useEffect(() => {
@ -87,7 +84,7 @@ function ProjectCard({
to={`${PATHS.FILE}/${encodeURIComponent(project.default_file)}`} 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" 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 && ( {imageUrl && (
<img <img
src={imageUrl} src={imageUrl}
@ -95,7 +92,7 @@ function ProjectCard({
className="h-full w-full transition-transform group-hover:scale-105 object-cover" 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"> <div className="pb-2 flex flex-col flex-grow flex-auto gap-2 rounded-b-sm">
{isEditing ? ( {isEditing ? (
<ProjectCardRenameForm <ProjectCardRenameForm

View File

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

View File

@ -8,10 +8,10 @@ import Tooltip from './Tooltip'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { reportRejection } from 'lib/trap' import { reportRejection } from 'lib/trap'
import { toSync } from 'lib/utils' import { toSync } from 'lib/utils'
import { useToken } from 'machines/appMachine'
export const RefreshButton = ({ children }: React.PropsWithChildren) => { export const RefreshButton = ({ children }: React.PropsWithChildren) => {
const token = useToken() const { auth } = useSettingsAuthContext()
const token = auth?.context?.token
const coreDumpManager = useMemo( const coreDumpManager = useMemo(
() => new CoreDumpManager(engineCommandManager, codeManager, token), () => 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 { useNavigation, useLocation } from 'react-router-dom'
import { PATHS } from 'lib/paths' import { PATHS } from 'lib/paths'
import { markOnce } from 'lib/performance' import { markOnce } from 'lib/performance'
import { useAuthNavigation } from 'hooks/useAuthNavigation'
export const RouteProviderContext = createContext({}) export const RouteProviderContext = createContext({})
export function RouteProvider({ children }: { children: ReactNode }) { export function RouteProvider({ children }: { children: ReactNode }) {
useAuthNavigation()
const [first, setFirstState] = useState(true) const [first, setFirstState] = useState(true)
const navigation = useNavigation() const navigation = useNavigation()
const location = useLocation() const location = useLocation()

View File

@ -2,7 +2,10 @@ import { trap } from 'lib/trap'
import { useMachine, useSelector } from '@xstate/react' import { useMachine, useSelector } from '@xstate/react'
import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom' import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom'
import { PATHS, BROWSER_PATH } from 'lib/paths' 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 React, { createContext, useEffect, useState } from 'react'
import useStateMachineCommands from '../hooks/useStateMachineCommands'
import { settingsMachine } from 'machines/settingsMachine' import { settingsMachine } from 'machines/settingsMachine'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import { import {
@ -13,6 +16,7 @@ import {
} from 'lib/theme' } from 'lib/theme'
import decamelize from 'decamelize' import decamelize from 'decamelize'
import { Actor, AnyStateMachine, ContextFrom, Prop, StateFrom } from 'xstate' import { Actor, AnyStateMachine, ContextFrom, Prop, StateFrom } from 'xstate'
import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig'
import { import {
kclManager, kclManager,
sceneInfra, sceneInfra,
@ -46,6 +50,7 @@ type MachineContext<T extends AnyStateMachine> = {
} }
type SettingsAuthContextType = { type SettingsAuthContextType = {
auth: MachineContext<typeof authMachine>
settings: MachineContext<typeof settingsMachine> settings: MachineContext<typeof settingsMachine>
} }
@ -365,9 +370,40 @@ export const SettingsAuthProviderBase = ({
) )
}, [settingsState.context.textEditor.blinkingCursor.current]) }, [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 ( return (
<SettingsAuthContext.Provider <SettingsAuthContext.Provider
value={{ value={{
auth: {
state: authState,
context: authState.context,
send: authSend,
},
settings: { settings: {
state: settingsState, state: settingsState,
context: settingsState.context, context: settingsState.context,
@ -381,3 +417,12 @@ export const SettingsAuthProviderBase = ({
} }
export default SettingsAuthProvider 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 { Fragment, useMemo, useState } from 'react'
import { PATHS } from 'lib/paths' import { PATHS } from 'lib/paths'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath' import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import Tooltip from './Tooltip' import Tooltip from './Tooltip'
import usePlatform from 'hooks/usePlatform' import usePlatform from 'hooks/usePlatform'
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
import { CustomIcon } from './CustomIcon' import { CustomIcon } from './CustomIcon'
import { authActor } from 'machines/appMachine'
type User = Models['User_type'] type User = Models['User_type']
@ -20,7 +20,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
const displayedName = getDisplayName(user) const displayedName = getDisplayName(user)
const [imageLoadFailed, setImageLoadFailed] = useState(false) const [imageLoadFailed, setImageLoadFailed] = useState(false)
const navigate = useNavigate() 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. // We filter this memoized list so that no orphan "break" elements are rendered.
const userMenuItems = useMemo<(ActionButtonProps | 'break')[]>( const userMenuItems = useMemo<(ActionButtonProps | 'break')[]>(

View File

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

View File

@ -9,7 +9,6 @@ import {
import { Range, Extension, Text } from '@codemirror/state' import { Range, Extension, Text } from '@codemirror/state'
import { NodeProp, Tree } from '@lezer/common' import { NodeProp, Tree } from '@lezer/common'
import { language, syntaxTree } from '@codemirror/language' import { language, syntaxTree } from '@codemirror/language'
import { isArray } from 'lib/utils'
interface PickerState { interface PickerState {
from: number from: number
@ -80,7 +79,7 @@ function discoverColorsInKCL(
) )
if (maybeWidgetOptions) { if (maybeWidgetOptions) {
if (isArray(maybeWidgetOptions)) { if (Array.isArray(maybeWidgetOptions)) {
console.error('Unexpected nested overlays') console.error('Unexpected nested overlays')
ret.push(...maybeWidgetOptions) ret.push(...maybeWidgetOptions)
} else { } else {
@ -151,7 +150,7 @@ function colorPickersDecorations(
return return
} }
if (!isArray(maybeWidgetOptions)) { if (!Array.isArray(maybeWidgetOptions)) {
widgets.push( widgets.push(
Decoration.widget({ Decoration.widget({
widget: new ColorPickerWidget(maybeWidgetOptions), 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 | undefined
export const VITE_KC_API_BASE_URL = env.VITE_KC_API_BASE_URL as string 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_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_SKIP_AUTH = env.VITE_KC_SKIP_AUTH as string | undefined
export const VITE_KC_CONNECTION_TIMEOUT_MS = export const VITE_KC_CONNECTION_TIMEOUT_MS =
env.VITE_KC_CONNECTION_TIMEOUT_MS as string | undefined 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>, streamRef: React.RefObject<HTMLDivElement>,
modelingSend: ReturnType<typeof useModelingContext>['send'], modelingSend: ReturnType<typeof useModelingContext>['send'],
modelingContext: ReturnType<typeof useModelingContext>['context'], modelingContext: ReturnType<typeof useModelingContext>['context'],
settings: SettingsViaQueryString = { settings = {
pool: null, pool: null,
theme: Themes.System, theme: Themes.System,
highlightEdges: true, highlightEdges: true,
enableSSAO: true, enableSSAO: true,
showScaleGrid: false, showScaleGrid: false,
cameraProjection: 'perspective', cameraProjection: 'perspective',
cameraOrbit: 'spherical', } as SettingsViaQueryString,
},
token?: string token?: string
) { ) {
const networkContext = useNetworkContext() const networkContext = useNetworkContext()

View File

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

View File

@ -21,19 +21,13 @@ import {
FilletParameters, FilletParameters,
ChamferParameters, ChamferParameters,
EdgeTreatmentParameters, EdgeTreatmentParameters,
deleteEdgeTreatment,
} from './addEdgeTreatment' } from './addEdgeTreatment'
import { getNodeFromPath } from '../queryAst' import { getNodeFromPath } from '../queryAst'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils' import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { createLiteral } from 'lang/modifyAst' import { createLiteral } from 'lang/modifyAst'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { Selection, Selections } from 'lib/selections' import { Selection, Selections } from 'lib/selections'
import { import { engineCommandManager, kclManager } from 'lib/singletons'
codeManager,
editorManager,
engineCommandManager,
kclManager,
} from 'lib/singletons'
import { VITE_KC_DEV_TOKEN } from 'env' import { VITE_KC_DEV_TOKEN } from 'env'
import { isOverlap } from 'lib/utils' import { isOverlap } from 'lib/utils'
import { codeRefFromRange } from 'lang/std/artifactGraph' import { codeRefFromRange } from 'lang/std/artifactGraph'
@ -61,13 +55,6 @@ afterAll(() => {
engineCommandManager.tearDown() engineCommandManager.tearDown()
}) })
const dependencies = {
kclManager,
engineCommandManager,
editorManager,
codeManager,
}
const runGetPathToExtrudeForSegmentSelectionTest = async ( const runGetPathToExtrudeForSegmentSelectionTest = async (
code: string, code: string,
selectedSegmentSnippet: string, selectedSegmentSnippet: string,
@ -146,8 +133,7 @@ const runGetPathToExtrudeForSegmentSelectionTest = async (
const pathResult = getPathToExtrudeForSegmentSelection( const pathResult = getPathToExtrudeForSegmentSelection(
ast, ast,
selection, selection,
artifactGraph, artifactGraph
dependencies
) )
if (err(pathResult)) return pathResult if (err(pathResult)) return pathResult
const { pathToExtrudeNode } = pathResult const { pathToExtrudeNode } = pathResult
@ -304,13 +290,8 @@ const runModifyAstCloneWithEdgeTreatmentAndTag = async (
otherSelections: [], otherSelections: [],
} }
// apply edge treatment to selection // apply edge treatment to seleciton
const result = modifyAstWithEdgeTreatmentAndTag( const result = modifyAstWithEdgeTreatmentAndTag(ast, selection, parameters)
ast,
selection,
parameters,
dependencies
)
if (err(result)) { if (err(result)) {
return result return result
} }
@ -320,46 +301,6 @@ const runModifyAstCloneWithEdgeTreatmentAndTag = async (
expect(newCode).toContain(expectedCode) 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 => ({ const createFilletParameters = (radiusValue: number): FilletParameters => ({
type: EdgeTreatmentType.Fillet, type: EdgeTreatmentType.Fillet,
radius: { 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, Identifier,
ObjectExpression, ObjectExpression,
PathToNode, PathToNode,
PipeExpression,
Program, Program,
VariableDeclaration, VariableDeclaration,
VariableDeclarator, VariableDeclarator,
@ -36,14 +35,15 @@ import {
import { err, trap } from 'lib/trap' import { err, trap } from 'lib/trap'
import { Selection, Selections } from 'lib/selections' import { Selection, Selections } from 'lib/selections'
import { KclCommandValue } from 'lib/commandTypes' import { KclCommandValue } from 'lib/commandTypes'
import { isArray } from 'lib/utils'
import { Artifact, getSweepFromSuspectedPath } from 'lang/std/artifactGraph' import { Artifact, getSweepFromSuspectedPath } from 'lang/std/artifactGraph'
import {
kclManager,
engineCommandManager,
editorManager,
codeManager,
} from 'lib/singletons'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
import { findKwArg } from 'lang/util' 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 // Edge Treatment Types
export enum EdgeTreatmentType { export enum EdgeTreatmentType {
@ -65,38 +65,21 @@ export type EdgeTreatmentParameters = ChamferParameters | FilletParameters
export async function applyEdgeTreatmentToSelection( export async function applyEdgeTreatmentToSelection(
ast: Node<Program>, ast: Node<Program>,
selection: Selections, selection: Selections,
parameters: EdgeTreatmentParameters, parameters: EdgeTreatmentParameters
dependencies: {
kclManager: KclManager
engineCommandManager: EngineCommandManager
editorManager: EditorManager
codeManager: CodeManager
}
): Promise<void | Error> { ): Promise<void | Error> {
// 1. clone and modify with edge treatment and tag // 1. clone and modify with edge treatment and tag
const result = modifyAstWithEdgeTreatmentAndTag( const result = modifyAstWithEdgeTreatmentAndTag(ast, selection, parameters)
ast,
selection,
parameters,
dependencies
)
if (err(result)) return result if (err(result)) return result
const { modifiedAst, pathToEdgeTreatmentNode } = result const { modifiedAst, pathToEdgeTreatmentNode } = result
// 2. update ast // 2. update ast
await updateAstAndFocus(modifiedAst, pathToEdgeTreatmentNode, dependencies) await updateAstAndFocus(modifiedAst, pathToEdgeTreatmentNode)
} }
export function modifyAstWithEdgeTreatmentAndTag( export function modifyAstWithEdgeTreatmentAndTag(
ast: Node<Program>, ast: Node<Program>,
selections: Selections, selections: Selections,
parameters: EdgeTreatmentParameters, parameters: EdgeTreatmentParameters
dependencies: {
kclManager: KclManager
engineCommandManager: EngineCommandManager
editorManager: EditorManager
codeManager: CodeManager
}
): ):
| { modifiedAst: Node<Program>; pathToEdgeTreatmentNode: Array<PathToNode> } | { modifiedAst: Node<Program>; pathToEdgeTreatmentNode: Array<PathToNode> }
| Error { | Error {
@ -106,7 +89,7 @@ export function modifyAstWithEdgeTreatmentAndTag(
const astResult = insertParametersIntoAst(clonedAst, parameters) const astResult = insertParametersIntoAst(clonedAst, parameters)
if (err(astResult)) return astResult 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) // Step 1: modify ast with tags and group them by extrude nodes (bodies)
const extrudeToTagsMap: Map< const extrudeToTagsMap: Map<
@ -119,8 +102,7 @@ export function modifyAstWithEdgeTreatmentAndTag(
const result = getPathToExtrudeForSegmentSelection( const result = getPathToExtrudeForSegmentSelection(
clonedAstForGetExtrude, clonedAstForGetExtrude,
selection, selection,
artifactGraph, artifactGraph
dependencies
) )
if (err(result)) return result if (err(result)) return result
const { pathToSegmentNode, pathToExtrudeNode } = result const { pathToSegmentNode, pathToExtrudeNode } = result
@ -276,13 +258,7 @@ function insertParametersIntoAst(
export function getPathToExtrudeForSegmentSelection( export function getPathToExtrudeForSegmentSelection(
ast: Program, ast: Program,
selection: Selection, selection: Selection,
artifactGraph: ArtifactGraph, artifactGraph: ArtifactGraph
dependencies: {
kclManager: KclManager
engineCommandManager: EngineCommandManager
editorManager: EditorManager
codeManager: CodeManager
}
): { pathToSegmentNode: PathToNode; pathToExtrudeNode: PathToNode } | Error { ): { pathToSegmentNode: PathToNode; pathToExtrudeNode: PathToNode } | Error {
const pathToSegmentNode = getNodePathFromSourceRange( const pathToSegmentNode = getNodePathFromSourceRange(
ast, ast,
@ -298,7 +274,7 @@ export function getPathToExtrudeForSegmentSelection(
const sketchVar = varDecNode.node.declaration.id.name const sketchVar = varDecNode.node.declaration.id.name
const sketch = sketchFromKclValue( const sketch = sketchFromKclValue(
dependencies.kclManager.programMemory.get(sketchVar), kclManager.programMemory.get(sketchVar),
sketchVar sketchVar
) )
if (trap(sketch)) return sketch if (trap(sketch)) return sketch
@ -317,28 +293,16 @@ export function getPathToExtrudeForSegmentSelection(
async function updateAstAndFocus( async function updateAstAndFocus(
modifiedAst: Node<Program>, modifiedAst: Node<Program>,
pathToEdgeTreatmentNode: Array<PathToNode>, pathToEdgeTreatmentNode: Array<PathToNode>
dependencies: {
kclManager: KclManager
engineCommandManager: EngineCommandManager
editorManager: EditorManager
codeManager: CodeManager
}
): Promise<void> { ): Promise<void> {
const updatedAst = await dependencies.kclManager.updateAst( const updatedAst = await kclManager.updateAst(modifiedAst, true, {
modifiedAst,
true,
{
focusPath: pathToEdgeTreatmentNode, focusPath: pathToEdgeTreatmentNode,
} })
)
await dependencies.codeManager.updateEditorWithAstAndWriteToFile( await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
updatedAst.newAst
)
if (updatedAst?.selections) { if (updatedAst?.selections) {
dependencies.editorManager.selectRange(updatedAst?.selections) editorManager.selectRange(updatedAst?.selections)
} }
} }
@ -818,142 +782,3 @@ export const isTagUsedInEdgeTreatment = ({
return edges 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, createVariableDeclaration,
} from 'lang/modifyAst' } from 'lang/modifyAst'
import { KCL_DEFAULT_CONSTANT_PREFIXES } from 'lib/constants' 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({ export function addShell({
node, node,
selection, selection,
artifactGraph, artifactGraph,
thickness, thickness,
dependencies,
}: { }: {
node: Node<Program> node: Node<Program>
selection: Selections selection: Selections
artifactGraph: ArtifactGraph artifactGraph: ArtifactGraph
thickness: Expr thickness: Expr
dependencies: {
kclManager: KclManager
engineCommandManager: EngineCommandManager
editorManager: EditorManager
codeManager: CodeManager
}
}): Error | { modifiedAst: Node<Program>; pathToNode: PathToNode } { }): Error | { modifiedAst: Node<Program>; pathToNode: PathToNode } {
const modifiedAst = structuredClone(node) const modifiedAst = structuredClone(node)
@ -53,8 +42,7 @@ export function addShell({
const extrudeLookupResult = getPathToExtrudeForSegmentSelection( const extrudeLookupResult = getPathToExtrudeForSegmentSelection(
clonedAstForGetExtrude, clonedAstForGetExtrude,
graphSelection, graphSelection,
artifactGraph, artifactGraph
dependencies
) )
if (err(extrudeLookupResult)) { if (err(extrudeLookupResult)) {
return new Error("Couldn't find extrude") return new Error("Couldn't find extrude")

View File

@ -5,11 +5,7 @@ import {
PathToNode, PathToNode,
Identifier, Identifier,
topLevelRange, topLevelRange,
PipeExpression,
CallExpression,
VariableDeclarator,
} from './wasm' } from './wasm'
import { ProgramMemory } from 'lang/wasm'
import { import {
findAllPreviousVariables, findAllPreviousVariables,
isNodeSafeToReplace, isNodeSafeToReplace,
@ -29,11 +25,9 @@ import {
createCallExpression, createCallExpression,
createLiteral, createLiteral,
createPipeSubstitution, createPipeSubstitution,
createCallExpressionStdLib,
} from './modifyAst' } from './modifyAst'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { codeRefFromRange } from './std/artifactGraph' import { codeRefFromRange } from './std/artifactGraph'
import { addCallExpressionsToPipe, addCloseToPipe } from 'lang/std/sketch'
beforeAll(async () => { beforeAll(async () => {
await initPromise await initPromise
@ -686,115 +680,3 @@ myNestedVar = [
expect(pathToNode).toEqual(pathToNode2) 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, topLevelRange,
VariableDeclaration, VariableDeclaration,
VariableDeclarator, VariableDeclarator,
recast,
} from './wasm' } from './wasm'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils' import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { createIdentifier, splitPathAtLastIndex } from './modifyAst' import { createIdentifier, splitPathAtLastIndex } from './modifyAst'
import { getSketchSegmentFromSourceRange } from './std/sketchConstraints' 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 { ARG_TAG, getArgForEnd, getFirstArg } from './std/sketch'
import { import {
getConstraintLevelFromSourceRange, getConstraintLevelFromSourceRange,
@ -80,28 +79,7 @@ export function getNodeFromPath<T>(
deepPath: successfulPaths, deepPath: successfulPaths,
} }
} }
const stackTraceError = new Error() return new Error('not an object')
const sourceCode = recast(node)
const levels = stackTraceError.stack?.split('\n')
const aFewFunctionNames: string[] = []
let tree = ''
levels?.forEach((val, index) => {
const fnName = val.trim().split(' ')[1]
const ending = index === levels.length - 1 ? ' ' : ' > '
tree += fnName + ending
if (index < 3) {
aFewFunctionNames.push(fnName)
}
})
const error = new Error(
`Failed to stopAt ${stopAt}, ${aFewFunctionNames
.filter((a) => a)
.join(' > ')}`
)
console.error(tree)
console.error(sourceCode)
console.error(error.stack)
return error
} }
parent = currentNode parent = currentNode
parentEdge = pathItem[0] parentEdge = pathItem[0]
@ -112,7 +90,7 @@ export function getNodeFromPath<T>(
} }
if ( if (
typeof stopAt !== 'undefined' && typeof stopAt !== 'undefined' &&
(isArray(stopAt) (Array.isArray(stopAt)
? stopAt.includes(currentNode.type) ? stopAt.includes(currentNode.type)
: currentNode.type === stopAt) : currentNode.type === stopAt)
) { ) {
@ -167,7 +145,6 @@ export function getNodeFromPathCurry(
type KCLNode = Node< type KCLNode = Node<
| Expr | Expr
| ExpressionStatement | ExpressionStatement
| ImportStatement
| VariableDeclaration | VariableDeclaration
| VariableDeclarator | VariableDeclarator
| ReturnStatement | ReturnStatement
@ -264,14 +241,10 @@ export function traverse(
// hmm this smell // hmm this smell
_traverse(_node.object, [...pathToNode, ['object', 'MemberExpression']]) _traverse(_node.object, [...pathToNode, ['object', 'MemberExpression']])
_traverse(_node.property, [...pathToNode, ['property', 'MemberExpression']]) _traverse(_node.property, [...pathToNode, ['property', 'MemberExpression']])
} else if (_node.type === 'ImportStatement') { } else if ('body' in _node && Array.isArray(_node.body)) {
// Do nothing. _node.body.forEach((expression, index) =>
} else if ('body' in _node && isArray(_node.body)) {
// TODO: Program should have a type field, but it currently doesn't.
const program = node as Node<Program>
program.body.forEach((expression, index) => {
_traverse(expression, [...pathToNode, ['body', ''], [index, 'index']]) _traverse(expression, [...pathToNode, ['body', ''], [index, 'index']])
}) )
} }
option?.leave?.(_node) option?.leave?.(_node)
} }

View File

@ -248,8 +248,6 @@ class EngineConnection extends EventTarget {
mediaStream?: MediaStream mediaStream?: MediaStream
idleMode: boolean = false idleMode: boolean = false
promise?: Promise<void> promise?: Promise<void>
sdpAnswer?: Models['RtcSessionDescription_type']
triggeredStart = false
onIceCandidate = function ( onIceCandidate = function (
this: RTCPeerConnection, this: RTCPeerConnection,
@ -555,7 +553,6 @@ class EngineConnection extends EventTarget {
* did not establish. * did not establish.
*/ */
connect(reconnecting?: boolean): Promise<void> { connect(reconnecting?: boolean): Promise<void> {
const that = this
return new Promise((resolve) => { return new Promise((resolve) => {
if (this.isConnecting() || this.isReady()) { if (this.isConnecting() || this.isReady()) {
return 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) => { 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) { if (event.candidate === null) {
initiateConnectingExclusive()
return return
} }
@ -628,6 +595,7 @@ class EngineConnection extends EventTarget {
}, },
} }
// Request a candidate to use
this.send({ this.send({
type: 'trickle_ice', type: 'trickle_ice',
candidate: { candidate: {
@ -637,38 +605,8 @@ class EngineConnection extends EventTarget {
usernameFragment: event.candidate.usernameFragment || undefined, 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?.('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) => { this.onIceCandidateError = (_event: Event) => {
const event = _event as RTCPeerConnectionIceErrorEvent const event = _event as RTCPeerConnectionIceErrorEvent
@ -696,8 +634,6 @@ class EngineConnection extends EventTarget {
}) })
) )
break break
case 'connecting':
break
case 'disconnected': case 'disconnected':
case 'failed': case 'failed':
this.pc?.removeEventListener('icecandidate', this.onIceCandidate) 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 break
case 'trickle_ice': case 'trickle_ice':
@ -1282,7 +1235,6 @@ class EngineConnection extends EventTarget {
if (closedPc && closedUDC && closedWS) { if (closedPc && closedUDC && closedWS) {
// Do not notify the rest of the program that we have cut off anything. // Do not notify the rest of the program that we have cut off anything.
this.state = { type: EngineConnectionStateType.Disconnected } this.state = { type: EngineConnectionStateType.Disconnected }
this.triggeredStart = false
} }
} }
} }
@ -1437,7 +1389,6 @@ export class EngineCommandManager extends EventTarget {
enableSSAO: true, enableSSAO: true,
showScaleGrid: false, showScaleGrid: false,
cameraProjection: 'perspective', cameraProjection: 'perspective',
cameraOrbit: 'spherical',
} }
} }
@ -1486,7 +1437,6 @@ export class EngineCommandManager extends EventTarget {
enableSSAO: true, enableSSAO: true,
showScaleGrid: false, showScaleGrid: false,
cameraProjection: 'orthographic', cameraProjection: 'orthographic',
cameraOrbit: 'spherical',
}, },
// When passed, use a completely separate connecting code path that simply // When passed, use a completely separate connecting code path that simply
// opens a websocket and this is a function that is called when connected. // opens a websocket and this is a function that is called when connected.
@ -2049,7 +1999,7 @@ export class EngineCommandManager extends EventTarget {
.catch((e) => { .catch((e) => {
// TODO: Previously was never caught, we are not rejecting these pendingCommands but this needs to be handled at some point. // TODO: Previously was never caught, we are not rejecting these pendingCommands but this needs to be handled at some point.
/*noop*/ /*noop*/
return e return null
}) })
} }
/** /**

View File

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

View File

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

View File

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

View File

@ -18,7 +18,6 @@ import {
default_project_settings, default_project_settings,
base64_decode, base64_decode,
clear_scene_and_bust_cache, clear_scene_and_bust_cache,
change_kcl_settings,
reloadModule, reloadModule,
} from 'lib/wasm_lib_wrapper' } 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 { Artifact } from './std/artifactGraph'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils' import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { NumericSuffix } from 'wasm-lib/kcl/bindings/NumericSuffix' 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 { Artifact } from 'wasm-lib/kcl/bindings/Artifact'
export type { ArtifactCommand } 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 return range[2] === 0
} }
function firstSourceRange(error: RustKclError): SourceRange {
return error.sourceRanges.length > 0
? sourceRangeFromRust(error.sourceRanges[0])
: defaultSourceRange()
}
export const wasmUrl = () => { export const wasmUrl = () => {
// For when we're in electron (file based) or web server (network based) // 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 // 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( return new KCLError(
parsed.kind, parsed.kind,
parsed.msg, parsed.msg,
firstSourceRange(parsed), sourceRangeFromRust(parsed.sourceRanges[0]),
[], [],
[], [],
defaultArtifactGraph() defaultArtifactGraph()
@ -628,7 +620,7 @@ export const executor = async (
const kclError = new KCLError( const kclError = new KCLError(
parsed.error.kind, parsed.error.kind,
parsed.error.msg, parsed.error.msg,
firstSourceRange(parsed.error), sourceRangeFromRust(parsed.error.sourceRanges[0]),
parsed.operations, parsed.operations,
parsed.artifactCommands, parsed.artifactCommands,
rustArtifactGraphToMap(parsed.artifactGraph) rustArtifactGraphToMap(parsed.artifactGraph)
@ -697,7 +689,7 @@ export const modifyAstForSketch = async (
const kclError = new KCLError( const kclError = new KCLError(
parsed.kind, parsed.kind,
parsed.msg, parsed.msg,
firstSourceRange(parsed), sourceRangeFromRust(parsed.sourceRanges[0]),
[], [],
[], [],
defaultArtifactGraph() defaultArtifactGraph()
@ -768,7 +760,7 @@ export function programMemoryInit(): ProgramMemory | Error {
return new KCLError( return new KCLError(
parsed.kind, parsed.kind,
parsed.msg, parsed.msg,
firstSourceRange(parsed), sourceRangeFromRust(parsed.sourceRanges[0]),
[], [],
[], [],
defaultArtifactGraph() defaultArtifactGraph()
@ -856,17 +848,3 @@ export function base64Decode(base64: string): ArrayBuffer | Error {
return new Error('Caught error decoding base64 string: ' + e) 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 { StateMachineCommandSetConfig } from 'lib/commandTypes'
import { authActor } from 'machines/appMachine' import { authMachine } from 'machines/authMachine'
import { ACTOR_IDS } from 'machines/machineConstants'
export const authCommands: Command[] = [ type AuthCommandSchema = {}
{
groupId: ACTOR_IDS.AUTH, export const authCommandBarConfig: StateMachineCommandSetConfig<
name: 'log-out', typeof authMachine,
displayName: 'Log out', AuthCommandSchema
icon: 'arrowLeft', > = {
needsReview: false, 'Log in': {
onSubmit: () => authActor.send({ type: 'Log out' }), hide: 'both',
}, },
] 'Log out': {
args: [],
icon: 'arrowLeft',
},
}

View File

@ -308,6 +308,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
description: description:
'Create a 3D body by moving a sketch region along an arbitrary path.', 'Create a 3D body by moving a sketch region along an arbitrary path.',
icon: 'sweep', icon: 'sweep',
status: 'development',
needsReview: false, needsReview: false,
args: { args: {
target: { target: {
@ -316,6 +317,8 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
required: true, required: true,
skip: true, skip: true,
multiple: false, multiple: false,
warningMessage:
'The sweep workflow is new and under tested. Please break it and report issues.',
}, },
trajectory: { trajectory: {
inputType: 'selection', inputType: 'selection',
@ -365,6 +368,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
Revolve: { Revolve: {
description: 'Create a 3D body by rotating a sketch region about an axis.', description: 'Create a 3D body by rotating a sketch region about an axis.',
icon: 'revolve', icon: 'revolve',
status: 'development',
needsReview: true, needsReview: true,
args: { args: {
selection: { selection: {
@ -373,6 +377,8 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
multiple: false, // TODO: multiple selection multiple: false, // TODO: multiple selection
required: true, required: true,
skip: true, skip: true,
warningMessage:
'The revolve workflow is new and under tested. Please break it and report issues.',
}, },
axisOrEdge: { axisOrEdge: {
inputType: 'options', 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 { uuidv4 } from 'lib/utils'
import { CommandBarContext } from 'machines/commandBarMachine' import { CommandBarContext } from 'machines/commandBarMachine'
import { Selections } from 'lib/selections' import { Selections } from 'lib/selections'
import { ApiError_type } from '@kittycad/lib/dist/types/src/models'
export const disableDryRunWithRetry = async (numberOfRetries = 3) => { export const disableDryRunWithRetry = async (numberOfRetries = 3) => {
for (let tries = 0; tries < numberOfRetries; tries++) { 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 ({ export const revolveAxisValidator = async ({
data, data,
context, context,
@ -98,7 +83,7 @@ export const revolveAxisValidator = async ({
value: 360, value: 360,
} }
const command = async () => { const revolveAboutEdgeCommand = async () => {
return await engineCommandManager.sendSceneCommand({ return await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd_id: uuidv4(), cmd_id: uuidv4(),
@ -107,18 +92,17 @@ export const revolveAxisValidator = async ({
angle: angleInDegrees, angle: angleInDegrees,
edge_id: edgeSelection, edge_id: edgeSelection,
target: sketchSelection, target: sketchSelection,
// Gotcha: Playwright will fail with larger tolerances, need to use a smaller one. tolerance: 0.0001,
tolerance: 1e-7,
}, },
}) })
} }
const result = await dryRunWrapper(command) const attemptRevolve = await dryRunWrapper(revolveAboutEdgeCommand)
if (result?.success) { if (attemptRevolve?.success) {
return true 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 ({ export const loftValidator = async ({
@ -144,7 +128,7 @@ export const loftValidator = async ({
return 'Unable to loft, selection contains less than two solid2ds' return 'Unable to loft, selection contains less than two solid2ds'
} }
const command = async () => { const loftCommand = async () => {
// TODO: check what to do with these // TODO: check what to do with these
const DEFAULT_V_DEGREE = 2 const DEFAULT_V_DEGREE = 2
const DEFAULT_TOLERANCE = 2 const DEFAULT_TOLERANCE = 2
@ -161,13 +145,13 @@ export const loftValidator = async ({
}, },
}) })
} }
const result = await dryRunWrapper(command) const attempt = await dryRunWrapper(loftCommand)
if (result?.success) { if (attempt?.success) {
return true 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 ({ export const shellValidator = async ({
@ -196,7 +180,7 @@ export const shellValidator = async ({
return "Unable to shell, couldn't find the solid" 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 // TODO: figure out something better than an arbitrarily small value
const DEFAULT_THICKNESS: Models['LengthUnit_type'] = 1e-9 const DEFAULT_THICKNESS: Models['LengthUnit_type'] = 1e-9
const DEFAULT_HOLLOW = false const DEFAULT_HOLLOW = false
@ -216,13 +200,12 @@ export const shellValidator = async ({
}) })
} }
const result = await dryRunWrapper(command) const attemptShell = await dryRunWrapper(shellCommand)
if (result?.success) { if (attemptShell?.success) {
return true return true
} }
const reason = parseEngineErrorMessage(result) || 'unknown' return 'Unable to shell with the provided selection'
return `Unable to shell with the current selection. Reason: ${reason}`
} }
export const sweepValidator = async ({ export const sweepValidator = async ({
@ -258,7 +241,7 @@ export const sweepValidator = async ({
} }
const target = targetArtifact.pathId const target = targetArtifact.pathId
const command = async () => { const sweepCommand = async () => {
// TODO: second look on defaults here // TODO: second look on defaults here
const DEFAULT_TOLERANCE: Models['LengthUnit_type'] = 1e-7 const DEFAULT_TOLERANCE: Models['LengthUnit_type'] = 1e-7
const DEFAULT_SECTIONAL = false const DEFAULT_SECTIONAL = false
@ -278,11 +261,10 @@ export const sweepValidator = async ({
}) })
} }
const result = await dryRunWrapper(command) const attemptSweep = await dryRunWrapper(sweepCommand)
if (result?.success) { if (attemptSweep?.success) {
return true return true
} }
const reason = parseEngineErrorMessage(result) || 'unknown' return 'Unable to sweep with the provided selection'
return `Unable to sweep with the current selection. Reason: ${reason}`
} }

View File

@ -26,7 +26,7 @@ export const FILE_EXT = '.kcl'
/** Default file to open when a project is opened */ /** Default file to open when a project is opened */
export const PROJECT_ENTRYPOINT = `main${FILE_EXT}` as const export const PROJECT_ENTRYPOINT = `main${FILE_EXT}` as const
/** Thumbnail file name */ /** 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 */ /** The localStorage key for last-opened projects */
export const FILE_PERSIST_KEY = `${PROJECT_FOLDER}-last-opened` as const export const FILE_PERSIST_KEY = `${PROJECT_FOLDER}-last-opened` as const
/** The default name given to new kcl files in a project */ /** 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 */ /** localStorage key for the playwright test-specific app settings file */
export const TEST_SETTINGS_FILE_KEY = 'playwright-test-settings' 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 SETTINGS_FILE_NAME = 'settings.toml'
export const TOKEN_FILE_NAME = 'token.txt' export const TOKEN_FILE_NAME = 'token.txt'
export const PROJECT_SETTINGS_FILE_NAME = 'project.toml' export const PROJECT_SETTINGS_FILE_NAME = 'project.toml'
@ -143,7 +145,7 @@ export const VIEW_NAMES_SEMANTIC = {
export const SIDEBAR_BUTTON_SUFFIX = '-pane-button' export const SIDEBAR_BUTTON_SUFFIX = '-pane-button'
/** Custom URL protocol our desktop registers */ /** 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 * A query parameter that triggers a modal

View File

@ -10,7 +10,6 @@ import {
import { import {
PROJECT_ENTRYPOINT, PROJECT_ENTRYPOINT,
PROJECT_FOLDER, PROJECT_FOLDER,
PROJECT_IMAGE_NAME,
PROJECT_SETTINGS_FILE_NAME, PROJECT_SETTINGS_FILE_NAME,
SETTINGS_FILE_NAME, SETTINGS_FILE_NAME,
TELEMETRY_FILE_NAME, TELEMETRY_FILE_NAME,
@ -626,19 +625,3 @@ export const getUser = async (
} }
return Promise.reject(new Error('unreachable')) 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 // Sketch the bend
filletExtrude = extrude(filletSketch, length = -width) filletExtrude = extrude(-width, filletSketch)
// Create a custom plane for the leg that sits on the wall // Create a custom plane for the leg that sits on the wall
customPlane = { customPlane = {
@ -102,7 +102,7 @@ bracketLeg2Sketch = startSketchOn(customPlane)
}, %), %) }, %), %)
// Extrude the second leg // Extrude the second leg
bracketLeg2Extrude = extrude(bracketLeg2Sketch, length = -thickness) bracketLeg2Extrude = extrude(-thickness, bracketLeg2Sketch)
|> fillet({ |> fillet({
radius = extFilletRadius, radius = extFilletRadius,
tags = [ tags = [

View File

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

View File

@ -1,4 +1,3 @@
import { VITE_KC_SITE_APP_URL } from 'env'
import { createCreateFileUrl } from './links' import { createCreateFileUrl } from './links'
describe(`link creation tests`, () => { describe(`link creation tests`, () => {
@ -9,7 +8,7 @@ describe(`link creation tests`, () => {
// Converted with external online tools // Converted with external online tools
const expectedEncodedCode = `ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg%3D%3D` 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 }) const result = createCreateFileUrl({ code, name, units })
expect(result.toString()).toBe(expectedLink) expect(result.toString()).toBe(expectedLink)

View File

@ -1,7 +1,11 @@
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models' 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 { 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 toast from 'react-hot-toast'
import { err } from './trap' import { err } from './trap'
export interface FileLinkParams { export interface FileLinkParams {
@ -47,7 +51,8 @@ export async function copyFileShareLink(
* open the URL in the desktop app. * open the URL in the desktop app.
*/ */
export function createCreateFileUrl({ code, name, units }: FileLinkParams) { 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({ const searchParams = new URLSearchParams({
[CREATE_FILE_URL_PARAM]: String(true), [CREATE_FILE_URL_PARAM]: String(true),
name, name,

View File

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

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