Compare commits

...

41 Commits

Author SHA1 Message Date
efe8089b08 Revert multi-profile (#4812)
* Revert "multi-profile follow up. (#4802)"

This reverts commit 2b2ed470c1.

* Revert "multi profile (#4532)"

This reverts commit 04e586d07b.

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

* Re-run CI after snapshots

* Re-run CI after snapshots

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

* Re-run CI after snapshots

* Add `fixme` to onboarding test

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-16 10:34:11 -05:00
49de3b0ac9 get ready to bump (kcl-lib and friends) world (#4794)
get ready to bump world

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-12-16 18:37:03 +11:00
2b2ed470c1 multi-profile follow up. (#4802)
* multi-profile work

* fix enter sketch on cap

* fix coderef problem for walls and caps

* allow sketch mode entry from circle

* clean up

* update snapshot

* Look at this (photo)Graph *in the voice of Nickelback*

* trigger CI

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

* add test

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

* fix how expression index is corrected, to make compatible with offset planes

* another test

* tweak test

* more test tweaks

* break up test to fix it hopfully

* fix onboarding test

* remove bad comment

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-16 18:36:48 +11:00
96652a0c48 Fix onboarding rendering (#4789)
* fix onboarding rendering

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

* updates

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

* updates

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

* empty string

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

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

* updates

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

* updates

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

* updates

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

* updates

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

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

* empty

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

* empty

* can be off by 20

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

* can be off by 20

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-14 01:35:34 +00:00
04e586d07b multi profile (#4532)
* multi-profile work

* another test

* clean up

* cover a quirk with a test

* last of tests

* fix typos

* Fix source range in snap test

---------

Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2024-12-13 17:57:33 -05:00
fe5f574a77 Revert "Fix so that tag declarators can be used as parameters (#4692)" (#4788)
This reverts commit e27840219b.
2024-12-13 21:39:40 +00:00
e787495ad0 add a test to make sure we cant shebang in a fn (#4781)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-12-13 20:10:33 +00:00
8bb9be7a5e Bump kittycad-modeling-cmds (#4777) 2024-12-13 19:42:41 +00:00
00892464e8 Always run cargo test in CI so that it can be required (#4786) 2024-12-13 14:34:23 -05:00
05ed2a3367 Loft uses kw arguments (#4757)
Part of #4600
2024-12-13 13:07:52 -06:00
10cc5bce59 Fix SourceRange values in OrderedCommands to match the TS type (#4785)
* Fix SourceRange type to match WASM commands

* Update artifact graph test snap

* Update artifact graph test
2024-12-13 19:03:24 +00:00
a32f150fc1 KCL tests: Update engine API snapshot (#4784)
When engine merged their big extrude ID bugfix,
they changed the response for extrudes on face.

Basically there's no longer a start face for
something you extrude from a face, because there
just isn't. There's a hole in a previous face,
it's just a gap.

Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2024-12-13 11:24:29 -06:00
ac60082e67 Fix ids for kurt so front end re-uses same ones on executions (#4780)
* updates

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

* updates

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

* working test;

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

* fix tests

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

* Update src/wasm-lib/tests/executor/main.rs

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

* Update src/wasm-lib/tests/executor/main.rs

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

* fix race condition

* fix whoopsie

* fix tsc

* for some dumb ass reason the model executes twice on load

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
2024-12-13 02:06:26 +00:00
d44dc1b21a Bump wasm-bindgen from 0.2.91 to 0.2.99 and wasm-bindgen-futures from… (#4732)
* Bump wasm-bindgen from 0.2.91 to 0.2.99 and wasm-bindgen-futures from 0.4.44 to 0.4.49

* Upgrade in the kcl crate also

* Update web-sys version constraint to match lock
2024-12-12 15:37:50 -06:00
813962ea4c Revert "warn on unneccessary brackets (#4769)" (#4776)
This reverts commit 4b6bbbe2c5 (PR #4769) because these tests are failing:

        FAIL [   0.013s] kcl-lib parsing::parser::tests::assign_brackets
        FAIL [   0.012s] kcl-lib parsing::parser::tests::test_arg
2024-12-12 15:37:37 -06:00
738443a6ab add test that ensures we use the cache, from playwright (#4721)
add test that ensures we use the cache;

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-12-12 15:37:23 -06:00
4b6bbbe2c5 warn on unneccessary brackets (#4769)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-12-13 08:20:57 +13:00
6ff8addc8b Remove non code from Digests (#4772)
Remove non code from Digests

@jessfraz and I talked it over; for the time being we're going to remove
comments from the AST digest. We already exclude source position, so
this is just increasing the degree to which we're going to ignore things
that are not germane to execution.

Before, we'd digest *some* but not all of the comments in the AST.
Silly, I know, right?

So, this code:

```
firstSketch = startSketchOn('XY')
  |> startProfileAt([-12, 12], %)
  |> line([-24, 0], %) // my thing
  |> close(%)
  |> extrude(6, %)
```

Would digest differently than:

```
firstSketch = startSketchOn('XY')
  |> startProfileAt([-12, 12], %)
  |> line([-24, 0], %)
  |> close(%)
  |> extrude(6, %)
```

Which is wrong. We've fully divested of hashing code comments, so this
will now hash to be the same. Hooray.
2024-12-12 18:55:09 +00:00
da05c38b9e KCL stdlib: Add atan2 function (#4771)
At Lee's request
2024-12-12 18:11:07 +00:00
191b9b71fd KCL: Keyword fn args like "x = 1" not like "x: 1" (#4770)
Aligns with how we're doing objects.
2024-12-12 17:53:35 +00:00
05163fdded Fix KCL warnings in doc comments from let, const, and new fn syntax (#4756)
* Fix KCL warnings in doc comments from let, const, and new fn syntax

* Update docs
2024-12-12 11:33:37 -05:00
7ed26e21c6 More Walk cleanup (#4738)
* More Walk cleanup

 - The `Node` type contained two enums by mistake. Those have been
   removed.

 - Export the `Visitor` and `Visitable` traits, as I start to migrate
   stuff to them.

 - Add a wrapper to pull the `digest` off the node without doing a
   `match` elsewhere.
2024-12-12 01:49:18 +00:00
c668d40efc make pipe have a hole (#4766)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-12-12 01:07:14 +00:00
f38c6b90b7 Color picker in the code pane (#4761)
* add color plugin

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

* fixes

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

* fmt

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

* snapshot test goober

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

* updates

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

* updates

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

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

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-12 00:45:39 +00:00
7bc8bae0ec Update Camera Controls to Zoo (#4755)
* update camera controls to Zoo

* update e2e and initial settings

* update types and camera controls ts

* update mod.rs test

* update test, test locally
2024-12-11 15:03:51 -08:00
3804aca27e Bump codecov/codecov-action from 4 to 5 (#4498)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 5.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-11 14:19:43 -08:00
b127680f2f Remove type coercion (#4759)
remove type coercion
2024-12-11 22:04:36 +00:00
b7de8e60cf Sweep in kcl (#4754)
* updates

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

* updates

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

* updates

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

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

* updates

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

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

* empty

* Update src/wasm-lib/kcl/src/docs/mod.rs

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

* updates

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

* updates

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

* updates

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

* updates

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2024-12-11 20:59:02 +00:00
058fccb5e1 Add a right-click menu to the stream, but only when not dragging (#4745)
* Refactor ContextMenu to be able to take a guard and other event types

* refactor: break out ViewControlMenu into its own component

* Add ViewControlMenu to Stream, but only on right-click non-drag mouseup

* Fix lints

* Don't use `useCallback` for contextmenu guard

* Update context menu position on subsequent right-clicks
2024-12-11 17:57:38 +00:00
00e97257ae Set disableDifferentialDownload to true for the auto updater (#4742)
Fixes #4120
2024-12-11 09:04:02 -06:00
aeb656d176 Remove flags we had for code sign on Cut Release PRs (#4728) 2024-12-11 09:03:12 -06:00
ac49ebd6e0 Revert "chore: implemented multiple instances instead of multiple appications?" (#4750)
Revert "chore: implemented multiple instances instead of multiple appications…"

This reverts commit 548c664db0.
2024-12-11 08:58:42 -06:00
b40f03ad25 Fix wasm init deprecation warning (#4747)
See `__wbg_init()` in `src/wasm-lib/pkg/wasm_lib.js`.
2024-12-11 04:54:20 -05:00
a8ad86e645 Add some comments to new API in wasm.ts (#4712)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-12-11 21:37:03 +13:00
87f50cd5e9 Implement as aliases for sub-expressions (#4723)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-12-11 21:26:42 +13:00
0400e6228e Add a "current" marker to UnitsMenu (#4744) 2024-12-11 06:00:19 +00:00
26f150fd6c Change diagnostic action button to primary color (#4737) 2024-12-11 00:25:43 -05:00
3049f405f5 Reapply "More aggressive using of cache on engine settings changes" (#4736)
* Reapply "More aggressive using of cache on engine settings changes (#4691)" (#4729)

This reverts commit 3f1f40eeba.

* Add a utility to get all the current values from the settings object

* Use an XState selector to get the latest settings snapshot for WASM

---------

Co-authored-by: Frank Noirot <frank@kittycad.io>
2024-12-11 02:50:22 +00:00
53d40301dc start of Appearance function (#4743)
* initial commit

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

* updates

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

* updates

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

* updates

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

* fix docs

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

* updates

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

* updates

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

* add more samples

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

* updates

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

* updatres

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

* regenerate docs

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

* updates

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

* patterns and appearance samples

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

* updates

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

* updates

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

* fmt

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-12-11 01:51:51 +00:00
671c01e36f More consistent GitHub links (#4741)
Fixes #4726
Tested locally with regular and nightly (`yarn files:flip-to-nightly`) configs.
2024-12-10 15:52:57 -06:00
e80151979b Pin electron versions and remove unused packages (#4708)
* Remove forge dependencies for packaging
Fixes #4628

* More clean up and pin current electron versions
2024-12-10 16:37:08 -05:00
194 changed files with 15366 additions and 3857 deletions

View File

@ -165,7 +165,6 @@ jobs:
- name: Build the app (release)
if: ${{ env.IS_RELEASE == 'true' || env.IS_NIGHTLY == 'true' }}
env:
PUBLISH_FOR_PULL_REQUEST: true
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
@ -173,7 +172,6 @@ jobs:
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
CSC_FOR_PULL_REQUEST: true
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
run: yarn electron-builder --config --publish always
@ -229,7 +227,6 @@ jobs:
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
CSC_FOR_PULL_REQUEST: true
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
run: yarn electron-builder --config --publish always

View File

@ -2,28 +2,8 @@ on:
push:
branches:
- main
paths:
- 'src/wasm-lib/**.rs'
- 'src/wasm-lib/**.hbs'
- 'src/wasm-lib/**.gen'
- 'src/wasm-lib/**.snap'
- '**/Cargo.toml'
- '**/Cargo.lock'
- '**/rust-toolchain.toml'
- 'src/wasm-lib/**.kcl'
- .github/workflows/cargo-test.yml
pull_request:
paths:
- 'src/wasm-lib/**.rs'
- 'src/wasm-lib/**.hbs'
- 'src/wasm-lib/**.gen'
- 'src/wasm-lib/**.snap'
- '**/Cargo.toml'
- '**/Cargo.lock'
- '**/rust-toolchain.toml'
- 'src/wasm-lib/**.kcl'
- .github/workflows/cargo-test.yml
workflow_dispatch:
permissions: read-all
concurrency:
@ -71,7 +51,7 @@ jobs:
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}
RUST_MIN_STACK: 10485760000
- name: Upload to codecov.io
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
token: ${{secrets.CODECOV_TOKEN}}
fail_ci_if_error: true

View File

@ -22,3 +22,5 @@ once fixed in engine will just start working here with no language changes.
- **Chamfers**: Chamfers cannot intersect, you will get an error. Only simple
chamfer cases work currently.
- **Appearance**: Changing the appearance on a loft does not work.

239
docs/kcl/appearance.md Normal file

File diff suppressed because one or more lines are too long

49
docs/kcl/atan2.md Normal file

File diff suppressed because one or more lines are too long

View File

@ -19,6 +19,7 @@ layout: manual
* [`angledLineThatIntersects`](kcl/angledLineThatIntersects)
* [`angledLineToX`](kcl/angledLineToX)
* [`angledLineToY`](kcl/angledLineToY)
* [`appearance`](kcl/appearance)
* [`arc`](kcl/arc)
* [`arcTo`](kcl/arcTo)
* [`asin`](kcl/asin)
@ -29,6 +30,7 @@ layout: manual
* [`assertLessThan`](kcl/assertLessThan)
* [`assertLessThanOrEq`](kcl/assertLessThanOrEq)
* [`atan`](kcl/atan)
* [`atan2`](kcl/atan2)
* [`bezierCurve`](kcl/bezierCurve)
* [`ceil`](kcl/ceil)
* [`chamfer`](kcl/chamfer)
@ -101,6 +103,7 @@ layout: manual
* [`startProfileAt`](kcl/startProfileAt)
* [`startSketchAt`](kcl/startSketchAt)
* [`startSketchOn`](kcl/startSketchOn)
* [`sweep`](kcl/sweep)
* [`tan`](kcl/tan)
* [`tangentToEnd`](kcl/tangentToEnd)
* [`tangentialArc`](kcl/tangentialArc)

File diff suppressed because one or more lines are too long

View File

@ -43,7 +43,7 @@ fn sum(arr) {
/* The above is basically like this pseudo-code:
fn sum(arr):
let sumSoFar = 0
sumSoFar = 0
for i in arr:
sumSoFar = add(sumSoFar, i)
return sumSoFar */
@ -96,14 +96,14 @@ fn decagon(radius) {
/* The `decagon` above is basically like this pseudo-code:
fn decagon(radius):
let stepAngle = (1/10) * tau()
let startOfDecagonSketch = startSketchAt([(cos(0)*radius), (sin(0) * radius)])
stepAngle = (1/10) * tau()
startOfDecagonSketch = startSketchAt([(cos(0)*radius), (sin(0) * radius)])
// Here's the reduce part.
let partialDecagon = startOfDecagonSketch
partialDecagon = startOfDecagonSketch
for i in [1..10]:
let x = cos(stepAngle * i) * radius
let y = sin(stepAngle * i) * radius
x = cos(stepAngle * i) * radius
y = sin(stepAngle * i) * radius
partialDecagon = lineTo([x, y], partialDecagon)
fullDecagon = partialDecagon // it's now full
return fullDecagon */

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

55
docs/kcl/sweep.md Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,23 @@
---
title: "AppearanceData"
excerpt: "Data for appearance."
layout: manual
---
Data for appearance.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `color` |`string`| Color of the new material, a hex string like "#ff0000". | No |
| `metalness` |`number` (**maximum:** 100.0)| Metalness of the new material, a percentage like 95.7. | No |
| `roughness` |`number` (**maximum:** 100.0)| Roughness of the new material, a percentage like 95.7. | No |

View File

@ -12,5 +12,10 @@ KCL value for an optional parameter which was not given an argument. (remember,
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |

View File

@ -0,0 +1,23 @@
---
title: "SweepData"
excerpt: "Data for a sweep."
layout: manual
---
Data for a sweep.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `path` |[`Sketch`](/docs/kcl/types/Sketch)| The path to sweep along. | No |
| `sectional` |`boolean`| If true, the sweep will be broken up into sub-sweeps (extrusions, revolves, sweeps) based on the trajectory path components. | No |
| `tolerance` |`number`| Tolerance for the sweep operation. | No |

View File

@ -94,6 +94,51 @@ test.describe('Editor tests', () => {
|> close(%)`)
})
test('ensure we use the cache, and do not re-execute', async ({ page }) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
await u.codeLocator.click()
await page.keyboard.type(`sketch001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> line([-20, 0], %)
|> close(%)`)
// Ensure we execute the first time.
await u.openDebugPanel()
await expect(
page.locator('[data-receive-command-type="scene_clear_all"]')
).toHaveCount(2)
await expect(
page.locator('[data-message-type="execution-done"]')
).toHaveCount(2)
// Add whitespace to the end of the code.
await u.codeLocator.click()
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('Home')
await page.keyboard.type(' ')
await page.keyboard.press('Enter')
await page.keyboard.type(' ')
// Ensure we don't execute the second time.
await u.openDebugPanel()
// Make sure we didn't clear the scene.
await expect(
page.locator('[data-message-type="execution-done"]')
).toHaveCount(3)
await expect(
page.locator('[data-receive-command-type="scene_clear_all"]')
).toHaveCount(2)
})
test('if you click the format button it formats your code and executes so lints are still there', async ({
page,
}) => {

View File

@ -214,23 +214,7 @@ export class SceneFixture {
coords: { x: number; y: number },
diff: number
) => {
let finalValue = colour
await expect
.poll(async () => {
const pixel = (await getPixelRGBs(this.page)(coords, 1))[0]
if (!pixel) return null
finalValue = pixel
return pixel.every(
(channel, index) => Math.abs(channel - colour[index]) < diff
)
})
.toBeTruthy()
.catch((cause) => {
throw new Error(
`ExpectPixelColor: expecting ${colour} got ${finalValue}`,
{ cause }
)
})
await expectPixelColor(this.page, colour, coords, diff)
}
get gizmo() {
@ -246,3 +230,28 @@ export class SceneFixture {
await buttonToTest.click()
}
}
export async function expectPixelColor(
page: Page,
colour: [number, number, number],
coords: { x: number; y: number },
diff: number
) {
let finalValue = colour
await expect
.poll(async () => {
const pixel = (await getPixelRGBs(page)(coords, 1))[0]
if (!pixel) return null
finalValue = pixel
return pixel.every(
(channel, index) => Math.abs(channel - colour[index]) < diff
)
})
.toBeTruthy()
.catch((cause) => {
throw new Error(
`ExpectPixelColor: expecting ${colour} got ${finalValue}`,
{ cause }
)
})
}

View File

@ -19,6 +19,7 @@ import {
TEST_SETTINGS_ONBOARDING_USER_MENU,
} from './storageStates'
import * as TOML from '@iarna/toml'
import { expectPixelColor } from './fixtures/sceneFixture'
test.beforeEach(async ({ context, page }, testInfo) => {
if (testInfo.tags.includes('@electron')) {
@ -45,7 +46,7 @@ test.describe('Onboarding tests', () => {
{ settingsKey: TEST_SETTINGS_KEY }
)
await page.setViewportSize({ width: 1200, height: 500 })
await page.setViewportSize({ width: 1200, height: 1000 })
await u.waitForAuthSkipAppStart()
@ -54,6 +55,12 @@ test.describe('Onboarding tests', () => {
// *and* that the code is shown in the editor
await expect(page.locator('.cm-content')).toContainText('// Shelf Bracket')
// Make sure the model loaded
const XYPlanePoint = { x: 774, y: 116 } as const
const modelColor: [number, number, number] = [45, 45, 45]
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
expect(await u.getGreatestPixDiff(XYPlanePoint, modelColor)).toBeLessThan(8)
})
test(
@ -72,7 +79,7 @@ test.describe('Onboarding tests', () => {
const u = await getUtils(page)
const viewportSize = { width: 1200, height: 500 }
const viewportSize = { width: 1200, height: 1000 }
await page.setViewportSize(viewportSize)
await test.step(`Create a project and open to the onboarding`, async () => {
@ -92,6 +99,14 @@ test.describe('Onboarding tests', () => {
await expect(page.locator('.cm-content')).toContainText(
'// Shelf Bracket'
)
// TODO: jess make less shit
// Make sure the model loaded
//const XYPlanePoint = { x: 986, y: 522 } as const
//const modelColor: [number, number, number] = [76, 76, 76]
//await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
//await expectPixelColor(page, modelColor, XYPlanePoint, 8)
})
await electronApp.close()
@ -108,7 +123,7 @@ test.describe('Onboarding tests', () => {
}, initialCode)
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await page.setViewportSize({ width: 1200, height: 1000 })
await u.waitForAuthSkipAppStart()
// Replay the onboarding
@ -140,6 +155,12 @@ test.describe('Onboarding tests', () => {
return localStorage.getItem('persistCode')
})
).toContain('// Shelf Bracket')
// Make sure the model loaded
const XYPlanePoint = { x: 986, y: 522 } as const
const modelColor: [number, number, number] = [76, 76, 76]
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
await expectPixelColor(page, modelColor, XYPlanePoint, 8)
})
test('Click through each onboarding step', async ({ page }) => {
@ -179,6 +200,17 @@ test.describe('Onboarding tests', () => {
// Test that the onboarding pane is gone
await expect(page.getByTestId('onboarding-content')).not.toBeVisible()
await expect(page.url()).not.toContain('onboarding')
await u.openAndClearDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// TODO: jess to fix
// Make sure the model loaded
//const XYPlanePoint = { x: 774, y: 516 } as const
// const modelColor: [number, number, number] = [129, 129, 129]
// await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
// await expectPixelColor(page, modelColor, XYPlanePoint, 20)
})
test('Onboarding redirects and code updating', async ({ page }) => {
@ -393,7 +425,7 @@ test.describe('Onboarding tests', () => {
})
})
test(
test.fixme(
'Restarting onboarding on desktop takes one attempt',
{ tag: '@electron' },
async ({ browser: _ }, testInfo) => {
@ -439,7 +471,7 @@ test(
})
await test.step('Navigate into project', async () => {
await page.setViewportSize({ width: 1200, height: 500 })
await page.setViewportSize({ width: 1200, height: 1000 })
page.on('console', console.log)
@ -462,7 +494,15 @@ test(
await test.step('Confirm that the onboarding has restarted', async () => {
await expect(tutorialProjectIndicator).toBeVisible()
await expect(tutorialModalText).toBeVisible()
// Make sure the model loaded
const XYPlanePoint = { x: 988, y: 523 } as const
const modelColor: [number, number, number] = [76, 76, 76]
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
await expectPixelColor(page, modelColor, XYPlanePoint, 8)
await tutorialDismissButton.click()
// Make sure model still there.
await expectPixelColor(page, modelColor, XYPlanePoint, 8)
})
await test.step('Clear code and restart onboarding from settings', async () => {

View File

@ -950,7 +950,75 @@ test(
test.describe('Grid visibility', { tag: '@snapshot' }, () => {
// FIXME: Skip on macos its being weird.
test.skip(process.platform === 'darwin', 'Skip on macos')
// test.skip(process.platform === 'darwin', 'Skip on macos')
test('Grid turned off to on via command bar', async ({ page }) => {
const u = await getUtils(page)
const stream = page.getByTestId('stream')
const mask = [
page.locator('#app-header'),
page.locator('#sidebar-top-ribbon'),
page.locator('#sidebar-bottom-ribbon'),
]
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
// wait for execution done
await expect(
page.locator('[data-message-type="execution-done"]')
).toHaveCount(1)
await u.closeDebugPanel()
await u.closeKclCodePanel()
// TODO: Find a way to truly know that the objects have finished
// rendering, because an execution-done message is not sufficient.
await page.waitForTimeout(1000)
// Open the command bar.
await page
.getByRole('button', { name: 'Commands', exact: false })
.or(page.getByRole('button', { name: '⌘K' }))
.click()
const commandName = 'show scale grid'
const commandOption = page.getByRole('option', {
name: commandName,
exact: false,
})
const cmdSearchBar = page.getByPlaceholder('Search commands')
// This selector changes after we set the setting
await cmdSearchBar.fill(commandName)
await expect(commandOption).toBeVisible()
await commandOption.click()
const toggleInput = page.getByPlaceholder('Off')
await expect(toggleInput).toBeVisible()
await expect(toggleInput).toBeFocused()
// Select On
await page.keyboard.press('ArrowDown')
await expect(page.getByRole('option', { name: 'Off' })).toHaveAttribute(
'data-headlessui-state',
'active selected'
)
await page.keyboard.press('ArrowUp')
await expect(page.getByRole('option', { name: 'On' })).toHaveAttribute(
'data-headlessui-state',
'active'
)
await page.keyboard.press('Enter')
// Check the toast appeared
await expect(
page.getByText(`Set show scale grid to "true" as a user default`)
).toBeVisible()
await expect(stream).toHaveScreenshot({
maxDiffPixels: 100,
mask,
})
})
test('Grid turned off', async ({ page }) => {
const u = await getUtils(page)
@ -1096,3 +1164,109 @@ test.fixme('theme persists', async ({ page, context }) => {
maxDiffPixels: 100,
})
})
test.describe('code color goober', { tag: '@snapshot' }, () => {
test('code color goober', async ({ page, context }) => {
const u = await getUtils(page)
await context.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`// Create a pipe using a sweep.
// Create a path for the sweep.
sweepPath = startSketchOn('XZ')
|> startProfileAt([0.05, 0.05], %)
|> line([0, 7], %)
|> tangentialArc({ offset = 90, radius = 5 }, %)
|> line([-3, 0], %)
|> tangentialArc({ offset = -90, radius = 5 }, %)
|> line([0, 7], %)
sweepSketch = startSketchOn('XY')
|> startProfileAt([2, 0], %)
|> arc({
angleEnd = 360,
angleStart = 0,
radius = 2
}, %)
|> sweep({
path = sweepPath,
}, %)
|> appearance({
color = "#bb00ff",
metalness = 90,
roughness = 90
}, %)
`
)
})
await page.setViewportSize({ width: 1200, height: 1000 })
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.clearAndCloseDebugPanel()
await expect(page, 'expect small color widget').toHaveScreenshot({
maxDiffPixels: 100,
})
})
test('code color goober opening window', async ({ page, context }) => {
const u = await getUtils(page)
await context.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`// Create a pipe using a sweep.
// Create a path for the sweep.
sweepPath = startSketchOn('XZ')
|> startProfileAt([0.05, 0.05], %)
|> line([0, 7], %)
|> tangentialArc({ offset = 90, radius = 5 }, %)
|> line([-3, 0], %)
|> tangentialArc({ offset = -90, radius = 5 }, %)
|> line([0, 7], %)
sweepSketch = startSketchOn('XY')
|> startProfileAt([2, 0], %)
|> arc({
angleEnd = 360,
angleStart = 0,
radius = 2
}, %)
|> sweep({
path = sweepPath,
}, %)
|> appearance({
color = "#bb00ff",
metalness = 90,
roughness = 90
}, %)
`
)
})
await page.setViewportSize({ width: 1200, height: 1000 })
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.clearAndCloseDebugPanel()
await expect(page.locator('.cm-css-color-picker-wrapper')).toBeVisible()
// Click the color widget
await page.locator('.cm-css-color-picker-wrapper input').click()
await expect(
page,
'expect small color widget to have window open'
).toHaveScreenshot({
maxDiffPixels: 100,
})
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -14,7 +14,7 @@ export const TEST_SETTINGS = {
},
modeling: {
defaultUnit: 'in',
mouseControls: 'KittyCAD',
mouseControls: 'Zoo',
cameraProjection: 'perspective',
showDebugPanel: true,
},

View File

@ -479,4 +479,26 @@ test.describe('Testing Camera Movement', () => {
})
}
})
test('Right-click opens context menu when not dragged', async ({ page }) => {
const u = await getUtils(page)
await u.waitForAuthSkipAppStart()
await test.step(`The menu should not show if we drag the mouse`, async () => {
await page.mouse.move(900, 200)
await page.mouse.down({ button: 'right' })
await page.mouse.move(900, 300)
await page.mouse.up({ button: 'right' })
await expect(page.getByTestId('view-controls-menu')).not.toBeVisible()
})
await test.step(`The menu should show if we don't drag the mouse`, async () => {
await page.mouse.move(900, 200)
await page.mouse.down({ button: 'right' })
await page.mouse.up({ button: 'right' })
await expect(page.getByTestId('view-controls-menu')).toBeVisible()
})
})
})

View File

@ -1,20 +1,9 @@
import type { ForgeConfig } from '@electron-forge/shared-types'
import { MakerSquirrel } from '@electron-forge/maker-squirrel'
import { MakerZIP } from '@electron-forge/maker-zip'
import { MakerDeb } from '@electron-forge/maker-deb'
import { MakerRpm } from '@electron-forge/maker-rpm'
import { VitePlugin } from '@electron-forge/plugin-vite'
import { MakerWix, MakerWixConfig } from '@electron-forge/maker-wix'
import { FusesPlugin } from '@electron-forge/plugin-fuses'
import { FuseV1Options, FuseVersion } from '@electron/fuses'
import path from 'path'
interface ExtendedMakerWixConfig extends MakerWixConfig {
// see https://github.com/electron/forge/issues/3673
// this is an undocumented property of electron-wix-msi
associateExtensions?: string
}
const rootDir = process.cwd()
const config: ForgeConfig = {
@ -39,26 +28,7 @@ const config: ForgeConfig = {
extendInfo: 'Info.plist', // Information for file associations.
},
rebuildConfig: {},
makers: [
new MakerSquirrel({
setupIcon: path.resolve(rootDir, 'assets', 'icon.ico'),
}),
new MakerWix({
icon: path.resolve(rootDir, 'assets', 'icon.ico'),
associateExtensions: 'kcl',
} as ExtendedMakerWixConfig),
new MakerZIP({}, ['darwin']),
new MakerRpm({
options: {
icon: path.resolve(rootDir, 'assets', 'icon.png'),
},
}),
new MakerDeb({
options: {
icon: path.resolve(rootDir, 'assets', 'icon.png'),
},
}),
],
makers: [],
plugins: [
new VitePlugin({
// `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.

View File

@ -39,7 +39,6 @@
"chokidar": "^4.0.1",
"codemirror": "^6.0.1",
"decamelize": "^6.0.0",
"electron-squirrel-startup": "^1.0.1",
"electron-updater": "6.3.0",
"fuse.js": "^7.0.0",
"html2canvas-pro": "^1.5.8",
@ -69,7 +68,7 @@
"yargs": "^17.7.2"
},
"scripts": {
"start": "vite",
"start": "vite --port=3000 --host=0.0.0.0",
"start:prod": "vite preview --port=3000",
"serve": "vite serve --port=3000",
"build": "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && source \"$HOME/.cargo/env\" && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -y && yarn build:wasm && vite build",
@ -104,8 +103,6 @@
"generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts",
"tron:start": "electron-forge start",
"tron:package": "electron-forge package",
"tron:make": "electron-forge make",
"tron:publish": "electron-forge publish",
"tron:test": "NODE_ENV=development yarn playwright test --config=playwright.electron.config.ts --grep=@electron",
"tronb:vite": "vite build -c vite.main.config.ts && vite build -c vite.preload.config.ts && vite build -c vite.renderer.config.ts",
"tronb:package": "electron-builder --config electron-builder.yml",
@ -148,17 +145,10 @@
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-env": "^7.25.4",
"@electron-forge/cli": "^7.4.0",
"@electron-forge/maker-deb": "^7.4.0",
"@electron-forge/maker-rpm": "^7.4.0",
"@electron-forge/maker-squirrel": "^7.4.0",
"@electron-forge/maker-wix": "^7.5.0",
"@electron-forge/maker-zip": "^7.5.0",
"@electron-forge/plugin-auto-unpack-natives": "^7.4.0",
"@electron-forge/plugin-fuses": "^7.4.0",
"@electron-forge/plugin-vite": "^7.4.0",
"@electron/fuses": "^1.8.0",
"@electron/rebuild": "^3.6.0",
"@electron-forge/cli": "7.4.0",
"@electron-forge/plugin-fuses": "7.4.0",
"@electron-forge/plugin-vite": "7.4.0",
"@electron/fuses": "1.8.0",
"@iarna/toml": "^2.2.5",
"@lezer/generator": "^1.7.1",
"@nabla/vite-plugin-eslint": "^2.0.5",
@ -188,9 +178,9 @@
"@xstate/cli": "^0.5.17",
"autoprefixer": "^10.4.19",
"d3-force": "^3.0.0",
"electron": "^32.1.2",
"electron-builder": "^24.13.3",
"electron-notarize": "^1.2.2",
"electron": "32.1.2",
"electron-builder": "24.13.3",
"electron-notarize": "1.2.2",
"eslint": "^8.0.1",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-css-modules": "^2.12.0",

View File

@ -119,6 +119,11 @@
"title": "Pipe and Flange Assembly",
"description": "A crucial component in various piping systems, designed to facilitate the connection, disconnection, and access to piping for inspection, cleaning, and modifications. This assembly combines pipes (long cylindrical conduits) with flanges (plate-like fittings) to create a secure yet detachable joint."
},
{
"file": "pipe-with-bend.kcl",
"title": "Pipe with bend",
"description": "A tubular section or hollow cylinder, usually but not necessarily of circular cross-section, used mainly to convey substances that can flow."
},
{
"file": "poopy-shoe.kcl",
"title": "Poopy Shoe",

View File

@ -105,7 +105,7 @@ export class CameraControls {
pendingZoom: number | null = null
pendingRotation: Vector2 | null = null
pendingPan: Vector2 | null = null
interactionGuards: MouseGuard = cameraMouseDragGuards.KittyCAD
interactionGuards: MouseGuard = cameraMouseDragGuards.Zoo
isFovAnimationInProgress = false
perspectiveFovBeforeOrtho = 45
get isPerspective() {

View File

@ -1,13 +1,23 @@
import toast from 'react-hot-toast'
import { ActionIcon, ActionIconProps } from './ActionIcon'
import { RefObject, useEffect, useMemo, useRef, useState } from 'react'
import {
MouseEvent,
RefObject,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { Dialog } from '@headlessui/react'
interface ContextMenuProps
export interface ContextMenuProps
extends Omit<React.HTMLAttributes<HTMLUListElement>, 'children'> {
items?: React.ReactElement[]
menuTargetElement?: RefObject<HTMLElement>
guard?: (e: globalThis.MouseEvent) => boolean
event?: 'contextmenu' | 'mouseup'
}
const DefaultContextMenuItems = [
@ -20,6 +30,8 @@ export function ContextMenu({
items = DefaultContextMenuItems,
menuTargetElement,
className,
guard,
event = 'contextmenu',
...props
}: ContextMenuProps) {
const dialogRef = useRef<HTMLDivElement>(null)
@ -32,6 +44,15 @@ export function ContextMenu({
useHotkeys('esc', () => setOpen(false), {
enabled: open,
})
const handleContextMenu = useCallback(
(e: globalThis.MouseEvent) => {
if (guard && !guard(e)) return
e.preventDefault()
setPosition({ x: e.clientX, y: e.clientY })
setOpen(true)
},
[guard, setPosition, setOpen]
)
const dialogPositionStyle = useMemo(() => {
if (!dialogRef.current)
@ -78,21 +99,9 @@ export function ContextMenu({
// Add context menu listener to target once mounted
useEffect(() => {
const handleContextMenu = (e: MouseEvent) => {
console.log('context menu', e)
e.preventDefault()
setPosition({ x: e.x, y: e.y })
setOpen(true)
}
menuTargetElement?.current?.addEventListener(
'contextmenu',
handleContextMenu
)
menuTargetElement?.current?.addEventListener(event, handleContextMenu)
return () => {
menuTargetElement?.current?.removeEventListener(
'contextmenu',
handleContextMenu
)
menuTargetElement?.current?.removeEventListener(event, handleContextMenu)
}
}, [menuTargetElement?.current])
@ -100,7 +109,10 @@ export function ContextMenu({
<Dialog open={open} onClose={() => setOpen(false)}>
<div
className="fixed inset-0 z-50 w-screen h-screen"
onContextMenu={(e) => e.preventDefault()}
onContextMenu={(e) => {
e.preventDefault()
setPosition({ x: e.clientX, y: e.clientY })
}}
>
<Dialog.Backdrop className="fixed z-10 inset-0" />
<Dialog.Panel

View File

@ -1,6 +1,6 @@
import { SceneInfra } from 'clientSideScene/sceneInfra'
import { sceneInfra } from 'lib/singletons'
import { MutableRefObject, useEffect, useMemo, useRef } from 'react'
import { MutableRefObject, useEffect, useRef } from 'react'
import {
WebGLRenderer,
Scene,
@ -19,16 +19,14 @@ import {
Intersection,
Object3D,
} from 'three'
import {
ContextMenu,
ContextMenuDivider,
ContextMenuItem,
ContextMenuItemRefresh,
} from './ContextMenu'
import { Popover } from '@headlessui/react'
import { CustomIcon } from './CustomIcon'
import { reportRejection } from 'lib/trap'
import { useModelingContext } from 'hooks/useModelingContext'
import {
useViewControlMenuItems,
ViewControlContextMenu,
} from './ViewControlMenu'
import { AxisNames } from 'lib/constants'
const CANVAS_SIZE = 80
const FRUSTUM_SIZE = 0.5
@ -40,64 +38,14 @@ enum AxisColors {
Z = '#6689ef',
Gray = '#c6c7c2',
}
enum AxisNames {
X = 'x',
Y = 'y',
Z = 'z',
NEG_X = '-x',
NEG_Y = '-y',
NEG_Z = '-z',
}
const axisNamesSemantic: Record<AxisNames, string> = {
[AxisNames.X]: 'Right',
[AxisNames.Y]: 'Back',
[AxisNames.Z]: 'Top',
[AxisNames.NEG_X]: 'Left',
[AxisNames.NEG_Y]: 'Front',
[AxisNames.NEG_Z]: 'Bottom',
}
export default function Gizmo() {
const menuItems = useViewControlMenuItems()
const wrapperRef = useRef<HTMLDivElement | null>(null)
const canvasRef = useRef<HTMLCanvasElement | null>(null)
const raycasterIntersect = useRef<Intersection<Object3D> | null>(null)
const cameraPassiveUpdateTimer = useRef(0)
const raycasterPassiveUpdateTimer = useRef(0)
const { send: modelingSend } = useModelingContext()
const menuItems = useMemo(
() => [
...Object.entries(axisNamesSemantic).map(([axisName, axisSemantic]) => (
<ContextMenuItem
key={axisName}
onClick={() => {
sceneInfra.camControls
.updateCameraToAxis(axisName as AxisNames)
.catch(reportRejection)
}}
>
{axisSemantic} view
</ContextMenuItem>
)),
<ContextMenuDivider />,
<ContextMenuItem
onClick={() => {
sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
}}
>
Reset view
</ContextMenuItem>,
<ContextMenuItem
onClick={() => {
modelingSend({ type: 'Center camera on selection' })
}}
>
Center view on selection
</ContextMenuItem>,
<ContextMenuDivider />,
<ContextMenuItemRefresh />,
],
[axisNamesSemantic]
)
useEffect(() => {
if (!canvasRef.current) return
@ -161,7 +109,7 @@ export default function Gizmo() {
className="grid place-content-center rounded-full overflow-hidden border border-solid border-primary/50 pointer-events-auto bg-chalkboard-10/70 dark:bg-chalkboard-100/80 backdrop-blur-sm"
>
<canvas ref={canvasRef} />
<ContextMenu menuTargetElement={wrapperRef} items={menuItems} />
<ViewControlContextMenu menuTargetElement={wrapperRef} />
</div>
<GizmoDropdown items={menuItems} />
</div>

View File

@ -1,4 +1,4 @@
import { APP_VERSION, RELEASE_URL } from 'routes/Settings'
import { APP_VERSION, getReleaseUrl } from 'routes/Settings'
import { CustomIcon } from 'components/CustomIcon'
import Tooltip from 'components/Tooltip'
import { PATHS } from 'lib/paths'
@ -72,8 +72,8 @@ export function LowerRightControls({
<menu className="flex items-center justify-end gap-3 pointer-events-auto">
{!location.pathname.startsWith(PATHS.HOME) && <ModelStateIndicator />}
<a
onClick={openExternalBrowserIfDesktop(RELEASE_URL)}
href={RELEASE_URL}
onClick={openExternalBrowserIfDesktop(getReleaseUrl())}
href={getReleaseUrl()}
target="_blank"
rel="noopener noreferrer"
className={'!no-underline font-mono text-xs ' + linkOverrideClassName}

View File

@ -69,14 +69,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const [isKclLspReady, setIsKclLspReady] = useState(false)
const [isCopilotLspReady, setIsCopilotLspReady] = useState(false)
const {
auth,
settings: {
context: {
modeling: { defaultUnit },
},
},
} = useSettingsAuthContext()
const { auth } = useSettingsAuthContext()
const token = auth?.context.token
const navigate = useNavigate()
@ -92,7 +85,6 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const initEvent: KclWorkerOptions = {
wasmUrl: wasmUrl(),
token: token,
baseUnit: defaultUnit.current,
apiBaseUrl: VITE_KC_API_BASE_URL,
}
lspWorker.postMessage({

View File

@ -6,6 +6,7 @@ import Tooltip from 'components/Tooltip'
import { CustomIconName } from 'components/CustomIcon'
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
import { ActionIcon } from 'components/ActionIcon'
import { onboardingPaths } from 'routes/Onboarding/paths'
export interface ModelingPaneProps {
id: string
@ -70,7 +71,7 @@ export const ModelingPane = ({
const { settings } = useSettingsAuthContext()
const onboardingStatus = settings.context.app.onboardingStatus
const pointerEventsCssClass =
onboardingStatus.current === 'camera'
onboardingStatus.current === onboardingPaths.CAMERA
? 'pointer-events-none '
: 'pointer-events-auto '
return (

View File

@ -19,6 +19,7 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
import { useKclContext } from 'lang/KclProvider'
import { MachineManagerContext } from 'components/MachineManagerProvider'
import { onboardingPaths } from 'routes/Onboarding/paths'
interface ModelingSidebarProps {
paneOpacity: '' | 'opacity-20' | 'opacity-40'
@ -41,7 +42,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
const onboardingStatus = settings.context.app.onboardingStatus
const { send, context } = useModelingContext()
const pointerEventsCssClass =
onboardingStatus.current === 'camera' ||
onboardingStatus.current === onboardingPaths.CAMERA ||
context.store?.openPanes.length === 0
? 'pointer-events-none '
: 'pointer-events-auto '

View File

@ -13,7 +13,7 @@ import { isDesktop } from 'lib/isDesktop'
import { ActionButton } from 'components/ActionButton'
import { SettingsFieldInput } from './SettingsFieldInput'
import toast from 'react-hot-toast'
import { APP_VERSION, IS_NIGHTLY, RELEASE_URL } from 'routes/Settings'
import { APP_VERSION, IS_NIGHTLY, getReleaseUrl } from 'routes/Settings'
import { PATHS } from 'lib/paths'
import {
createAndOpenNewTutorialProject,
@ -246,8 +246,8 @@ export const AllSettingsFields = forwardRef(
to inject the version from package.json */}
App version {APP_VERSION}.{' '}
<a
onClick={openExternalBrowserIfDesktop(RELEASE_URL)}
href={RELEASE_URL}
onClick={openExternalBrowserIfDesktop(getReleaseUrl())}
href={getReleaseUrl()}
target="_blank"
rel="noopener noreferrer"
>

View File

@ -1,5 +1,5 @@
import { trap } from 'lib/trap'
import { useMachine } from '@xstate/react'
import { useMachine, useSelector } from '@xstate/react'
import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom'
import { PATHS, BROWSER_PATH } from 'lib/paths'
import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine'
@ -23,7 +23,6 @@ import {
engineCommandManager,
sceneEntitiesManager,
} from 'lib/singletons'
import { uuidv4 } from 'lib/utils'
import { IndexLoaderData } from 'lib/types'
import { settings } from 'lib/settings/initialSettings'
import {
@ -55,11 +54,15 @@ type SettingsAuthContextType = {
settings: MachineContext<typeof settingsMachine>
}
// a little hacky for sure, open to changing it
// this implies that we should only even have one instance of this provider mounted at any one time
// but I think that's a safe assumption
let settingsStateRef: ContextFrom<typeof settingsMachine> | undefined
export const getSettingsState = () => settingsStateRef
/**
* This variable is used to store the last snapshot of the settings context
* for use outside of React, such as in `wasm.ts`. It is updated every time
* the settings machine changes with `useSelector`.
* TODO: when we decouple XState from React, we can just subscribe to the actor directly from `wasm.ts`
*/
export let lastSettingsContextSnapshot:
| ContextFrom<typeof settingsMachine>
| undefined
export const SettingsAuthContext = createContext({} as SettingsAuthContextType)
@ -129,27 +132,11 @@ export const SettingsAuthProviderBase = ({
.setTheme(context.app.theme.current)
.catch(reportRejection)
},
setEngineScaleGridVisibility: ({ context }) => {
engineCommandManager.setScaleGridVisibility(
context.modeling.showScaleGrid.current
)
},
setClientTheme: ({ context }) => {
const opposingTheme = getOppositeTheme(context.app.theme.current)
sceneInfra.theme = opposingTheme
sceneEntitiesManager.updateSegmentBaseColor(opposingTheme)
},
setEngineEdges: ({ context }) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
engineCommandManager.sendSceneCommand({
cmd_id: uuidv4(),
type: 'modeling_cmd_req',
cmd: {
type: 'edge_lines_visible' as any, // TODO update kittycad.ts to get this new command type
hidden: !context.modeling.highlightEdges.current,
},
})
},
toastSuccess: ({ event }) => {
if (!('data' in event)) return
const eventParts = event.type.replace(/^set./, '').split('.') as [
@ -175,17 +162,27 @@ export const SettingsAuthProviderBase = ({
},
'Execute AST': ({ context, event }) => {
try {
const relevantSetting = (s: typeof settings) => {
return (
s.modeling?.defaultUnit?.current !==
context.modeling.defaultUnit.current ||
s.modeling.showScaleGrid.current !==
context.modeling.showScaleGrid.current ||
s.modeling?.highlightEdges.current !==
context.modeling.highlightEdges.current
)
}
const allSettingsIncludesUnitChange =
event.type === 'Set all settings' &&
event.settings?.modeling?.defaultUnit?.current !==
context.modeling.defaultUnit.current
relevantSetting(event.settings)
const resetSettingsIncludesUnitChange =
event.type === 'Reset settings' &&
context.modeling.defaultUnit.current !==
settings?.modeling?.defaultUnit?.default
event.type === 'Reset settings' && relevantSetting(settings)
if (
event.type === 'set.modeling.defaultUnit' ||
event.type === 'set.modeling.showScaleGrid' ||
event.type === 'set.modeling.highlightEdges' ||
allSettingsIncludesUnitChange ||
resetSettingsIncludesUnitChange
) {
@ -214,7 +211,10 @@ export const SettingsAuthProviderBase = ({
}),
{ input: loadedSettings }
)
settingsStateRef = settingsState.context
// Any time the actor changes, update the settings state for external use
useSelector(settingsActor, (s) => {
lastSettingsContextSnapshot = s.context
})
useEffect(() => {
if (!isDesktop()) return

View File

@ -20,6 +20,7 @@ import { IndexLoaderData } from 'lib/types'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { err, reportRejection } from 'lib/trap'
import { getArtifactOfTypes } from 'lang/std/artifactGraph'
import { ViewControlContextMenu } from './ViewControlMenu'
enum StreamState {
Playing = 'playing',
@ -30,6 +31,7 @@ enum StreamState {
export const Stream = () => {
const [isLoading, setIsLoading] = useState(true)
const videoWrapperRef = useRef<HTMLDivElement>(null)
const videoRef = useRef<HTMLVideoElement>(null)
const { settings } = useSettingsAuthContext()
const { state, send } = useModelingContext()
@ -258,7 +260,7 @@ export const Stream = () => {
setIsLoading(false)
}, [mediaStream])
const handleMouseUp: MouseEventHandler<HTMLDivElement> = (e) => {
const handleClick: MouseEventHandler<HTMLDivElement> = (e) => {
// If we've got no stream or connection, don't do anything
if (!isNetworkOkay) return
if (!videoRef.current) return
@ -320,10 +322,11 @@ export const Stream = () => {
return (
<div
ref={videoWrapperRef}
className="absolute inset-0 z-0"
id="stream"
data-testid="stream"
onClick={handleMouseUp}
onClick={handleClick}
onDoubleClick={enterSketchModeIfSelectingSketch}
onContextMenu={(e) => e.preventDefault()}
onContextMenuCapture={(e) => e.preventDefault()}
@ -384,6 +387,14 @@ export const Stream = () => {
</Loading>
</div>
)}
<ViewControlContextMenu
event="mouseup"
guard={(e) =>
sceneInfra.camControls.wasDragging === false &&
btnName(e).right === true
}
menuTargetElement={videoWrapperRef}
/>
</div>
)
}

View File

@ -2,6 +2,7 @@ import toast from 'react-hot-toast'
import { ActionButton } from './ActionButton'
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { Marked } from '@ts-stack/markdown'
import { getReleaseUrl } from 'routes/Settings'
export function ToastUpdate({
version,
@ -32,10 +33,8 @@ export function ToastUpdate({
A new update has downloaded and will be available next time you
start the app. You can view the release notes{' '}
<a
onClick={openExternalBrowserIfDesktop(
`https://github.com/KittyCAD/modeling-app/releases/tag/v${version}`
)}
href={`https://github.com/KittyCAD/modeling-app/releases/tag/v${version}`}
onClick={openExternalBrowserIfDesktop(getReleaseUrl(version))}
href={getReleaseUrl(version)}
target="_blank"
rel="noreferrer"
>

View File

@ -41,7 +41,10 @@ export function UnitsMenu() {
close()
}}
>
{baseUnitLabels[unit]}
<span className="flex-1">{baseUnitLabels[unit]}</span>
{unit === settings.context.modeling.defaultUnit.current && (
<span className="text-chalkboard-60">current</span>
)}
</button>
</li>
))}

View File

@ -0,0 +1,66 @@
import { reportRejection } from 'lib/trap'
import {
ContextMenu,
ContextMenuDivider,
ContextMenuItem,
ContextMenuItemRefresh,
ContextMenuProps,
} from './ContextMenu'
import { AxisNames, VIEW_NAMES_SEMANTIC } from 'lib/constants'
import { useModelingContext } from 'hooks/useModelingContext'
import { useMemo } from 'react'
import { sceneInfra } from 'lib/singletons'
export function useViewControlMenuItems() {
const { send: modelingSend } = useModelingContext()
const menuItems = useMemo(
() => [
...Object.entries(VIEW_NAMES_SEMANTIC).map(([axisName, axisSemantic]) => (
<ContextMenuItem
key={axisName}
onClick={() => {
sceneInfra.camControls
.updateCameraToAxis(axisName as AxisNames)
.catch(reportRejection)
}}
>
{axisSemantic} view
</ContextMenuItem>
)),
<ContextMenuDivider />,
<ContextMenuItem
onClick={() => {
sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
}}
>
Reset view
</ContextMenuItem>,
<ContextMenuItem
onClick={() => {
modelingSend({ type: 'Center camera on selection' })
}}
>
Center view on selection
</ContextMenuItem>,
<ContextMenuDivider />,
<ContextMenuItemRefresh />,
],
[VIEW_NAMES_SEMANTIC]
)
return menuItems
}
export function ViewControlContextMenu({
menuTargetElement: wrapperRef,
...props
}: ContextMenuProps) {
const menuItems = useViewControlMenuItems()
return (
<ContextMenu
data-testid="view-controls-menu"
menuTargetElement={wrapperRef}
items={menuItems}
{...props}
/>
)
}

View File

@ -0,0 +1,327 @@
import {
EditorView,
WidgetType,
ViewUpdate,
ViewPlugin,
DecorationSet,
Decoration,
} from '@codemirror/view'
import { Range, Extension, Text } from '@codemirror/state'
import { NodeProp, Tree } from '@lezer/common'
import { language, syntaxTree } from '@codemirror/language'
interface PickerState {
from: number
to: number
alpha: string
colorType: ColorType
}
export interface WidgetOptions extends PickerState {
color: string
}
export type ColorData = Omit<WidgetOptions, 'from' | 'to'>
const pickerState = new WeakMap<HTMLInputElement, PickerState>()
export enum ColorType {
hex = 'HEX',
}
const hexRegex = /(^|\b)(#[0-9a-f]{3,9})(\b|$)/i
function discoverColorsInKCL(
syntaxTree: Tree,
from: number,
to: number,
typeName: string,
doc: Text,
language?: string
): WidgetOptions | Array<WidgetOptions> | null {
switch (typeName) {
case 'Program':
case 'VariableDeclaration':
case 'CallExpression':
case 'ObjectExpression':
case 'ObjectProperty':
case 'ArgumentList':
case 'PipeExpression': {
let innerTree = syntaxTree.resolveInner(from, 0).tree
if (!innerTree) {
innerTree = syntaxTree.resolveInner(from, 1).tree
if (!innerTree) {
return null
}
}
const overlayTree = innerTree.prop(NodeProp.mounted)?.tree
if (overlayTree?.type.name !== 'Styles') {
return null
}
const ret: Array<WidgetOptions> = []
overlayTree.iterate({
from: 0,
to: overlayTree.length,
enter: ({ type, from: overlayFrom, to: overlayTo }) => {
const maybeWidgetOptions = discoverColorsInKCL(
syntaxTree,
// We add one because the tree doesn't include the
// quotation mark from the style tag
from + 1 + overlayFrom,
from + 1 + overlayTo,
type.name,
doc,
language
)
if (maybeWidgetOptions) {
if (Array.isArray(maybeWidgetOptions)) {
console.error('Unexpected nested overlays')
ret.push(...maybeWidgetOptions)
} else {
ret.push(maybeWidgetOptions)
}
}
},
})
return ret
}
case 'String': {
const result = parseColorLiteral(doc.sliceString(from, to))
if (!result) {
return null
}
return {
...result,
from,
to,
}
}
default:
return null
}
}
export function parseColorLiteral(colorLiteral: string): ColorData | null {
const literal = colorLiteral.replace(/"/g, '')
const match = hexRegex.exec(literal)
if (!match) {
return null
}
const [color, alpha] = toFullHex(literal)
return {
colorType: ColorType.hex,
color,
alpha,
}
}
function colorPickersDecorations(
view: EditorView,
discoverColors: typeof discoverColorsInKCL
) {
const widgets: Array<Range<Decoration>> = []
const st = syntaxTree(view.state)
for (const range of view.visibleRanges) {
st.iterate({
from: range.from,
to: range.to,
enter: ({ type, from, to }) => {
const maybeWidgetOptions = discoverColors(
st,
from,
to,
type.name,
view.state.doc,
view.state.facet(language)?.name
)
if (!maybeWidgetOptions) {
return
}
if (!Array.isArray(maybeWidgetOptions)) {
widgets.push(
Decoration.widget({
widget: new ColorPickerWidget(maybeWidgetOptions),
side: 1,
}).range(maybeWidgetOptions.from)
)
return
}
for (const wo of maybeWidgetOptions) {
widgets.push(
Decoration.widget({
widget: new ColorPickerWidget(wo),
side: 1,
}).range(wo.from)
)
}
},
})
}
return Decoration.set(widgets)
}
function toFullHex(color: string): string[] {
if (color.length === 4) {
// 3-char hex
return [
`#${color[1].repeat(2)}${color[2].repeat(2)}${color[3].repeat(2)}`,
'',
]
}
if (color.length === 5) {
// 4-char hex (alpha)
return [
`#${color[1].repeat(2)}${color[2].repeat(2)}${color[3].repeat(2)}`,
color[4].repeat(2),
]
}
if (color.length === 9) {
// 8-char hex (alpha)
return [`#${color.slice(1, -2)}`, color.slice(-2)]
}
return [color, '']
}
export const wrapperClassName = 'cm-css-color-picker-wrapper'
class ColorPickerWidget extends WidgetType {
private readonly state: PickerState
private readonly color: string
constructor({ color, ...state }: WidgetOptions) {
super()
this.state = state
this.color = color
}
eq(other: ColorPickerWidget) {
return (
other.state.colorType === this.state.colorType &&
other.color === this.color &&
other.state.from === this.state.from &&
other.state.to === this.state.to &&
other.state.alpha === this.state.alpha
)
}
toDOM() {
const picker = document.createElement('input')
pickerState.set(picker, this.state)
picker.type = 'color'
picker.value = this.color
const wrapper = document.createElement('span')
wrapper.appendChild(picker)
wrapper.className = wrapperClassName
return wrapper
}
ignoreEvent() {
return false
}
}
export const colorPickerTheme = EditorView.baseTheme({
[`.${wrapperClassName}`]: {
display: 'inline-block',
outline: '1px solid #eee',
marginRight: '0.6ch',
height: '1em',
width: '1em',
transform: 'translateY(1px)',
},
[`.${wrapperClassName} input[type="color"]`]: {
cursor: 'pointer',
height: '100%',
width: '100%',
padding: 0,
border: 'none',
'&::-webkit-color-swatch-wrapper': {
padding: 0,
},
'&::-webkit-color-swatch': {
border: 'none',
},
'&::-moz-color-swatch': {
border: 'none',
},
},
})
interface IFactoryOptions {
discoverColors: typeof discoverColorsInKCL
}
export const makeColorPicker = (options: IFactoryOptions) =>
ViewPlugin.fromClass(
class ColorPickerViewPlugin {
decorations: DecorationSet
constructor(view: EditorView) {
this.decorations = colorPickersDecorations(view, options.discoverColors)
}
update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged) {
this.decorations = colorPickersDecorations(
update.view,
options.discoverColors
)
}
}
},
{
decorations: (v) => v.decorations,
eventHandlers: {
change: (e, view) => {
const target = e.target as HTMLInputElement
if (
target.nodeName !== 'INPUT' ||
!target.parentElement ||
!target.parentElement.classList.contains(wrapperClassName)
) {
return false
}
const data = pickerState.get(target)!
let converted = '"' + target.value + data.alpha + '"'
view.dispatch({
changes: {
from: data.from,
to: data.to,
insert: converted,
},
})
return true
},
},
}
)
export const colorPicker: Extension = [
makeColorPicker({ discoverColors: discoverColorsInKCL }),
colorPickerTheme,
]

View File

@ -17,6 +17,7 @@ import { kclPlugin } from '.'
import type * as LSP from 'vscode-languageserver-protocol'
// @ts-ignore: No types available
import { parser } from './kcl.grammar'
import { colorPicker } from './colors'
export interface LanguageOptions {
workspaceFolders: LSP.WorkspaceFolder[]
@ -54,14 +55,14 @@ export const KclLanguage = LRLanguage.define({
})
export function kcl(options: LanguageOptions) {
return new LanguageSupport(
KclLanguage,
return new LanguageSupport(KclLanguage, [
colorPicker,
kclPlugin({
documentUri: options.documentUri,
workspaceFolders: options.workspaceFolders,
allowHTMLContent: true,
client: options.client,
processLspNotification: options.processLspNotification,
})
)
}),
])
}

View File

@ -1,7 +1,5 @@
import { LspWorkerEventType } from '@kittycad/codemirror-lsp-client'
import { UnitLength } from 'wasm-lib/kcl/bindings/UnitLength'
export enum LspWorker {
Kcl = 'kcl',
Copilot = 'copilot',
@ -9,7 +7,6 @@ export enum LspWorker {
export interface KclWorkerOptions {
wasmUrl: string
token: string
baseUnit: UnitLength
apiBaseUrl: string
}

View File

@ -17,7 +17,6 @@ import {
KclWorkerOptions,
CopilotWorkerOptions,
} from 'editor/plugins/lsp/types'
import { EngineCommandManager } from 'lang/std/engineConnection'
import { err, reportRejection } from 'lib/trap'
const intoServer: IntoServer = new IntoServer()
@ -46,14 +45,12 @@ export async function copilotLspRun(
export async function kclLspRun(
config: ServerConfig,
engineCommandManager: EngineCommandManager | null,
token: string,
baseUnit: string,
baseUrl: string
) {
try {
console.log('start kcl lsp')
await kcl_lsp_run(config, engineCommandManager, baseUnit, token, baseUrl)
await kcl_lsp_run(config, null, undefined, token, baseUrl)
} catch (e: any) {
console.log('kcl lsp failed', e)
// We can't restart here because a moved value, we should do this another way.
@ -82,13 +79,7 @@ onmessage = function (event: MessageEvent) {
switch (worker) {
case LspWorker.Kcl:
const kclData = eventData as KclWorkerOptions
await kclLspRun(
config,
null,
kclData.token,
kclData.baseUnit,
kclData.apiBaseUrl
)
await kclLspRun(config, kclData.token, kclData.apiBaseUrl)
break
case LspWorker.Copilot:
let copilotData = eventData as CopilotWorkerOptions

View File

@ -2,7 +2,7 @@ import { useLayoutEffect, useEffect, useRef } from 'react'
import { engineCommandManager, kclManager } from 'lib/singletons'
import { deferExecution } from 'lib/utils'
import { Themes } from 'lib/theme'
import { makeDefaultPlanes, modifyGrid } from 'lang/wasm'
import { makeDefaultPlanes } from 'lang/wasm'
import { useModelingContext } from './useModelingContext'
import { useNetworkContext } from 'hooks/useNetworkContext'
import { useAppState, useAppStream } from 'AppState'
@ -56,9 +56,6 @@ export function useSetupEngineManager(
makeDefaultPlanes: () => {
return makeDefaultPlanes(kclManager.engineCommandManager)
},
modifyGrid: (hidden: boolean) => {
return modifyGrid(kclManager.engineCommandManager, hidden)
},
})
hasSetNonZeroDimensions.current = true
}

View File

@ -317,3 +317,8 @@ code {
#code-mirror-override .cm-editor {
height: 100% !important;
}
/* Can't use #code-mirror-override here as we're outside of this div */
.body-bg .cm-diagnosticAction {
@apply bg-primary;
}

View File

@ -311,8 +311,6 @@ export class KclManager {
// Do not send send scene commands if the program was interrupted, go to clean up
if (!isInterrupted) {
this.addDiagnostics(await lintAst({ ast: ast }))
sceneInfra.modelingSend({ type: 'code edit during sketch' })
setSelectionFilterToDefault(execState.memory, this.engineCommandManager)
if (args.zoomToFit) {
@ -358,7 +356,13 @@ export class KclManager {
this.lastSuccessfulProgramMemory = execState.memory
}
this.ast = { ...ast }
// updateArtifactGraph relies on updated executeState/programMemory
await this.engineCommandManager.updateArtifactGraph(this.ast)
this._executeCallback()
if (!isInterrupted) {
sceneInfra.modelingSend({ type: 'code edit during sketch' })
}
this.engineCommandManager.addCommandLog({
type: 'execution-done',
data: null,

View File

@ -66,9 +66,7 @@ export async function executeAst({
? enginelessExecutor(ast, programMemoryOverride)
: _executor(ast, engineCommandManager))
await engineCommandManager.waitForAllCommands(
programMemoryOverride !== undefined
)
await engineCommandManager.waitForAllCommands()
return {
logs: [],

View File

@ -40,7 +40,6 @@ beforeAll(async () => {
makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager),
setMediaStream: () => {},
setIsStreamReady: () => {},
modifyGrid: async () => {},
callbackOnEngineLiteConnect: () => {
resolve(true)
},
@ -248,7 +247,7 @@ extrude003 = extrude(-15, sketch003)`
selectedSegmentSnippet,
expectedExtrudeSnippet
)
})
}, 5_000)
})
const runModifyAstCloneWithEdgeTreatmentAndTag = async (
@ -478,7 +477,7 @@ extrude001 = extrude(-15, sketch001)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(-15, sketch001)
|> chamfer({ length: 5, tags: [seg01] }, %)`
|> chamfer({ length = 5, tags = [seg01] }, %)`
const segmentSnippets = ['line([-20, 0], %)']
const expectedCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %)
@ -488,8 +487,8 @@ extrude001 = extrude(-15, sketch001)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(-15, sketch001)
|> chamfer({ length: 5, tags: [seg01] }, %)
|> ${edgeTreatmentType}({ ${parameterName}: 3, tags: [seg02] }, %)`
|> chamfer({ length = 5, tags = [seg01] }, %)
|> ${edgeTreatmentType}({ ${parameterName} = 3, tags = [seg02] }, %)`
await runModifyAstCloneWithEdgeTreatmentAndTag(
code,

View File

@ -13,7 +13,7 @@ Map {
"range": [
12,
31,
0,
true,
],
},
"id": "UUID",
@ -33,7 +33,7 @@ Map {
"range": [
37,
64,
0,
true,
],
},
"id": "UUID",
@ -60,7 +60,7 @@ Map {
"range": [
70,
86,
0,
true,
],
},
"edgeIds": [
@ -83,7 +83,7 @@ Map {
"range": [
92,
119,
0,
true,
],
},
"edgeCutId": "UUID",
@ -107,7 +107,7 @@ Map {
"range": [
125,
150,
0,
true,
],
},
"edgeIds": [
@ -130,7 +130,7 @@ Map {
"range": [
156,
203,
0,
true,
],
},
"edgeIds": [
@ -153,7 +153,7 @@ Map {
"range": [
209,
217,
0,
true,
],
},
"edgeIds": [],
@ -177,7 +177,7 @@ Map {
"range": [
231,
254,
0,
true,
],
},
"edgeIds": [
@ -320,7 +320,7 @@ Map {
"range": [
260,
299,
0,
true,
],
},
"consumedEdgeId": "UUID",
@ -340,7 +340,7 @@ Map {
"range": [
350,
377,
0,
true,
],
},
"id": "UUID",
@ -366,7 +366,7 @@ Map {
"range": [
383,
398,
0,
true,
],
},
"edgeIds": [
@ -389,7 +389,7 @@ Map {
"range": [
404,
420,
0,
true,
],
},
"edgeIds": [
@ -412,7 +412,7 @@ Map {
"range": [
426,
473,
0,
true,
],
},
"edgeIds": [
@ -435,7 +435,7 @@ Map {
"range": [
479,
487,
0,
true,
],
},
"edgeIds": [],
@ -459,7 +459,7 @@ Map {
"range": [
501,
522,
0,
true,
],
},
"edgeIds": [
@ -478,7 +478,6 @@ Map {
"UUID",
"UUID",
"UUID",
"UUID",
],
"type": "sweep",
},
@ -507,14 +506,6 @@ Map {
"type": "wall",
},
"UUID-34" => {
"edgeCutEdgeIds": [],
"id": "UUID",
"pathIds": [],
"subType": "start",
"sweepId": "UUID",
"type": "cap",
},
"UUID-35" => {
"edgeCutEdgeIds": [],
"id": "UUID",
"pathIds": [],
@ -522,42 +513,42 @@ Map {
"sweepId": "UUID",
"type": "cap",
},
"UUID-36" => {
"UUID-35" => {
"id": "UUID",
"segId": "UUID",
"subType": "opposite",
"sweepId": "UUID",
"type": "sweepEdge",
},
"UUID-36" => {
"id": "UUID",
"segId": "UUID",
"subType": "adjacent",
"sweepId": "UUID",
"type": "sweepEdge",
},
"UUID-37" => {
"id": "UUID",
"segId": "UUID",
"subType": "adjacent",
"subType": "opposite",
"sweepId": "UUID",
"type": "sweepEdge",
},
"UUID-38" => {
"id": "UUID",
"segId": "UUID",
"subType": "opposite",
"subType": "adjacent",
"sweepId": "UUID",
"type": "sweepEdge",
},
"UUID-39" => {
"id": "UUID",
"segId": "UUID",
"subType": "adjacent",
"sweepId": "UUID",
"type": "sweepEdge",
},
"UUID-40" => {
"id": "UUID",
"segId": "UUID",
"subType": "opposite",
"sweepId": "UUID",
"type": "sweepEdge",
},
"UUID-41" => {
"UUID-40" => {
"id": "UUID",
"segId": "UUID",
"subType": "adjacent",

View File

@ -139,7 +139,6 @@ beforeAll(async () => {
makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager),
setMediaStream: () => {},
setIsStreamReady: () => {},
modifyGrid: async () => {},
// eslint-disable-next-line @typescript-eslint/no-misused-promises
callbackOnEngineLiteConnect: async () => {
const cacheEntries = Object.entries(codeToWriteCacheFor) as [
@ -260,11 +259,13 @@ describe('testing createArtifactGraph', () => {
if (err(extrusion)) throw extrusion
expect(extrusion.type).toBe('sweep')
const firstExtrusionIsACubeIE6Sides = 6
const secondExtrusionIsATriangularPrismIE5Sides = 5
// Each face of the triangular prism (5), but without the bottom cap.
// The engine doesn't generate that.
const secondExtrusionIsATriangularPrism = 4
expect(extrusion.surfaces.length).toBe(
!index
? firstExtrusionIsACubeIE6Sides
: secondExtrusionIsATriangularPrismIE5Sides
: secondExtrusionIsATriangularPrism
)
})
})
@ -660,7 +661,7 @@ describe('testing getArtifactsToUpdate', () => {
sweepId: '',
codeRef: {
pathToNode: [['body', '']],
range: [37, 64, 0],
range: [37, 64, true],
},
},
])
@ -673,7 +674,7 @@ describe('testing getArtifactsToUpdate', () => {
surfaceIds: [],
edgeIds: [],
codeRef: {
range: [231, 254, 0],
range: [231, 254, true],
pathToNode: [['body', '']],
},
},
@ -684,7 +685,7 @@ describe('testing getArtifactsToUpdate', () => {
planeId: expect.any(String),
sweepId: expect.any(String),
codeRef: {
range: [37, 64, 0],
range: [37, 64, true],
pathToNode: [['body', '']],
},
solid2dId: expect.any(String),
@ -698,7 +699,7 @@ describe('testing getArtifactsToUpdate', () => {
surfaceId: '',
edgeIds: [],
codeRef: {
range: [70, 86, 0],
range: [70, 86, true],
pathToNode: [['body', '']],
},
},
@ -709,7 +710,7 @@ describe('testing getArtifactsToUpdate', () => {
planeId: expect.any(String),
sweepId: expect.any(String),
codeRef: {
range: [37, 64, 0],
range: [37, 64, true],
pathToNode: [['body', '']],
},
solid2dId: expect.any(String),
@ -724,7 +725,7 @@ describe('testing getArtifactsToUpdate', () => {
edgeIds: [],
surfaceId: '',
codeRef: {
range: [260, 299, 0],
range: [260, 299, true],
pathToNode: [['body', '']],
},
},
@ -735,7 +736,7 @@ describe('testing getArtifactsToUpdate', () => {
surfaceId: expect.any(String),
edgeIds: expect.any(Array),
codeRef: {
range: [92, 119, 0],
range: [92, 119, true],
pathToNode: [['body', '']],
},
edgeCutId: expect.any(String),
@ -757,7 +758,7 @@ describe('testing getArtifactsToUpdate', () => {
surfaceId: expect.any(String),
edgeIds: expect.any(Array),
codeRef: {
range: [156, 203, 0],
range: [156, 203, true],
pathToNode: [['body', '']],
},
},
@ -769,7 +770,7 @@ describe('testing getArtifactsToUpdate', () => {
surfaceIds: expect.any(Array),
edgeIds: expect.any(Array),
codeRef: {
range: [231, 254, 0],
range: [231, 254, true],
pathToNode: [['body', '']],
},
},
@ -788,7 +789,7 @@ describe('testing getArtifactsToUpdate', () => {
surfaceId: expect.any(String),
edgeIds: expect.any(Array),
codeRef: {
range: [125, 150, 0],
range: [125, 150, true],
pathToNode: [['body', '']],
},
},
@ -800,7 +801,7 @@ describe('testing getArtifactsToUpdate', () => {
surfaceIds: expect.any(Array),
edgeIds: expect.any(Array),
codeRef: {
range: [231, 254, 0],
range: [231, 254, true],
pathToNode: [['body', '']],
},
},
@ -819,7 +820,7 @@ describe('testing getArtifactsToUpdate', () => {
surfaceId: expect.any(String),
edgeIds: expect.any(Array),
codeRef: {
range: [92, 119, 0],
range: [92, 119, true],
pathToNode: [['body', '']],
},
edgeCutId: expect.any(String),
@ -832,7 +833,7 @@ describe('testing getArtifactsToUpdate', () => {
surfaceIds: expect.any(Array),
edgeIds: expect.any(Array),
codeRef: {
range: [231, 254, 0],
range: [231, 254, true],
pathToNode: [['body', '']],
},
},
@ -851,7 +852,7 @@ describe('testing getArtifactsToUpdate', () => {
surfaceId: expect.any(String),
edgeIds: expect.any(Array),
codeRef: {
range: [70, 86, 0],
range: [70, 86, true],
pathToNode: [['body', '']],
},
},
@ -863,7 +864,7 @@ describe('testing getArtifactsToUpdate', () => {
surfaceIds: expect.any(Array),
edgeIds: expect.any(Array),
codeRef: {
range: [231, 254, 0],
range: [231, 254, true],
pathToNode: [['body', '']],
},
},
@ -883,7 +884,7 @@ describe('testing getArtifactsToUpdate', () => {
surfaceIds: expect.any(Array),
edgeIds: expect.any(Array),
codeRef: {
range: [231, 254, 0],
range: [231, 254, true],
pathToNode: [['body', '']],
},
},
@ -903,7 +904,7 @@ describe('testing getArtifactsToUpdate', () => {
surfaceIds: expect.any(Array),
edgeIds: expect.any(Array),
codeRef: {
range: [231, 254, 0],
range: [231, 254, true],
pathToNode: [['body', '']],
},
},

View File

@ -871,15 +871,3 @@ export function codeRefFromRange(range: SourceRange, ast: Program): CodeRef {
pathToNode: getNodePathFromSourceRange(ast, range),
}
}
export function isSolid2D(artifact: Artifact): artifact is solid2D {
return (artifact as solid2D).pathId !== undefined
}
export function isSegment(artifact: Artifact): artifact is SegmentArtifact {
return (artifact as SegmentArtifact).pathId !== undefined
}
export function isSweep(artifact: Artifact): artifact is SweepArtifact {
return (artifact as SweepArtifact).pathId !== undefined
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 357 KiB

After

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 577 KiB

After

Width:  |  Height:  |  Size: 568 KiB

View File

@ -1,4 +1,11 @@
import { defaultSourceRange, SourceRange } from 'lang/wasm'
import {
defaultRustSourceRange,
defaultSourceRange,
Program,
RustSourceRange,
SourceRange,
sourceRangeFromRust,
} from 'lang/wasm'
import { VITE_KC_API_WS_MODELING_URL, VITE_KC_DEV_TOKEN } from 'env'
import { Models } from '@kittycad/lib'
import { exportSave } from 'lib/exportSave'
@ -1302,8 +1309,8 @@ export enum EngineCommandManagerEvents {
interface PendingMessage {
command: EngineCommand
range: SourceRange
idToRangeMap: { [key: string]: SourceRange }
range: RustSourceRange
idToRangeMap: { [key: string]: RustSourceRange }
resolve: (data: [Models['WebSocketResponse_type']]) => void
reject: (reason: string) => void
promise: Promise<[Models['WebSocketResponse_type']]>
@ -1399,7 +1406,6 @@ export class EngineCommandManager extends EventTarget {
}
private makeDefaultPlanes: () => Promise<DefaultPlanes> | null = () => null
private modifyGrid: (hidden: boolean) => Promise<void> | null = () => null
private onEngineConnectionOpened = () => {}
private onEngineConnectionClosed = () => {}
@ -1432,7 +1438,6 @@ export class EngineCommandManager extends EventTarget {
height,
token,
makeDefaultPlanes,
modifyGrid,
settings = {
pool: null,
theme: Themes.Dark,
@ -1452,14 +1457,12 @@ export class EngineCommandManager extends EventTarget {
height: number
token?: string
makeDefaultPlanes: () => Promise<DefaultPlanes>
modifyGrid: (hidden: boolean) => Promise<void>
settings?: SettingsViaQueryString
}) {
if (settings) {
this.settings = settings
}
this.makeDefaultPlanes = makeDefaultPlanes
this.modifyGrid = modifyGrid
if (width === 0 || height === 0) {
return
}
@ -1539,11 +1542,6 @@ export class EngineCommandManager extends EventTarget {
type: 'default_camera_get_settings',
},
})
// We want modify the grid first because we don't want it to flash.
// Ideally these would already be default hidden in engine (TODO do
// that) https://github.com/KittyCAD/engine/issues/2282
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.modifyGrid(!this.settings.showScaleGrid)?.then(async () => {
await this.initPlanes()
setIsStreamReady(true)
@ -1553,7 +1551,6 @@ export class EngineCommandManager extends EventTarget {
detail: this.engineConnection,
})
)
})
}
this.engineConnection.addEventListener(
@ -2003,7 +2000,7 @@ export class EngineCommandManager extends EventTarget {
{
command,
idToRangeMap: {},
range: defaultSourceRange(),
range: defaultRustSourceRange(),
},
true // isSceneCommand
)
@ -2034,9 +2031,9 @@ export class EngineCommandManager extends EventTarget {
return Promise.reject(new Error('rangeStr is undefined'))
if (commandStr === undefined)
return Promise.reject(new Error('commandStr is undefined'))
const range: SourceRange = JSON.parse(rangeStr)
const range: RustSourceRange = JSON.parse(rangeStr)
const command: EngineCommand = JSON.parse(commandStr)
const idToRangeMap: { [key: string]: SourceRange } =
const idToRangeMap: { [key: string]: RustSourceRange } =
JSON.parse(idToRangeStr)
// Current executeAst is stale, going to interrupt, a new executeAst will trigger
@ -2079,10 +2076,14 @@ export class EngineCommandManager extends EventTarget {
if (message.command.type === 'modeling_cmd_req') {
this.orderedCommands.push({
command: message.command,
range: message.range,
range: sourceRangeFromRust(message.range),
})
} else if (message.command.type === 'modeling_cmd_batch_req') {
message.command.requests.forEach((req) => {
const cmdId = req.cmd_id || ''
const range = cmdId
? sourceRangeFromRust(message.idToRangeMap[cmdId])
: defaultSourceRange()
const cmd: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: req.cmd_id,
@ -2090,7 +2091,7 @@ export class EngineCommandManager extends EventTarget {
}
this.orderedCommands.push({
command: cmd,
range: message.idToRangeMap[req.cmd_id || ''],
range,
})
})
}
@ -2109,30 +2110,23 @@ export class EngineCommandManager extends EventTarget {
* When an execution takes place we want to wait until we've got replies for all of the commands
* When this is done when we build the artifact map synchronously.
*/
async waitForAllCommands(useFakeExecutor = false) {
await Promise.all(Object.values(this.pendingCommands).map((a) => a.promise))
setTimeout(() => {
// the ast is wrong without this one tick timeout.
// an example is `Solids should be select and deletable` e2e test will fail
// because the out of date ast messes with selections
// TODO: race condition
if (!this?.kclManager) return
waitForAllCommands() {
return Promise.all(
Object.values(this.pendingCommands).map((a) => a.promise)
)
}
updateArtifactGraph(ast: Program) {
this.artifactGraph = createArtifactGraph({
orderedCommands: this.orderedCommands,
responseMap: this.responseMap,
ast: this.kclManager.ast,
ast,
})
if (useFakeExecutor) {
// mock executions don't produce an artifactGraph, so this will always be empty
// skipping the below logic to wait for the next real execution
return
}
// TODO check if these still need to be deferred once e2e tests are working again.
if (this.artifactGraph.size) {
this.deferredArtifactEmptied(null)
} else {
this.deferredArtifactPopulated(null)
}
})
}
/**
@ -2212,15 +2206,6 @@ export class EngineCommandManager extends EventTarget {
}).catch(reportRejection)
}
/**
* Set the visibility of the scale grid in the engine scene.
* @param visible - whether to show or hide the scale grid
*/
setScaleGridVisibility(visible: boolean) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.modifyGrid(!visible)
}
// Some "objects" have the same source range, such as sketch_mode_start and start_path.
// So when passing a range, we need to also specify the command type
mapRangeToObjectId(

View File

@ -1,14 +1,13 @@
import init, {
parse_wasm,
recast_wasm,
execute_wasm,
execute,
kcl_lint,
modify_ast_for_sketch_wasm,
is_points_ccw,
get_tangential_arc_to_info,
program_memory_init,
make_default_planes,
modify_grid,
coredump,
toml_stringify,
default_app_settings,
@ -43,7 +42,9 @@ import { Environment } from '../wasm-lib/kcl/bindings/Environment'
import { Node } from 'wasm-lib/kcl/bindings/Node'
import { CompilationError } from 'wasm-lib/kcl/bindings/CompilationError'
import { SourceRange as RustSourceRange } from 'wasm-lib/kcl/bindings/SourceRange'
import { getAllCurrentSettings } from 'lib/settings/settingsUtils'
export type { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
export type { Program } from '../wasm-lib/kcl/bindings/Program'
export type { Expr } from '../wasm-lib/kcl/bindings/Expr'
export type { ObjectExpression } from '../wasm-lib/kcl/bindings/ObjectExpression'
@ -64,6 +65,7 @@ export type { BinaryPart } from '../wasm-lib/kcl/bindings/BinaryPart'
export type { Literal } from '../wasm-lib/kcl/bindings/Literal'
export type { LiteralValue } from '../wasm-lib/kcl/bindings/LiteralValue'
export type { ArrayExpression } from '../wasm-lib/kcl/bindings/ArrayExpression'
export type { SourceRange as RustSourceRange } from 'wasm-lib/kcl/bindings/SourceRange'
export type SyntaxType =
| 'Program'
@ -92,16 +94,37 @@ export type { Solid } from '../wasm-lib/kcl/bindings/Solid'
export type { KclValue } from '../wasm-lib/kcl/bindings/KclValue'
export type { ExtrudeSurface } from '../wasm-lib/kcl/bindings/ExtrudeSurface'
/**
* The first two items are the start and end points (byte offsets from the start of the file).
* The third item is whether the source range belongs to the 'main' file, i.e., the file currently
* being rendered/displayed in the editor (TODO we need to handle modules better in the frontend).
*/
export type SourceRange = [number, number, boolean]
/**
* Convert a SourceRange as used inside the KCL interpreter into the above one for use in the
* frontend (essentially we're eagerly checking whether the frontend should care about the SourceRange
* so as not to expose details of the interpreter's current representation of module ids throughout
* the frontend).
*/
export function sourceRangeFromRust(s: RustSourceRange): SourceRange {
return [s[0], s[1], s[2] === 0]
}
/**
* Create a default SourceRange for testing or as a placeholder.
*/
export function defaultSourceRange(): SourceRange {
return [0, 0, true]
}
/**
* Create a default RustSourceRange for testing or as a placeholder.
*/
export function defaultRustSourceRange(): RustSourceRange {
return [0, 0, 0]
}
export const wasmUrl = () => {
// For when we're in electron (file based) or web server (network based)
// For some reason relative paths don't work as expected. Otherwise we would
@ -122,7 +145,7 @@ const initialise = async () => {
const fullUrl = wasmUrl()
const input = await fetch(fullUrl)
const buffer = await input.arrayBuffer()
return await init(buffer)
return await init({ module_or_path: buffer })
} catch (e) {
console.log('Error initialising WASM', e)
return Promise.reject(e)
@ -163,6 +186,10 @@ export class ParseResult {
}
}
/**
* Parsing was successful. There is guaranteed to be an AST and no fatal errors. There may or may
* not be warnings or non-fatal errors.
*/
class SuccessParseResult extends ParseResult {
program: Node<Program>
@ -493,18 +520,19 @@ export const _executor = async (
return Promise.reject(programMemoryOverride)
try {
let baseUnit = 'mm'
let jsAppSettings = default_app_settings()
if (!TEST) {
const getSettingsState = import('components/SettingsAuthProvider').then(
(module) => module.getSettingsState
)
baseUnit =
(await getSettingsState)()?.modeling.defaultUnit.current || 'mm'
const lastSettingsSnapshot = await import(
'components/SettingsAuthProvider'
).then((module) => module.lastSettingsContextSnapshot)
if (lastSettingsSnapshot) {
jsAppSettings = getAllCurrentSettings(lastSettingsSnapshot)
}
const execState: RawExecState = await execute_wasm(
}
const execState: RawExecState = await execute(
JSON.stringify(node),
JSON.stringify(programMemoryOverride?.toRaw() || null),
baseUnit,
JSON.stringify({ settings: jsAppSettings }),
engineCommandManager,
fileSystemManager
)
@ -552,20 +580,6 @@ export const makeDefaultPlanes = async (
}
}
export const modifyGrid = async (
engineCommandManager: EngineCommandManager,
hidden: boolean
): Promise<void> => {
try {
await modify_grid(engineCommandManager, hidden)
return
} catch (e) {
// TODO: do something real with the error.
console.log('modify grid error', e)
return Promise.reject(e)
}
}
export const modifyAstForSketch = async (
engineCommandManager: EngineCommandManager,
ast: Node<Program>,

View File

@ -10,7 +10,7 @@ const noModifiersPressed = (e: MouseEvent) =>
!e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey
export type CameraSystem =
| 'KittyCAD'
| 'Zoo'
| 'OnShape'
| 'Trackpad Friendly'
| 'Solidworks'
@ -19,7 +19,7 @@ export type CameraSystem =
| 'AutoCAD'
export const cameraSystems: CameraSystem[] = [
'KittyCAD',
'Zoo',
'OnShape',
'Trackpad Friendly',
'Solidworks',
@ -34,9 +34,8 @@ export function mouseControlsToCameraSystem(
switch (mouseControl) {
// TODO: understand why the values come back without underscores and fix the root cause
// @ts-ignore: TS2678
case 'kittycad':
case 'kitty_cad':
return 'KittyCAD'
case 'zoo':
return 'Zoo'
// TODO: understand why the values come back without underscores and fix the root cause
// @ts-ignore: TS2678
case 'onshape':
@ -86,7 +85,7 @@ export const btnName = (e: MouseEvent) => ({
})
export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
KittyCAD: {
Zoo: {
pan: {
description: 'Shift + Right click drag or middle click drag',
callback: (e) =>

View File

@ -3,7 +3,6 @@ import { engineCommandManager } from 'lib/singletons'
import { uuidv4 } from 'lib/utils'
import { CommandBarContext } from 'machines/commandBarMachine'
import { Selections } from 'lib/selections'
import { isSolid2D, isSegment, isSweep } from 'lang/std/artifactGraph'
export const disableDryRunWithRetry = async (numberOfRetries = 3) => {
for (let tries = 0; tries < numberOfRetries; tries++) {
@ -64,7 +63,7 @@ export const revolveAxisValidator = async ({
return 'Unable to revolve, sketch not found'
}
if (!(isSolid2D(artifact) || isSegment(artifact) || isSweep(artifact))) {
if (!('pathId' in artifact)) {
return 'Unable to revolve, sketch has no path'
}

View File

@ -118,3 +118,21 @@ export const KCL_AXIS_Y = 'Y'
export const KCL_AXIS_NEG_X = '-X'
export const KCL_AXIS_NEG_Y = '-Y'
export const KCL_DEFAULT_AXIS = 'X'
export enum AxisNames {
X = 'x',
Y = 'y',
Z = 'z',
NEG_X = '-x',
NEG_Y = '-y',
NEG_Z = '-z',
}
/** Semantic names of views from AxisNames */
export const VIEW_NAMES_SEMANTIC = {
[AxisNames.X]: 'Right',
[AxisNames.Y]: 'Back',
[AxisNames.Z]: 'Top',
[AxisNames.NEG_X]: 'Left',
[AxisNames.NEG_Y]: 'Front',
[AxisNames.NEG_Z]: 'Bottom',
} as const

View File

@ -15,6 +15,7 @@ import { fileSystemManager } from 'lang/std/fileSystemManager'
import { getProjectInfo } from './desktop'
import { createSettings } from './settings/initialSettings'
import { normalizeLineEndings } from 'lib/codeEditor'
import { OnboardingStatus } from 'wasm-lib/kcl/bindings/OnboardingStatus'
// The root loader simply resolves the settings and any errors that
// occurred during the settings load
@ -53,14 +54,15 @@ export const telemetryLoader: LoaderFunction = async ({
// Redirect users to the appropriate onboarding page if they haven't completed it
export const onboardingRedirectLoader: ActionFunction = async (args) => {
const { settings } = await loadAndValidateSettings()
const onboardingStatus = settings.app.onboardingStatus.current || ''
const onboardingStatus: OnboardingStatus =
settings.app.onboardingStatus.current || ''
const notEnRouteToOnboarding = !args.request.url.includes(
PATHS.ONBOARDING.INDEX
)
// '' is the initial state, 'done' and 'dismissed' are the final states
// '' is the initial state, 'completed' and 'dismissed' are the final states
const hasValidOnboardingStatus =
onboardingStatus.length === 0 ||
!(onboardingStatus === 'done' || onboardingStatus === 'dismissed')
!(onboardingStatus === 'completed' || onboardingStatus === 'dismissed')
const shouldRedirectToOnboarding =
notEnRouteToOnboarding && hasValidOnboardingStatus

View File

@ -19,6 +19,7 @@ import Tooltip from 'components/Tooltip'
import { toSync } from 'lib/utils'
import { reportRejection } from 'lib/trap'
import { CameraProjectionType } from 'wasm-lib/kcl/bindings/CameraProjectionType'
import { OnboardingStatus } from 'wasm-lib/kcl/bindings/OnboardingStatus'
/**
* A setting that can be set at the user or project level
@ -189,8 +190,10 @@ export function createSettings() {
inputType: 'boolean',
},
}),
onboardingStatus: new Setting<string>({
onboardingStatus: new Setting<OnboardingStatus>({
defaultValue: '',
// TODO: this could be better but we don't have a TS side real enum
// for this yet
validate: (v) => typeof v === 'string',
hideOnPlatform: 'both',
}),
@ -283,7 +286,7 @@ export function createSettings() {
* The controls for how to navigate the 3D view
*/
mouseControls: new Setting<CameraSystem>({
defaultValue: 'KittyCAD',
defaultValue: 'Zoo',
description: 'The controls for how to navigate the 3D view',
validate: (v) => cameraSystems.includes(v as CameraSystem),
hideOnLevel: 'project',

View File

@ -2,6 +2,7 @@ import { DeepPartial } from 'lib/types'
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import {
configurationToSettingsPayload,
getAllCurrentSettings,
projectConfigurationToSettingsPayload,
setSettingsAtLevel,
} from './settingsUtils'
@ -65,3 +66,48 @@ describe(`testing settings initialization`, () => {
expect(settings.app.themeColor.current).toBe('200')
})
})
describe(`testing getAllCurrentSettings`, () => {
it(`returns the correct settings`, () => {
// Set up the settings
let settings = createSettings()
const appConfiguration: DeepPartial<Configuration> = {
settings: {
app: {
appearance: {
theme: 'dark',
color: 190,
},
},
},
}
const projectConfiguration: DeepPartial<Configuration> = {
settings: {
app: {
appearance: {
theme: 'light',
color: 200,
},
},
modeling: {
base_unit: 'ft',
},
},
}
const appSettingsPayload = configurationToSettingsPayload(appConfiguration)
const projectSettingsPayload =
projectConfigurationToSettingsPayload(projectConfiguration)
setSettingsAtLevel(settings, 'user', appSettingsPayload)
setSettingsAtLevel(settings, 'project', projectSettingsPayload)
// Now the test: get all the settings' current resolved values
const allCurrentSettings = getAllCurrentSettings(settings)
// This one gets the 'user'-level theme because it's ignored at the project level
// (see the test "doesn't read theme from project settings")
expect(allCurrentSettings.app.theme).toBe('dark')
expect(allCurrentSettings.app.themeColor).toBe('200')
expect(allCurrentSettings.modeling.defaultUnit).toBe('ft')
})
})

View File

@ -286,6 +286,27 @@ export function getChangedSettingsAtLevel(
return changedSettings
}
export function getAllCurrentSettings(
allSettings: typeof settings
): SaveSettingsPayload {
const currentSettings = {} as SaveSettingsPayload
Object.entries(allSettings).forEach(([category, settingsCategory]) => {
const categoryKey = category as keyof typeof settings
Object.entries(settingsCategory).forEach(
([setting, settingValue]: [string, Setting]) => {
const settingKey =
setting as keyof (typeof settings)[typeof categoryKey]
currentSettings[categoryKey] = {
...currentSettings[categoryKey],
[settingKey]: settingValue.current,
}
}
)
})
return currentSettings
}
export function setSettingsAtLevel(
allSettings: typeof settings,
level: SettingsLevel,

View File

@ -112,9 +112,6 @@ export async function executor(
makeDefaultPlanes: () => {
return new Promise((resolve) => resolve(defaultPlanes))
},
modifyGrid: (hidden: boolean) => {
return new Promise((resolve) => resolve())
},
})
return new Promise((resolve) => {

View File

@ -42,8 +42,6 @@ export const settingsMachine = setup({
setClientTheme: () => {},
'Execute AST': () => {},
toastSuccess: () => {},
setEngineEdges: () => {},
setEngineScaleGridVisibility: () => {},
setClientSideSceneUnits: () => {},
persistSettings: () => {},
resetSettings: assign(({ context, event }) => {
@ -172,7 +170,7 @@ export const settingsMachine = setup({
'set.modeling.highlightEdges': {
target: 'persisting settings',
actions: ['setSettingAtLevel', 'toastSuccess', 'setEngineEdges'],
actions: ['setSettingAtLevel', 'toastSuccess', 'Execute AST'],
},
'Reset settings': {
@ -201,11 +199,7 @@ export const settingsMachine = setup({
'set.modeling.showScaleGrid': {
target: 'persisting settings',
actions: [
'setSettingAtLevel',
'toastSuccess',
'setEngineScaleGridVisibility',
],
actions: ['setSettingAtLevel', 'toastSuccess', 'Execute AST'],
},
},
},

View File

@ -23,15 +23,6 @@ import argvFromYargs from './commandLineArgs'
let mainWindow: BrowserWindow | null = null
// Supporting multiple instances instead of multiple applications
let cmdQPressed = false
const instances: BrowserWindow[] = []
const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
app.quit()
process.exit(0)
}
// Check the command line arguments for a project path
const args = parseCLIArgs()
@ -53,11 +44,6 @@ process.env.VITE_KC_SITE_BASE_URL ??= 'https://zoo.dev'
process.env.VITE_KC_SKIP_AUTH ??= 'false'
process.env.VITE_KC_CONNECTION_TIMEOUT_MS ??= '15000'
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (require('electron-squirrel-startup')) {
app.quit()
}
const ZOO_STUDIO_PROTOCOL = 'zoo-studio'
/// Register our application to handle all "electron-fiddle://" protocols.
@ -126,34 +112,16 @@ const createWindow = (filePath?: string): BrowserWindow => {
newWindow.show()
instances.push(newWindow)
return newWindow
}
// before-quit with multiple instances
if (process.platform === 'darwin') {
// Quit from the dock context menu should quit the application directly
app.on('before-quit', () => {
cmdQPressed = true
})
}
// Quit when all windows are closed, even on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q, but it is a really weird behavior with our app.
// app.on('window-all-closed', () => {
// app.quit()
// })
app.on('window-all-closed', () => {
if (cmdQPressed || process.platform !== 'darwin') {
app.quit()
}
})
// Various actions can trigger this event, such as launching the application for the first time,
// attempting to re-launch the application when it's already running, or clicking on the application's dock or taskbar icon.
app.on('activate', () => createWindow())
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
@ -162,10 +130,6 @@ app.on('ready', (event, data) => {
mainWindow = createWindow()
})
// This event will be emitted inside the primary instance of your application when a second instance
// has been executed and calls app.requestSingleInstanceLock().
app.on('second-instance', (event, argv, workingDirectory) => createWindow())
// For now there is no good reason to separate these out to another file(s)
// There is just not enough code to warrant it and further abstracts everything
// which is already quite abstracted
@ -287,6 +251,9 @@ export function getAutoUpdater(): AppUpdater {
app.on('ready', () => {
const autoUpdater = getAutoUpdater()
// TODO: we're getting `Error: Response ends without calling any handlers` with our setup,
// so at the moment this isn't worth enabling
autoUpdater.disableDifferentialDownload = true
setTimeout(() => {
autoUpdater.checkForUpdates().catch(reportRejection)
}, 1000)

View File

@ -1,4 +1,6 @@
export const onboardingPaths = {
import { OnboardingStatus } from 'wasm-lib/kcl/bindings/OnboardingStatus'
export const onboardingPaths: Record<string, OnboardingStatus> = {
INDEX: '/',
CAMERA: '/camera',
STREAMING: '/streaming',

View File

@ -32,9 +32,11 @@ export const PACKAGE_NAME = isDesktop()
export const IS_NIGHTLY = PACKAGE_NAME.indexOf('-nightly') > -1
export const RELEASE_URL = `https://github.com/KittyCAD/modeling-app/releases/tag/${
export function getReleaseUrl(version: string = APP_VERSION) {
return `https://github.com/KittyCAD/modeling-app/releases/tag/${
IS_NIGHTLY ? 'nightly-' : ''
}v${APP_VERSION}`
}v${version}`
}
export const Settings = () => {
const navigate = useNavigate()

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

@ -121,9 +121,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.93"
version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775"
checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7"
dependencies = [
"backtrace",
]
@ -401,9 +401,9 @@ dependencies = [
[[package]]
name = "chrono"
version = "0.4.38"
version = "0.4.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
dependencies = [
"android-tzdata",
"iana-time-zone",
@ -723,7 +723,7 @@ dependencies = [
[[package]]
name = "derive-docs"
version = "0.1.32"
version = "0.1.33"
dependencies = [
"Inflector",
"anyhow",
@ -1112,7 +1112,7 @@ dependencies = [
"fnv",
"futures-core",
"futures-sink",
"http 1.1.0",
"http 1.2.0",
"indexmap 2.7.0",
"slab",
"tokio",
@ -1215,9 +1215,9 @@ dependencies = [
[[package]]
name = "http"
version = "1.1.0"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea"
dependencies = [
"bytes",
"fnv",
@ -1242,7 +1242,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
"bytes",
"http 1.1.0",
"http 1.2.0",
]
[[package]]
@ -1253,7 +1253,7 @@ checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
dependencies = [
"bytes",
"futures-util",
"http 1.1.0",
"http 1.2.0",
"http-body 1.0.1",
"pin-project-lite",
]
@ -1303,7 +1303,7 @@ dependencies = [
"futures-channel",
"futures-util",
"h2",
"http 1.1.0",
"http 1.2.0",
"http-body 1.0.1",
"httparse",
"itoa",
@ -1320,7 +1320,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333"
dependencies = [
"futures-util",
"http 1.1.0",
"http 1.2.0",
"hyper 1.4.1",
"hyper-util",
"rustls",
@ -1340,7 +1340,7 @@ dependencies = [
"bytes",
"futures-channel",
"futures-util",
"http 1.1.0",
"http 1.2.0",
"http-body 1.0.1",
"hyper 1.4.1",
"pin-project-lite",
@ -1674,16 +1674,17 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "js-sys"
version = "0.3.72"
version = "0.3.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9"
checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "kcl-lib"
version = "0.2.28"
version = "0.2.29"
dependencies = [
"anyhow",
"approx 0.5.1",
@ -1705,7 +1706,7 @@ dependencies = [
"git_rev",
"gltf-json",
"handlebars",
"http 1.1.0",
"http 1.2.0",
"iai",
"image",
"indexmap 2.7.0",
@ -1721,7 +1722,9 @@ dependencies = [
"parse-display 0.9.1",
"pretty_assertions",
"pyo3",
"regex",
"reqwest",
"rgba_simple",
"ropey",
"schemars",
"serde",
@ -1749,7 +1752,7 @@ dependencies = [
[[package]]
name = "kcl-test-server"
version = "0.1.18"
version = "0.1.19"
dependencies = [
"anyhow",
"hyper 0.14.30",
@ -1790,7 +1793,7 @@ dependencies = [
"data-encoding",
"format_serde_error",
"futures",
"http 1.1.0",
"http 1.2.0",
"itertools 0.13.0",
"log",
"mime_guess",
@ -1816,9 +1819,9 @@ dependencies = [
[[package]]
name = "kittycad-modeling-cmds"
version = "0.2.77"
version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b77259b37acafa360d98af27431ac394bc8899eeed7037513832ddbee856811"
checksum = "10a9cab4476455be70ea57643c31444068b056d091bd348cab6044c0d8ad7fcc"
dependencies = [
"anyhow",
"chrono",
@ -1826,7 +1829,7 @@ dependencies = [
"enum-iterator",
"enum-iterator-derive",
"euler",
"http 1.1.0",
"http 1.2.0",
"kittycad-modeling-cmds-macros",
"kittycad-unit-conversion-derive",
"measurements",
@ -2860,7 +2863,7 @@ dependencies = [
"futures-core",
"futures-util",
"h2",
"http 1.1.0",
"http 1.2.0",
"http-body 1.0.1",
"http-body-util",
"hyper 1.4.1",
@ -2902,7 +2905,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f67ad7fdf5c0a015763fcd164bee294b13fb7b6f89f1b55961d40f00c3e32d6b"
dependencies = [
"async-trait",
"http 1.1.0",
"http 1.2.0",
"reqwest",
"reqwest-middleware",
]
@ -2915,7 +2918,7 @@ checksum = "d1ccd3b55e711f91a9885a2fa6fbbb2e39db1776420b062efc058c6410f7e5e3"
dependencies = [
"anyhow",
"async-trait",
"http 1.1.0",
"http 1.2.0",
"reqwest",
"serde",
"thiserror 1.0.68",
@ -2932,7 +2935,7 @@ dependencies = [
"async-trait",
"futures",
"getrandom",
"http 1.1.0",
"http 1.2.0",
"hyper 1.4.1",
"parking_lot 0.11.2",
"reqwest",
@ -2953,7 +2956,7 @@ dependencies = [
"anyhow",
"async-trait",
"getrandom",
"http 1.1.0",
"http 1.2.0",
"matchit",
"opentelemetry",
"reqwest",
@ -2971,6 +2974,12 @@ dependencies = [
"rand 0.8.5",
]
[[package]]
name = "rgba_simple"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6cd655523701785087f69900df39892fb7b9b0721aa67682f571c38c32ac58a"
[[package]]
name = "ring"
version = "0.17.8"
@ -3191,9 +3200,9 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
[[package]]
name = "serde"
version = "1.0.215"
version = "1.0.216"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f"
checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e"
dependencies = [
"serde_derive",
]
@ -3209,9 +3218,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.215"
version = "1.0.216"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0"
checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e"
dependencies = [
"proc-macro2",
"quote",
@ -4019,7 +4028,7 @@ dependencies = [
"byteorder",
"bytes",
"data-encoding",
"http 1.1.0",
"http 1.2.0",
"httparse",
"log",
"rand 0.8.5",
@ -4227,9 +4236,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.95"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e"
checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396"
dependencies = [
"cfg-if",
"once_cell",
@ -4238,13 +4247,12 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.95"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358"
checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.87",
@ -4253,22 +4261,23 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.44"
version = "0.4.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65471f79c1022ffa5291d33520cbbb53b7687b01c2f8e83b57d102eed7ed479d"
checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2"
dependencies = [
"cfg-if",
"futures-core",
"js-sys",
"once_cell",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.95"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56"
checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@ -4276,9 +4285,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.95"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68"
checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2"
dependencies = [
"proc-macro2",
"quote",
@ -4289,9 +4298,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.95"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d"
checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6"
[[package]]
name = "wasm-lib"
@ -4353,9 +4362,9 @@ dependencies = [
[[package]]
name = "web-sys"
version = "0.3.72"
version = "0.3.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112"
checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc"
dependencies = [
"js-sys",
"wasm-bindgen",

View File

@ -20,8 +20,8 @@ serde_json = "1.0.128"
tokio = { version = "1.41.1", features = ["sync"] }
toml = "0.8.19"
uuid = { version = "1.11.0", features = ["v4", "js", "serde"] }
wasm-bindgen = "0.2.91"
wasm-bindgen-futures = "0.4.44"
wasm-bindgen = "0.2.99"
wasm-bindgen-futures = "0.4.49"
[dev-dependencies]
anyhow = "1"
@ -43,7 +43,7 @@ wasm-bindgen-futures = { version = "0.4.44", features = ["futures-core-03-stream
wasm-streams = "0.4.1"
[target.'cfg(target_arch = "wasm32")'.dependencies.web-sys]
version = "0.3.72"
version = "0.3.76"
features = [
"console",
"HtmlTextAreaElement",
@ -76,7 +76,7 @@ members = [
[workspace.dependencies]
http = "1"
kittycad = { version = "0.3.28", default-features = false, features = ["js", "requests"] }
kittycad-modeling-cmds = { version = "0.2.77", features = ["websocket"] }
kittycad-modeling-cmds = { version = "0.2.79", features = ["websocket"] }
[workspace.lints.clippy]
assertions_on_result_states = "warn"

View File

@ -1,7 +1,7 @@
[package]
name = "derive-docs"
description = "A tool for generating documentation from Rust derive macros"
version = "0.1.32"
version = "0.1.33"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -6,6 +6,8 @@
mod tests;
mod unbox;
use std::collections::HashMap;
use convert_case::Casing;
use inflector::Inflector;
use once_cell::sync::Lazy;
@ -47,6 +49,10 @@ struct StdlibMetadata {
/// If false, all arguments require labels.
#[serde(default)]
unlabeled_first: bool,
/// Key = argument name, value = argument doc.
#[serde(default)]
arg_docs: HashMap<String, String>,
}
#[proc_macro_attribute]
@ -282,6 +288,17 @@ fn do_stdlib_inner(
let ty_string = rust_type_to_openapi_type(&ty_string);
let required = !ty_ident.to_string().starts_with("Option <");
let description = if let Some(s) = metadata.arg_docs.get(&arg_name) {
quote! { #s }
} else if metadata.keywords && ty_string != "Args" && ty_string != "ExecState" {
errors.push(Error::new_spanned(
&arg,
"Argument was not documented in the arg_docs block",
));
continue;
} else {
quote! { String::new() }
};
let label_required = !(i == 0 && metadata.unlabeled_first);
if ty_string != "ExecState" && ty_string != "Args" {
let schema = quote! {
@ -294,6 +311,7 @@ fn do_stdlib_inner(
schema: #schema,
required: #required,
label_required: #label_required,
description: #description.to_string(),
}
});
}
@ -355,6 +373,7 @@ fn do_stdlib_inner(
schema,
required: true,
label_required: true,
description: String::new(),
})
}
} else {

View File

@ -116,6 +116,9 @@ fn test_stdlib_line_to() {
let (item, errors) = do_stdlib(
quote! {
name = "lineTo",
arg_docs = {
sketch = "the sketch you're adding the line to"
}
},
quote! {
/// This is some function.

View File

@ -91,6 +91,7 @@ impl crate::docs::StdLibFn for SomeFn {
schema: generator.root_schema_for::<Foo>(),
required: true,
label_required: true,
description: String::new().to_string(),
}]
}
@ -105,6 +106,7 @@ impl crate::docs::StdLibFn for SomeFn {
schema,
required: true,
label_required: true,
description: String::new(),
})
}

View File

@ -91,6 +91,7 @@ impl crate::docs::StdLibFn for SomeFn {
schema: generator.root_schema_for::<str>(),
required: true,
label_required: true,
description: String::new().to_string(),
}]
}
@ -105,6 +106,7 @@ impl crate::docs::StdLibFn for SomeFn {
schema,
required: true,
label_required: true,
description: String::new(),
})
}

View File

@ -129,6 +129,7 @@ impl crate::docs::StdLibFn for Show {
schema: generator.root_schema_for::<[f64; 2usize]>(),
required: true,
label_required: true,
description: String::new().to_string(),
}]
}
@ -143,6 +144,7 @@ impl crate::docs::StdLibFn for Show {
schema,
required: true,
label_required: true,
description: String::new(),
})
}

View File

@ -92,6 +92,7 @@ impl crate::docs::StdLibFn for Show {
schema: generator.root_schema_for::<f64>(),
required: true,
label_required: true,
description: String::new().to_string(),
}]
}
@ -106,6 +107,7 @@ impl crate::docs::StdLibFn for Show {
schema,
required: true,
label_required: true,
description: String::new(),
})
}

View File

@ -130,6 +130,7 @@ impl crate::docs::StdLibFn for MyFunc {
schema: generator.root_schema_for::<Option<kittycad::types::InputFormat>>(),
required: false,
label_required: true,
description: String::new().to_string(),
}]
}
@ -144,6 +145,7 @@ impl crate::docs::StdLibFn for MyFunc {
schema,
required: true,
label_required: true,
description: String::new(),
})
}

View File

@ -131,6 +131,7 @@ impl crate::docs::StdLibFn for LineTo {
schema: generator.root_schema_for::<LineToData>(),
required: true,
label_required: true,
description: String::new().to_string(),
},
crate::docs::StdLibFnArg {
name: "sketch".to_string(),
@ -138,6 +139,7 @@ impl crate::docs::StdLibFn for LineTo {
schema: generator.root_schema_for::<Sketch>(),
required: true,
label_required: true,
description: "the sketch you're adding the line to".to_string(),
},
]
}
@ -153,6 +155,7 @@ impl crate::docs::StdLibFn for LineTo {
schema,
required: true,
label_required: true,
description: String::new(),
})
}

View File

@ -129,6 +129,7 @@ impl crate::docs::StdLibFn for Min {
schema: generator.root_schema_for::<Vec<f64>>(),
required: true,
label_required: true,
description: String::new().to_string(),
}]
}
@ -143,6 +144,7 @@ impl crate::docs::StdLibFn for Min {
schema,
required: true,
label_required: true,
description: String::new(),
})
}

View File

@ -92,6 +92,7 @@ impl crate::docs::StdLibFn for Show {
schema: generator.root_schema_for::<Option<f64>>(),
required: false,
label_required: true,
description: String::new().to_string(),
}]
}
@ -106,6 +107,7 @@ impl crate::docs::StdLibFn for Show {
schema,
required: true,
label_required: true,
description: String::new(),
})
}

View File

@ -92,6 +92,7 @@ impl crate::docs::StdLibFn for Import {
schema: generator.root_schema_for::<Option<kittycad::types::InputFormat>>(),
required: false,
label_required: true,
description: String::new().to_string(),
}]
}
@ -106,6 +107,7 @@ impl crate::docs::StdLibFn for Import {
schema,
required: true,
label_required: true,
description: String::new(),
})
}

View File

@ -92,6 +92,7 @@ impl crate::docs::StdLibFn for Import {
schema: generator.root_schema_for::<Option<kittycad::types::InputFormat>>(),
required: false,
label_required: true,
description: String::new().to_string(),
}]
}
@ -106,6 +107,7 @@ impl crate::docs::StdLibFn for Import {
schema,
required: true,
label_required: true,
description: String::new(),
})
}

View File

@ -92,6 +92,7 @@ impl crate::docs::StdLibFn for Import {
schema: generator.root_schema_for::<Option<kittycad::types::InputFormat>>(),
required: false,
label_required: true,
description: String::new().to_string(),
}]
}
@ -106,6 +107,7 @@ impl crate::docs::StdLibFn for Import {
schema,
required: true,
label_required: true,
description: String::new(),
})
}

View File

@ -92,6 +92,7 @@ impl crate::docs::StdLibFn for Show {
schema: generator.root_schema_for::<Vec<f64>>(),
required: true,
label_required: true,
description: String::new().to_string(),
}]
}
@ -106,6 +107,7 @@ impl crate::docs::StdLibFn for Show {
schema,
required: true,
label_required: true,
description: String::new(),
})
}

View File

@ -99,6 +99,7 @@ impl crate::docs::StdLibFn for SomeFunction {
schema,
required: true,
label_required: true,
description: String::new(),
})
}

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-test-server"
description = "A test server for KCL"
version = "0.1.18"
version = "0.1.19"
edition = "2021"
license = "MIT"

View File

@ -189,7 +189,11 @@ impl EngineConnection {
uuid_to_cpp(path_id)
)
}
kcmc::ModelingCmd::Extrude(kcmc::Extrude { distance, target }) => {
kcmc::ModelingCmd::Extrude(kcmc::Extrude {
distance,
target,
faces: _, // Engine team: start using this once the frontend and engine both use it.
}) => {
format!(
r#"
scene->getSceneObject(Utils::UUID("{target}"))->extrudeToSolid3D({} * scaleFactor, true);

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-lib"
description = "KittyCAD Language implementation and tools"
version = "0.2.28"
version = "0.2.29"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"
@ -22,7 +22,7 @@ clap = { version = "4.5.21", default-features = false, optional = true, features
] }
convert_case = "0.6.0"
dashmap = "6.1.0"
derive-docs = { version = "0.1.32", path = "../derive-docs" }
derive-docs = { version = "0.1.33", path = "../derive-docs" }
dhat = { version = "0.3", optional = true }
fnv = "1.0.7"
form_urlencoded = "1.2.1"
@ -40,10 +40,12 @@ miette = "7.2.0"
mime_guess = "2.0.5"
parse-display = "0.9.1"
pyo3 = { version = "0.22.6", optional = true }
regex = "1.11.1"
reqwest = { version = "0.12", default-features = false, features = [
"stream",
"rustls-tls",
] }
rgba_simple = "0.10.0"
ropey = "1.6.1"
schemars = { version = "0.8.17", features = [
"impl_json_schema",
@ -80,9 +82,9 @@ tokio = { version = "1.41.1", features = ["sync", "time"] }
tower-lsp = { version = "0.20.0", default-features = false, features = [
"runtime-agnostic",
] }
wasm-bindgen = "0.2.91"
wasm-bindgen-futures = "0.4.44"
web-sys = { version = "0.3.72", features = ["console"] }
wasm-bindgen = "0.2.99"
wasm-bindgen-futures = "0.4.49"
web-sys = { version = "0.3.76", features = ["console"] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
approx = "0.5"

View File

@ -597,6 +597,8 @@ fn clean_function_name(name: &str) -> String {
fn_name = fn_name.replace("seg_", "segment_");
} else if fn_name.starts_with("log_") {
fn_name = fn_name.replace("log_", "log");
} else if fn_name.ends_with("tan_2") {
fn_name = fn_name.replace("tan_2", "tan2");
}
fn_name

View File

@ -13,6 +13,8 @@ use tower_lsp::lsp_types::{
MarkupKind, ParameterInformation, ParameterLabel, SignatureHelp, SignatureInformation,
};
use crate::execution::Sketch;
use crate::std::Primitive;
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, ts_rs::TS)]
@ -57,6 +59,12 @@ pub struct StdLibFnArg {
pub schema: schemars::schema::RootSchema,
/// If the argument is required.
pub required: bool,
/// Additional information that could be used instead of the type's description.
/// This is helpful if the type is really basic, like "u32" -- that won't tell the user much about
/// how this argument is meant to be used.
/// Empty string means this has no docs.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub description: String,
/// Even in functions that use keyword arguments, not every parameter requires a label (most do though).
/// Some functions allow one unlabeled parameter, which has to be first in the
/// argument list.
@ -104,6 +112,11 @@ impl StdLibFnArg {
}
pub fn description(&self) -> Option<String> {
// Check if we explicitly gave this stdlib arg a description.
if !self.description.is_empty() {
return Some(self.description.clone());
}
// If not, then try to get something meaningful from the schema.
get_description_string_from_schema(&self.schema.clone())
}
}
@ -232,6 +245,11 @@ pub trait StdLibFn: std::fmt::Debug + Send + Sync {
}
fn to_autocomplete_snippet(&self) -> Result<String> {
if self.name() == "loft" {
return Ok("loft([${0:sketch000}, ${1:sketch001}])${}".to_string());
} else if self.name() == "hole" {
return Ok("hole(${0:holeSketch}, ${1:%})${}".to_string());
}
let mut args = Vec::new();
let mut index = 0;
for arg in self.args(true).iter() {
@ -451,6 +469,16 @@ fn get_autocomplete_snippet_from_schema(
) -> Result<Option<(usize, String)>> {
match schema {
schemars::schema::Schema::Object(o) => {
// Check if the schema is the same as a Sketch.
let mut settings = schemars::gen::SchemaSettings::openapi3();
// We set this so we can recurse them later.
settings.inline_subschemas = true;
let mut generator = schemars::gen::SchemaGenerator::new(settings);
let sketch_schema = generator.root_schema_for::<Sketch>().schema;
if sketch_schema.object == o.object {
return Ok(Some((index, format!("${{{}:sketch{}}}", index, "000"))));
}
if let Some(serde_json::Value::Bool(nullable)) = o.extensions.get("nullable") {
if *nullable {
return Ok(None);
@ -489,6 +517,12 @@ fn get_autocomplete_snippet_from_schema(
continue;
}
if prop_name == "color" {
fn_docs.push_str(&format!("\t{}: ${{{}:\"#ff0000\"}},\n", prop_name, i));
i += 1;
continue;
}
if let Some((new_index, snippet)) = get_autocomplete_snippet_from_schema(prop, i)? {
fn_docs.push_str(&format!("\t{}: {},\n", prop_name, snippet));
i = new_index + 1;
@ -946,6 +980,47 @@ mod tests {
);
}
#[test]
fn get_autocomplete_snippet_appearance() {
let appearance_fn: Box<dyn StdLibFn> = Box::new(crate::std::appearance::Appearance);
let snippet = appearance_fn.to_autocomplete_snippet().unwrap();
assert_eq!(
snippet,
r#"appearance({
color: ${0:"#
.to_owned()
+ "\"#"
+ r#"ff0000"},
}, ${1:%})${}"#
);
}
#[test]
fn get_autocomplete_snippet_loft() {
let loft_fn: Box<dyn StdLibFn> = Box::new(crate::std::loft::Loft);
let snippet = loft_fn.to_autocomplete_snippet().unwrap();
assert_eq!(snippet, r#"loft([${0:sketch000}, ${1:sketch001}])${}"#);
}
#[test]
fn get_autocomplete_snippet_sweep() {
let sweep_fn: Box<dyn StdLibFn> = Box::new(crate::std::sweep::Sweep);
let snippet = sweep_fn.to_autocomplete_snippet().unwrap();
assert_eq!(
snippet,
r#"sweep({
path: ${0:sketch000},
}, ${1:%})${}"#
);
}
#[test]
fn get_autocomplete_snippet_hole() {
let hole_fn: Box<dyn StdLibFn> = Box::new(crate::std::sketch::Hole);
let snippet = hole_fn.to_autocomplete_snippet().unwrap();
assert_eq!(snippet, r#"hole(${0:holeSketch}, ${1:%})${}"#);
}
// We want to test the snippets we compile at lsp start.
#[test]
fn get_all_stdlib_autocomplete_snippets() {

View File

@ -120,6 +120,61 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
Ok(())
}
/// Set the visibility of edges.
async fn set_edge_visibility(
&self,
visible: bool,
source_range: SourceRange,
) -> Result<(), crate::errors::KclError> {
self.batch_modeling_cmd(
uuid::Uuid::new_v4(),
source_range,
&ModelingCmd::from(mcmd::EdgeLinesVisible { hidden: !visible }),
)
.await?;
Ok(())
}
async fn set_units(
&self,
units: crate::UnitLength,
source_range: SourceRange,
) -> Result<(), crate::errors::KclError> {
// Before we even start executing the program, set the units.
self.batch_modeling_cmd(
uuid::Uuid::new_v4(),
source_range,
&ModelingCmd::from(mcmd::SetSceneUnits { unit: units.into() }),
)
.await?;
Ok(())
}
/// Re-run the command to apply the settings.
async fn reapply_settings(
&self,
settings: &crate::ExecutorSettings,
source_range: SourceRange,
) -> Result<(), crate::errors::KclError> {
// Set the edge visibility.
self.set_edge_visibility(settings.highlight_edges, source_range).await?;
// Change the units.
self.set_units(settings.units, source_range).await?;
// Send the command to show the grid.
self.modify_grid(!settings.show_grid, source_range).await?;
// We do not have commands for changing ssao on the fly.
// Flush the batch queue, so the settings are applied right away.
self.flush_batch(false, source_range).await?;
Ok(())
}
// Add a modeling command to the batch but don't fire it right away.
async fn batch_modeling_cmd(
&self,
@ -504,11 +559,11 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
}))
}
async fn modify_grid(&self, hidden: bool) -> Result<(), KclError> {
async fn modify_grid(&self, hidden: bool, source_range: SourceRange) -> Result<(), KclError> {
// Hide/show the grid.
self.batch_modeling_cmd(
uuid::Uuid::new_v4(),
Default::default(),
source_range,
&ModelingCmd::from(mcmd::ObjectVisible {
hidden,
object_id: *GRID_OBJECT_ID,
@ -519,7 +574,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
// Hide/show the grid scale text.
self.batch_modeling_cmd(
uuid::Uuid::new_v4(),
Default::default(),
source_range,
&ModelingCmd::from(mcmd::ObjectVisible {
hidden,
object_id: *GRID_SCALE_TEXT_OBJECT_ID,
@ -527,8 +582,6 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
)
.await?;
self.flush_batch(false, Default::default()).await?;
Ok(())
}

View File

@ -0,0 +1,50 @@
//! Functions for helping with caching an ast and finding the parts the changed.
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
execution::ExecState,
parsing::ast::types::{Node, Program},
};
/// Information for the caching an AST and smartly re-executing it if we can.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct CacheInformation {
/// The old information.
pub old: Option<OldAstState>,
/// The new ast to executed.
pub new_ast: Node<Program>,
}
/// The old ast and program memory.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct OldAstState {
/// The ast.
pub ast: Node<Program>,
/// The exec state.
pub exec_state: ExecState,
/// The last settings used for execution.
pub settings: crate::execution::ExecutorSettings,
}
impl From<crate::Program> for CacheInformation {
fn from(program: crate::Program) -> Self {
CacheInformation {
old: None,
new_ast: program.ast,
}
}
}
/// The result of a cache check.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct CacheResult {
/// Should we clear the scene and start over?
pub clear_scene: bool,
/// The program that needs to be executed.
pub program: Node<Program>,
}

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