Compare commits

...

22 Commits

Author SHA1 Message Date
9a3ac64603 KCL: Appearance stdlib fn is now kwargs (#5308) 2025-02-07 16:36:51 -06:00
67e60cb832 fix: remove noisy log line in linux (#5306)
fix: remove noisy log line

Co-authored-by: Tom Pridham <pridham.tom@gmail.com>
2025-02-07 19:23:06 +00:00
110037df79 Fix: update sweep snapshots code after kwargs merge (#5307)
* Fix: update sweep snapshots code after kwargs merge

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

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-07 13:51:28 -05:00
30b1dae38a KCL: Sweep stdlib fn now uses keyword args (#5300)
Before:
```
|> sweep({ path = myPath }, %)
```

After:
```
|> sweep(path = myPath)
```
2025-02-07 18:35:04 +00:00
f20fc5b467 Remove units from share link flow, enable it on nightly (#5304)
* Remove units from the share link flow

They should rely on inline settings annotations

* @pierremtb's fix to turn on the menu item in nightly

* fmt

* Don't show web banner if the create file query param is present

* Change copy to 'Share current part (via Zoo link)'

---------

Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
Co-authored-by: Pierre Jacquier <pierre@zoo.dev>
2025-02-07 18:19:56 +00:00
1bfc3a0a3c CM KCL: add named args to fn calls (#5303)
* CM KCL: add named args to fn calls

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

* Rename to match other code

---------

Co-authored-by: Matt Mundell <matt@mundell.me>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-07 16:47:45 +00:00
f6e975db84 Add a "back" button to the onboarding buttons, move the dismiss button to a little corner x button (#5296)
* Add previous button to OnboardingButtons, move dismiss to popover corner

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

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

* Clean up diagnostics

I am thoroughly enjoying nvim now

* Amend "click through" test to also click back

* fmt

* Set this test back to fixme, that work should be its own PR

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-07 11:30:36 -05:00
b82eec85fd Add basic axis case for Helix point-and-click (#5240)
* Move Helix button to a section with offset plane (3d 'construction' elements)
Fixes #5234

* Add generix axis case for Helix point-and-click
Fixes #5072 #5236 #5073

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

* Trigger CI

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

* Trigger CI

* Clean up

* Temp remove point and click test

* Add back point and click test

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

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

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

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

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

* Clean up

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

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

* Fix helix arg after change to kwargs

* More fixes wrt helix arg after change to kwargs

* Fixed thanks to @adamchalmers

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-07 10:16:36 -05:00
5dc4213295 let users have big editors (#5295)
* let users have big editor

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

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

* Update src/components/ModelingSidebar/ModelingSidebar.tsx

Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
2025-02-07 05:19:09 -05:00
61807e7629 KCL: Make shell stdlib fn use keyword arguments (#5293)
Before:

```
shell({ faces = ['end'], thickness = caseThickness }, case)
```

After:
```
shell(case, faces = ['end'], thickness = caseThickness)
```
2025-02-07 02:03:12 +00:00
357bbffce5 refactor: Rename function to not have unneeded prefix (#5292) 2025-02-06 19:54:58 -05:00
6ac9c49773 KCL: Patterns of patterns can use the original sketch/solid as target (#5284)
Right now, if you model something like this box with a button:

<img width="413" alt="Screenshot 2025-02-06 at 3 08 03 PM" src="https://github.com/user-attachments/assets/04818a70-7cf3-4ee3-b8c5-df5959ac10db" />

Let's say you want to pattern the button, and repeat it a second time. If you try, you'll actually pattern the entire model (box + button).

<img width="486" alt="Screenshot 2025-02-06 at 3 08 52 PM" src="https://github.com/user-attachments/assets/09fc28d9-5d80-4ab3-b4dc-b8de2945fcba" />

Why? Because right now, when you sketch on a face (like the button was), both the box and the button share the same ID. All extrusions from a solid will share the same ID, because they all refer to the same composite solid.

This is helpful in some ways -- arguably the solid _is_ just one big complex shape now -- but it's not helpful in other ways. What if I want to only pattern the button? Luckily there's an original ID for the button part, which is still stored. So we just need a way to tell the pattern stdlib functions whether to use the target's main ID or its original ID. This PR adds a new optional bool, `useOriginal`, to patterns. It's false by default, to keep backwards-compatibility (make sure that old KCL code doesn't change).

This PR is based on https://github.com/KittyCAD/modeling-app/pull/3914. It's based on work Serena and I are doing to fix a bug (engine does not allow patterning a 3D solid which was sketched on a face of another solid). @gserena01 our test program is now:

```
w = 400

case = startSketchOn('XY')
  |> startProfileAt([-w, -w], %)
  |> line(endAbsolute = [-w, w])
  |> line(endAbsolute = [w, -w])
  |> line(endAbsolute = [-w, -w])
  |> close()
  |> extrude(length = 200)

bump1 = startSketchOn(case, 'end')
  |> circle({ center = [-50, -50], radius = 40 }, %)
  |> extrude(length = 20)

// We pass in "bump1" here since we want to pattern just this object on the face.
useOriginal = true
target = bump1
transform = {
  axis = [1, 0, 0],
  instances = 3,
  distance = -100
}
patternLinear3d(transform, target, useOriginal)
```

If you change the `useOriginal = true` to `false` you can see the difference.
2025-02-06 17:46:47 -06:00
020497cde2 Don't close the command palette on backspace (#5291)
* Don't close the command palette on backspace

It leads to awkward UX like the one reported in #5007. Users can still
go back between arguments using <kbd>Backspace</kbd> on an empty
argument, but to close it they have to hit <kbd>Escape</kbd>,
<kbd>Cmd+K</kbd>, or click the close button.

* fmt
'
2025-02-06 16:28:35 -05:00
688852a5df Bump @types/mocha from 10.0.7 to 10.0.10 (#5028)
Bumps [@types/mocha](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/mocha) from 10.0.7 to 10.0.10.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/mocha)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-06 15:17:14 -05:00
6c635bd70d refactor: Remove dead code to update the artifact graph after mock execution (#5158)
Remove dead code to update the artifact graph after mock execution
2025-02-06 17:56:19 +00:00
1c0a38a1e2 Update lower-right corner units menu to read and edit inline settings annotations if present (#5212)
* WIP show annotation length unit setting in LowerRightControls if present

* Add logic for changing settings annotation if it's present

* Add E2E test

* Cleanup lints, fmt, tsc, logs

* Change to use settings from Rust helper function

- Fix thrown error to use the cause field
- Fix function names to not use "get"

* Remove unneeded constants

* Post-merge fixups

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

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

* Add back `ImportStatement` to make tsc happy (thanks @jtran!)

* fmt

---------

Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-06 17:37:13 +00:00
019cb815f9 Fix resizing view breaking app on high DPI displays (#5275)
* Fix resizing view breaking app on high DPI displays

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

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
2025-02-06 12:06:50 -05:00
c8653beae7 updating onboarding bracket with SSI (#5281)
Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
2025-02-06 16:15:21 +00:00
9ea3cb51ba Bump miette from 7.2.0 to 7.5.0 in /src/wasm-lib (#5224)
Bumps [miette](https://github.com/zkat/miette) from 7.2.0 to 7.5.0.
- [Release notes](https://github.com/zkat/miette/releases)
- [Changelog](https://github.com/zkat/miette/blob/main/CHANGELOG.md)
- [Commits](https://github.com/zkat/miette/commits)

---
updated-dependencies:
- dependency-name: miette
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-06 08:06:10 -08:00
e0de0493ab Skip prompt-to-edit playwright tests on windows (#5290)
* Skip prompt-to-edit playwright tests on windows

* Fix lint

* Fix test hookk

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

* Clear bad snapshots

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-06 09:45:12 -05:00
11cac0c30e Skip text-to-cad playwright tests on windows (#5286) 2025-02-06 08:48:17 -05:00
4de50edf5a Bump @types/node from 22.7.8 to 22.13.1 (#5262)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.7.8 to 22.13.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
2025-02-06 10:51:07 +00:00
144 changed files with 21463 additions and 4105 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -9,7 +9,7 @@ Repeat a 2-dimensional sketch along some dimension, with a dynamic amount
of distance between each repetition, some specified number of times. of distance between each repetition, some specified number of times.
```js ```js
patternLinear2d(data: LinearPattern2dData, sketch_set: SketchSet) -> [Sketch] patternLinear2d(data: LinearPattern2dData, sketch_set: SketchSet, use_original?: bool) -> [Sketch]
``` ```
@ -19,6 +19,7 @@ patternLinear2d(data: LinearPattern2dData, sketch_set: SketchSet) -> [Sketch]
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `data` | [`LinearPattern2dData`](/docs/kcl/types/LinearPattern2dData) | Data for a linear pattern on a 2D sketch. | Yes | | `data` | [`LinearPattern2dData`](/docs/kcl/types/LinearPattern2dData) | Data for a linear pattern on a 2D sketch. | Yes |
| `sketch_set` | [`SketchSet`](/docs/kcl/types/SketchSet) | A sketch or a group of sketches. | Yes | | `sketch_set` | [`SketchSet`](/docs/kcl/types/SketchSet) | A sketch or a group of sketches. | Yes |
| `use_original` | `bool` | | No |
### Returns ### Returns

View File

@ -9,7 +9,7 @@ Repeat a 3-dimensional solid along a linear path, with a dynamic amount
of distance between each repetition, some specified number of times. of distance between each repetition, some specified number of times.
```js ```js
patternLinear3d(data: LinearPattern3dData, solid_set: SolidSet) -> [Solid] patternLinear3d(data: LinearPattern3dData, solid_set: SolidSet, use_original?: bool) -> [Solid]
``` ```
@ -19,6 +19,7 @@ patternLinear3d(data: LinearPattern3dData, solid_set: SolidSet) -> [Solid]
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `data` | [`LinearPattern3dData`](/docs/kcl/types/LinearPattern3dData) | Data for a linear pattern on a 3D model. | Yes | | `data` | [`LinearPattern3dData`](/docs/kcl/types/LinearPattern3dData) | Data for a linear pattern on a 3D model. | Yes |
| `solid_set` | [`SolidSet`](/docs/kcl/types/SolidSet) | A solid or a group of solids. | Yes | | `solid_set` | [`SolidSet`](/docs/kcl/types/SolidSet) | A solid or a group of solids. | Yes |
| `use_original` | `bool` | | No |
### Returns ### Returns

View File

@ -35,7 +35,7 @@ The transform function returns a transform object. All properties of the object
- `rotation.origin` (either "local" i.e. rotate around its own center, "global" i.e. rotate around the scene's center, or a 3D point, defaults to "local") - `rotation.origin` (either "local" i.e. rotate around its own center, "global" i.e. rotate around the scene's center, or a 3D point, defaults to "local")
```js ```js
patternTransform(total_instances: integer, transform_function: FunctionParam, solid_set: SolidSet) -> [Solid] patternTransform(total_instances: integer, transform_function: FunctionParam, solid_set: SolidSet, use_original?: bool) -> [Solid]
``` ```
@ -46,6 +46,7 @@ patternTransform(total_instances: integer, transform_function: FunctionParam, so
| `total_instances` | `integer` | | Yes | | `total_instances` | `integer` | | Yes |
| `transform_function` | `FunctionParam` | | Yes | | `transform_function` | `FunctionParam` | | Yes |
| `solid_set` | [`SolidSet`](/docs/kcl/types/SolidSet) | A solid or a group of solids. | Yes | | `solid_set` | [`SolidSet`](/docs/kcl/types/SolidSet) | A solid or a group of solids. | Yes |
| `use_original` | `bool` | | No |
### Returns ### Returns

View File

@ -9,7 +9,7 @@ Just like patternTransform, but works on 2D sketches not 3D solids.
```js ```js
patternTransform2d(total_instances: integer, transform_function: FunctionParam, solid_set: SketchSet) -> [Sketch] patternTransform2d(total_instances: integer, transform_function: FunctionParam, solid_set: SketchSet, use_original?: bool) -> [Sketch]
``` ```
@ -20,6 +20,7 @@ patternTransform2d(total_instances: integer, transform_function: FunctionParam,
| `total_instances` | `integer` | | Yes | | `total_instances` | `integer` | | Yes |
| `transform_function` | `FunctionParam` | | Yes | | `transform_function` | `FunctionParam` | | Yes |
| `solid_set` | [`SketchSet`](/docs/kcl/types/SketchSet) | A sketch or a group of sketches. | Yes | | `solid_set` | [`SketchSet`](/docs/kcl/types/SketchSet) | A sketch or a group of sketches. | Yes |
| `use_original` | `bool` | | No |
### Returns ### Returns

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -20,5 +20,6 @@ Data for a circular pattern on a 2D sketch.
| `center` |`[number, number]`| The center about which to make the pattern. This is a 2D vector. | No | | `center` |`[number, number]`| The center about which to make the pattern. This is a 2D vector. | No |
| `arcDegrees` |`number`| The arc angle (in degrees) to place the repetitions. Must be greater than 0. | No | | `arcDegrees` |`number`| The arc angle (in degrees) to place the repetitions. Must be greater than 0. | No |
| `rotateDuplicates` |`boolean`| Whether or not to rotate the duplicates as they are copied. | No | | `rotateDuplicates` |`boolean`| Whether or not to rotate the duplicates as they are copied. | No |
| `useOriginal` |`boolean`| If the target being patterned is itself a pattern, then, should you use the original solid, or the pattern? | No |

View File

@ -21,5 +21,6 @@ Data for a circular pattern on a 3D model.
| `center` |`[number, number, number]`| The center about which to make the pattern. This is a 3D vector. | No | | `center` |`[number, number, number]`| The center about which to make the pattern. This is a 3D vector. | No |
| `arcDegrees` |`number`| The arc angle (in degrees) to place the repetitions. Must be greater than 0. | No | | `arcDegrees` |`number`| The arc angle (in degrees) to place the repetitions. Must be greater than 0. | No |
| `rotateDuplicates` |`boolean`| Whether or not to rotate the duplicates as they are copied. | No | | `rotateDuplicates` |`boolean`| Whether or not to rotate the duplicates as they are copied. | No |
| `useOriginal` |`boolean`| If the target being patterned is itself a pattern, then, should you use the original solid, or the pattern? | No |

View File

@ -22,6 +22,7 @@ A sketch is a collection of paths.
| `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No | | `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No |
| `tags` |`object`| Tag identifiers that have been declared in this sketch. | No | | `tags` |`object`| Tag identifiers that have been declared in this sketch. | No |
| `artifactId` |[`ArtifactId`](/docs/kcl/types/ArtifactId)| The original id of the sketch. This stays the same even if the sketch is is sketched on face etc. | No | | `artifactId` |[`ArtifactId`](/docs/kcl/types/ArtifactId)| The original id of the sketch. This stays the same even if the sketch is is sketched on face etc. | No |
| `originalId` |`string`| | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch is a collection of paths. | No | | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch is a collection of paths. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |

View File

@ -31,6 +31,7 @@ A sketch is a collection of paths.
| `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No | | `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No |
| `tags` |`object`| Tag identifiers that have been declared in this sketch. | No | | `tags` |`object`| Tag identifiers that have been declared in this sketch. | No |
| `artifactId` |[`ArtifactId`](/docs/kcl/types/ArtifactId)| The original id of the sketch. This stays the same even if the sketch is is sketched on face etc. | No | | `artifactId` |[`ArtifactId`](/docs/kcl/types/ArtifactId)| The original id of the sketch. This stays the same even if the sketch is is sketched on face etc. | No |
| `originalId` |`string`| | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch or a group of sketches. | No | | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch or a group of sketches. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |

View File

@ -20,6 +20,7 @@ export class ToolbarFixture {
shellButton!: Locator shellButton!: Locator
revolveButton!: Locator revolveButton!: Locator
offsetPlaneButton!: Locator offsetPlaneButton!: Locator
helixButton!: Locator
startSketchBtn!: Locator startSketchBtn!: Locator
lineBtn!: Locator lineBtn!: Locator
rectangleBtn!: Locator rectangleBtn!: Locator
@ -49,6 +50,7 @@ export class ToolbarFixture {
this.shellButton = page.getByTestId('shell') this.shellButton = page.getByTestId('shell')
this.revolveButton = page.getByTestId('revolve') this.revolveButton = page.getByTestId('revolve')
this.offsetPlaneButton = page.getByTestId('plane-offset') this.offsetPlaneButton = page.getByTestId('plane-offset')
this.helixButton = page.getByTestId('helix')
this.startSketchBtn = page.getByTestId('sketch') this.startSketchBtn = page.getByTestId('sketch')
this.lineBtn = page.getByTestId('line') this.lineBtn = page.getByTestId('line')
this.rectangleBtn = page.getByTestId('corner-rectangle') this.rectangleBtn = page.getByTestId('corner-rectangle')

View File

@ -27,7 +27,7 @@ test.describe('Onboarding tests', () => {
}, },
cleanProjectDir: true, cleanProjectDir: true,
}, },
async ({ context, page, homePage }) => { async ({ page, homePage }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene() await homePage.goToModelingScene()
@ -68,7 +68,7 @@ test.describe('Onboarding tests', () => {
}, },
cleanProjectDir: true, cleanProjectDir: true,
}, },
async ({ page, homePage }, testInfo) => { async ({ page }) => {
const u = await getUtils(page) const u = await getUtils(page)
const viewportSize = { width: 1200, height: 500 } const viewportSize = { width: 1200, height: 500 }
@ -154,7 +154,7 @@ test.describe('Onboarding tests', () => {
) )
test( test(
'Click through each onboarding step', 'Click through each onboarding step and back',
{ {
appSettings: { appSettings: {
app: { app: {
@ -187,15 +187,21 @@ test.describe('Onboarding tests', () => {
).toBeVisible() ).toBeVisible()
const nextButton = page.getByTestId('onboarding-next') const nextButton = page.getByTestId('onboarding-next')
const prevButton = page.getByTestId('onboarding-prev')
while ((await nextButton.innerText()) !== 'Finish') { while ((await nextButton.innerText()) !== 'Finish') {
await nextButton.hover() await nextButton.hover()
await nextButton.click() await nextButton.click()
} }
// Finish the onboarding while ((await prevButton.innerText()) !== 'Dismiss') {
await nextButton.hover() await prevButton.hover()
await nextButton.click() await prevButton.click()
}
// Dismiss the onboarding
await prevButton.hover()
await prevButton.click()
// Test that the onboarding pane is gone // Test that the onboarding pane is gone
await expect(page.getByTestId('onboarding-content')).not.toBeVisible() await expect(page.getByTestId('onboarding-content')).not.toBeVisible()
@ -269,7 +275,7 @@ test.describe('Onboarding tests', () => {
cleanProjectDir: true, cleanProjectDir: true,
}, },
async ({ context, page, homePage }) => { async ({ page, homePage }) => {
const u = await getUtils(page) const u = await getUtils(page)
const badCode = `// This is bad code we shouldn't see` const badCode = `// This is bad code we shouldn't see`
@ -336,10 +342,10 @@ test.describe('Onboarding tests', () => {
await homePage.goToModelingScene() await homePage.goToModelingScene()
// Test that the text in this step is correct // Test that the text in this step is correct
const avatarLocator = await page const avatarLocator = page
.getByTestId('user-sidebar-toggle') .getByTestId('user-sidebar-toggle')
.locator('img') .locator('img')
const onboardingOverlayLocator = await page const onboardingOverlayLocator = page
.getByTestId('onboarding-content') .getByTestId('onboarding-content')
.locator('div') .locator('div')
.nth(1) .nth(1)
@ -447,7 +453,7 @@ test.fixme(
}, },
cleanProjectDir: true, cleanProjectDir: true,
}, },
async ({ context, page, homePage }, testInfo) => { async ({ context, page }) => {
await context.folderSetupFn(async (dir) => { await context.folderSetupFn(async (dir) => {
const routerTemplateDir = join(dir, 'router-template-slate') const routerTemplateDir = join(dir, 'router-template-slate')
await fsp.mkdir(routerTemplateDir, { recursive: true }) await fsp.mkdir(routerTemplateDir, { recursive: true })
@ -486,10 +492,6 @@ test.fixme(
}) })
await test.step('Navigate into project', async () => { await test.step('Navigate into project', async () => {
await page.setBodyDimensions({ width: 1200, height: 500 })
page.on('console', console.log)
await expect( await expect(
page.getByRole('heading', { name: 'Your Projects' }) page.getByRole('heading', { name: 'Your Projects' })
).toBeVisible() ).toBeVisible()

View File

@ -775,6 +775,71 @@ openSketch = startSketchOn('XY')
}) })
}) })
test('Helix point-and-click', async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
// One dumb hardcoded screen pixel value
const testPoint = { x: 620, y: 257 }
const expectedOutput = `helix001 = helix(revolutions = 1, angleStart = 360, counterClockWise = false, radius = 5, axis = 'X', length = 5)`
await homePage.goToModelingScene()
await test.step(`Look for the red of the default plane`, async () => {
await scene.expectPixelColor([96, 52, 52], testPoint, 15)
})
await test.step(`Go through the command bar flow`, async () => {
await toolbar.helixButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'revolutions',
currentArgValue: '1',
headerArguments: {
AngleStart: '',
Axis: '',
CounterClockWise: '',
Length: '',
Radius: '',
Revolutions: '',
},
highlightedHeaderArg: 'revolutions',
commandName: 'Helix',
})
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
})
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
await editor.expectEditor.toContain(expectedOutput)
await editor.expectState({
diagnostics: [],
activeLines: [expectedOutput],
highlightedCode: '',
})
// Red plane is now gone, white helix is there
await scene.expectPixelColor([250, 250, 250], testPoint, 15)
})
await test.step('Delete offset plane via feature tree selection', async () => {
await editor.closePane()
const operationButton = await toolbar.getFeatureTreeOperation('Helix', 0)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Backspace')
// Red plane is back
await scene.expectPixelColor([96, 52, 52], testPoint, 15)
})
})
const loftPointAndClickCases = [ const loftPointAndClickCases = [
{ shouldPreselect: true }, { shouldPreselect: true },
{ shouldPreselect: false }, { shouldPreselect: false },
@ -964,7 +1029,7 @@ sketch002 = startSketchOn('XZ')
testPoint.x - 50, testPoint.x - 50,
testPoint.y testPoint.y
) )
const sweepDeclaration = 'sweep001 = sweep({ path = sketch002 }, sketch001)' const sweepDeclaration = 'sweep001 = sweep(sketch001, path = sketch002)'
await test.step(`Look for sketch001`, async () => { await test.step(`Look for sketch001`, async () => {
await toolbar.closePane('code') await toolbar.closePane('code')
@ -1890,7 +1955,7 @@ chamfer04 = chamfer({ length = 5, tags = [getOppositeEdge(seg02)]}, extrude001
const testPoint = { x: 575, y: 200 } const testPoint = { x: 575, y: 200 }
const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y) const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const shellDeclaration = const shellDeclaration =
"shell001 = shell({ faces = ['end'], thickness = 5 }, extrude001)" "shell001 = shell(extrude001, faces = ['end'], thickness = 5)"
await test.step(`Look for the grey of the shape`, async () => { await test.step(`Look for the grey of the shape`, async () => {
await scene.expectPixelColor([127, 127, 127], testPoint, 15) await scene.expectPixelColor([127, 127, 127], testPoint, 15)
@ -1990,8 +2055,7 @@ extrude001 = extrude(sketch001, length = 40)
const [clickOnWall] = scene.makeMouseHelpers(testPoint.x, testPoint.y + 70) const [clickOnWall] = scene.makeMouseHelpers(testPoint.x, testPoint.y + 70)
const mutatedCode = 'xLine(-40, %, $seg01)' const mutatedCode = 'xLine(-40, %, $seg01)'
const shellDeclaration = const shellDeclaration =
"shell001 = shell({ faces = ['end', seg01], thickness = 5}, extrude001)" "shell001 = shell(extrude001, faces = ['end', seg01], thickness = 5)"
const formattedOutLastLine = '}, extrude001)'
await test.step(`Look for the grey of the shape`, async () => { await test.step(`Look for the grey of the shape`, async () => {
await scene.expectPixelColor([99, 99, 99], testPoint, 15) await scene.expectPixelColor([99, 99, 99], testPoint, 15)
@ -2034,7 +2098,7 @@ extrude001 = extrude(sketch001, length = 40)
await editor.expectEditor.toContain(shellDeclaration) await editor.expectEditor.toContain(shellDeclaration)
await editor.expectState({ await editor.expectState({
diagnostics: [], diagnostics: [],
activeLines: [formattedOutLastLine], activeLines: [shellDeclaration],
highlightedCode: '', highlightedCode: '',
}) })
await scene.expectPixelColor([49, 49, 49], testPoint, 15) await scene.expectPixelColor([49, 49, 49], testPoint, 15)
@ -2088,9 +2152,8 @@ extrude002 = extrude(sketch002, length = 50)
// One dumb hardcoded screen pixel value // One dumb hardcoded screen pixel value
const testPoint = { x: 550, y: 295 } const testPoint = { x: 550, y: 295 }
const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y) const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const shellDeclaration = `shell001 = shell({ faces = ['end'], thickness = 5 }, ${ const shellTarget = hasExtrudesInPipe ? 'sketch002' : 'extrude002'
hasExtrudesInPipe ? 'sketch002' : 'extrude002' const shellDeclaration = `shell001 = shell(${shellTarget}, faces = ['end'], thickness = 5)`
})`
await test.step(`Look for the grey of the shape`, async () => { await test.step(`Look for the grey of the shape`, async () => {
await toolbar.closePane('code') await toolbar.closePane('code')
@ -2158,7 +2221,7 @@ extrude002 = extrude(sketch002, length = 50)
sketch002 = startSketchOn('XZ') sketch002 = startSketchOn('XZ')
|> startProfileAt([0, 0], %) |> startProfileAt([0, 0], %)
|> xLine(-2000, %) |> xLine(-2000, %)
sweep001 = sweep({ path = sketch002 }, sketch001) sweep001 = sweep(sketch001, path = sketch002)
` `
await context.addInitScript((initialCode) => { await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode) localStorage.setItem('persistCode', initialCode)

View File

@ -35,106 +35,108 @@ sketch003 = startSketchOn('XY')
extrude003 = extrude(sketch003, length = 20) extrude003 = extrude(sketch003, length = 20)
` `
test.describe('Check the happy path, for basic changing color', () => { test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
const cases = [ test.fixme('Check the happy path, for basic changing color', () => {
{ const cases = [
desc: 'User accepts change', {
shouldReject: false, desc: 'User accepts change',
}, shouldReject: false,
{ },
desc: 'User rejects change', {
shouldReject: true, desc: 'User rejects change',
}, shouldReject: true,
] as const },
for (const { desc, shouldReject } of cases) { ] as const
test(`${desc}`, async ({ for (const { desc, shouldReject } of cases) {
context, test(`${desc}`, async ({
homePage, context,
cmdBar, homePage,
editor, cmdBar,
page, editor,
scene, page,
}) => { scene,
await context.addInitScript((file) => { }) => {
localStorage.setItem('persistCode', file) await context.addInitScript((file) => {
}, file) localStorage.setItem('persistCode', file)
await homePage.goToModelingScene() }, file)
await homePage.goToModelingScene()
const body1CapCoords = { x: 571, y: 351 } const body1CapCoords = { x: 571, y: 351 }
const greenCheckCoords = { x: 565, y: 345 } const greenCheckCoords = { x: 565, y: 345 }
const body2WallCoords = { x: 609, y: 153 } const body2WallCoords = { x: 609, y: 153 }
const [clickBody1Cap] = scene.makeMouseHelpers( const [clickBody1Cap] = scene.makeMouseHelpers(
body1CapCoords.x, body1CapCoords.x,
body1CapCoords.y body1CapCoords.y
) )
const yellow: [number, number, number] = [179, 179, 131] const yellow: [number, number, number] = [179, 179, 131]
const green: [number, number, number] = [108, 152, 75] const green: [number, number, number] = [108, 152, 75]
const notGreen: [number, number, number] = [132, 132, 132] const notGreen: [number, number, number] = [132, 132, 132]
const body2NotGreen: [number, number, number] = [88, 88, 88] const body2NotGreen: [number, number, number] = [88, 88, 88]
const submittingToast = page.getByText('Submitting to Text-to-CAD API...') const submittingToast = page.getByText(
const successToast = page.getByText('Prompt to edit successful') 'Submitting to Text-to-CAD API...'
const acceptBtn = page.getByRole('button', { name: 'checkmark Accept' }) )
const rejectBtn = page.getByRole('button', { name: 'close Reject' }) const successToast = page.getByText('Prompt to edit successful')
const acceptBtn = page.getByRole('button', { name: 'checkmark Accept' })
const rejectBtn = page.getByRole('button', { name: 'close Reject' })
await test.step('wait for scene to load select body and check selection came through', async () => { await test.step('wait for scene to load select body and check selection came through', async () => {
await scene.expectPixelColor([134, 134, 134], body1CapCoords, 15) await scene.expectPixelColor([134, 134, 134], body1CapCoords, 15)
await clickBody1Cap() await clickBody1Cap()
await scene.expectPixelColor(yellow, body1CapCoords, 20) await scene.expectPixelColor(yellow, body1CapCoords, 20)
await editor.expectState({ await editor.expectState({
highlightedCode: '', highlightedCode: '',
activeLines: ['|>startProfileAt([-73.64,-42.89],%)'], activeLines: ['|>startProfileAt([-73.64,-42.89],%)'],
diagnostics: [], diagnostics: [],
})
}) })
})
await test.step('fire off edit prompt', async () => { await test.step('fire off edit prompt', async () => {
await cmdBar.openCmdBar('promptToEdit') await cmdBar.openCmdBar('promptToEdit')
// being specific about the color with a hex means asserting pixel color is more stable // being specific about the color with a hex means asserting pixel color is more stable
await page await page
.getByTestId('cmd-bar-arg-value') .getByTestId('cmd-bar-arg-value')
.fill('make this neon green please, use #39FF14') .fill('make this neon green please, use #39FF14')
await page.waitForTimeout(100) await page.waitForTimeout(100)
await cmdBar.progressCmdBar() await cmdBar.progressCmdBar()
await expect(submittingToast).toBeVisible() await expect(submittingToast).toBeVisible()
await expect(submittingToast).not.toBeVisible({ timeout: 2 * 60_000 }) // can take a while await expect(submittingToast).not.toBeVisible({ timeout: 2 * 60_000 }) // can take a while
await expect(successToast).toBeVisible() await expect(successToast).toBeVisible()
}) })
await test.step('verify initial change', async () => {
await scene.expectPixelColor(green, greenCheckCoords, 15)
await scene.expectPixelColor(body2NotGreen, body2WallCoords, 15)
await editor.expectEditor.toContain('appearance({')
})
if (!shouldReject) {
await test.step('check accept works and can be "undo"ed', async () => {
await acceptBtn.click()
await expect(successToast).not.toBeVisible()
await test.step('verify initial change', async () => {
await scene.expectPixelColor(green, greenCheckCoords, 15) await scene.expectPixelColor(green, greenCheckCoords, 15)
await editor.expectEditor.toContain('appearance({') await scene.expectPixelColor(body2NotGreen, body2WallCoords, 15)
await editor.expectEditor.toContain('appearance(')
// ctrl-z works after accepting
await page.keyboard.down('ControlOrMeta')
await page.keyboard.press('KeyZ')
await page.keyboard.up('ControlOrMeta')
await editor.expectEditor.not.toContain('appearance({')
await scene.expectPixelColor(notGreen, greenCheckCoords, 15)
}) })
} else {
await test.step('check reject works', async () => {
await rejectBtn.click()
await expect(successToast).not.toBeVisible()
await scene.expectPixelColor(notGreen, greenCheckCoords, 15) if (!shouldReject) {
await editor.expectEditor.not.toContain('appearance({') await test.step('check accept works and can be "undo"ed', async () => {
}) await acceptBtn.click()
} await expect(successToast).not.toBeVisible()
})
} await scene.expectPixelColor(green, greenCheckCoords, 15)
}) await editor.expectEditor.toContain('appearance(')
// ctrl-z works after accepting
await page.keyboard.down('ControlOrMeta')
await page.keyboard.press('KeyZ')
await page.keyboard.up('ControlOrMeta')
await editor.expectEditor.not.toContain('appearance(')
await scene.expectPixelColor(notGreen, greenCheckCoords, 15)
})
} else {
await test.step('check reject works', async () => {
await rejectBtn.click()
await expect(successToast).not.toBeVisible()
await scene.expectPixelColor(notGreen, greenCheckCoords, 15)
await editor.expectEditor.not.toContain('appearance(')
})
}
})
}
})
test.describe('bad path', { tag: ['@skipWin'] }, () => {
test(`bad edit prompt`, async ({ test(`bad edit prompt`, async ({
context, context,
homePage, homePage,

View File

@ -253,7 +253,7 @@ extrude001 = extrude(sketch001, length = 50)
|> |>
example = extrude(exampleSketch, length = 5) example = extrude(exampleSketch, length = 5)
shell({ faces: ['end'], thickness: 0.25 }, exampleSketch)` shell(exampleSketch, faces = ['end'], thickness = 0.25)`
) )
}) })
@ -314,6 +314,7 @@ extrude001 = extrude(sketch001, length = 50)
) )
test('when engine fails export we handle the failure and alert the user', async ({ test('when engine fails export we handle the failure and alert the user', async ({
scene,
page, page,
homePage, homePage,
}) => { }) => {
@ -383,10 +384,7 @@ extrude001 = extrude(sketch001, length = 50)
await page.keyboard.press('End') await page.keyboard.press('End')
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
// wait for execution done await scene.waitForExecutionDone()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// Now try exporting // Now try exporting

View File

@ -1187,14 +1187,12 @@ sweepSketch = startSketchOn('XY')
angleStart = 0, angleStart = 0,
radius = 2 radius = 2
}, %) }, %)
|> sweep({ |> sweep(path = sweepPath)
path = sweepPath, |> appearance(
}, %)
|> appearance({
color = "#bb00ff", color = "#bb00ff",
metalness = 90, metalness = 90,
roughness = 90 roughness = 90
}, %) )
` `
) )
}) })
@ -1235,14 +1233,12 @@ sweepSketch = startSketchOn('XY')
angleStart = 0, angleStart = 0,
radius = 2 radius = 2
}, %) }, %)
|> sweep({ |> sweep(path = sweepPath)
path = sweepPath, |> appearance(
}, %)
|> appearance({
color = "#bb00ff", color = "#bb00ff",
metalness = 90, metalness = 90,
roughness = 90 roughness = 90
}, %) )
` `
) )
}) })

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -896,4 +896,53 @@ test.describe('Testing settings', () => {
}) })
} }
) )
test(`Change inline units setting`, async ({
page,
homePage,
context,
editor,
}) => {
const initialInlineUnits = 'yd'
const editedInlineUnits = { short: 'mm', long: 'Millimeters' }
const inlineSettingsString = (s: string) =>
`@settings(defaultLengthUnit = ${s})`
const unitsIndicator = page.getByRole('button', {
name: 'Current units are:',
})
const unitsChangeButton = (name: string) =>
page.getByRole('button', { name, exact: true })
await context.folderSetupFn(async (dir) => {
const bracketDir = join(dir, 'project-000')
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile(
executorInputPath('cube.kcl'),
join(bracketDir, 'main.kcl')
)
})
await test.step(`Initial units from settings`, async () => {
await homePage.openProject('project-000')
await expect(unitsIndicator).toHaveText('Current units are: in')
})
await test.step(`Manually write inline settings`, async () => {
await editor.openPane()
await editor.replaceCode(
`fn cube`,
`${inlineSettingsString(initialInlineUnits)}
fn cube`
)
await expect(unitsIndicator).toContainText(initialInlineUnits)
})
await test.step(`Change units setting via lower-right control`, async () => {
await unitsIndicator.click()
await unitsChangeButton(editedInlineUnits.long).click()
await expect(
page.getByText(`Updated per-file units to ${editedInlineUnits.short}`)
).toBeVisible()
})
})
}) })

View File

@ -3,7 +3,7 @@ import { getUtils, createProject } from './test-utils'
import { join } from 'path' import { join } from 'path'
import fs from 'fs' import fs from 'fs'
test.describe('Text-to-CAD tests', () => { test.describe('Text-to-CAD tests', { tag: ['@skipWin'] }, () => {
test('basic lego happy case', async ({ page, homePage }) => { test('basic lego happy case', async ({ page, homePage }) => {
const u = await getUtils(page) const u = await getUtils(page)

View File

@ -85,7 +85,7 @@
"fmt": "prettier --write ./src *.ts *.json *.js ./e2e ./packages", "fmt": "prettier --write ./src *.ts *.json *.js ./e2e ./packages",
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages", "fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages",
"fetch:wasm": "./get-latest-wasm-bundle.sh", "fetch:wasm": "./get-latest-wasm-bundle.sh",
"fetch:samples": "echo \"Fetching latest KCL samples...\" && curl -o public/kcl-samples-manifest-fallback.json https://raw.githubusercontent.com/KittyCAD/kcl-samples/main/manifest.json", "fetch:samples": "echo \"Fetching latest KCL samples...\" && curl -o public/kcl-samples-manifest-fallback.json https://raw.githubusercontent.com/KittyCAD/kcl-samples/achalmers/kw-appearance/manifest.json",
"isomorphic-copy-wasm": "(copy src/wasm-lib/pkg/wasm_lib_bg.wasm public || cp src/wasm-lib/pkg/wasm_lib_bg.wasm public)", "isomorphic-copy-wasm": "(copy src/wasm-lib/pkg/wasm_lib_bg.wasm public || cp src/wasm-lib/pkg/wasm_lib_bg.wasm public)",
"build:wasm-dev": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && yarn isomorphic-copy-wasm && yarn fmt", "build:wasm-dev": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && yarn isomorphic-copy-wasm && yarn fmt",
"build:wasm": "yarn wasm-prep && cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings && cd ../.. && yarn isomorphic-copy-wasm && yarn fmt", "build:wasm": "yarn wasm-prep && cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings && cd ../.. && yarn isomorphic-copy-wasm && yarn fmt",
@ -158,8 +158,8 @@
"@types/electron": "^1.6.10", "@types/electron": "^1.6.10",
"@types/isomorphic-fetch": "^0.0.39", "@types/isomorphic-fetch": "^0.0.39",
"@types/minimist": "^1.2.5", "@types/minimist": "^1.2.5",
"@types/mocha": "^10.0.6", "@types/mocha": "^10.0.10",
"@types/node": "^22.7.8", "@types/node": "^22.13.1",
"@types/pixelmatch": "^5.2.6", "@types/pixelmatch": "^5.2.6",
"@types/pngjs": "^6.0.4", "@types/pngjs": "^6.0.4",
"@types/react": "^18.3.4", "@types/react": "^18.3.4",

View File

@ -59,7 +59,9 @@ UnaryOp { AddOp | BangOp }
ObjectProperty { PropertyName (":" | Equals) expression } ObjectProperty { PropertyName (":" | Equals) expression }
ArgumentList { "(" commaSep<expression> ")" } LabeledArgument { ArgumentLabel Equals expression }
ArgumentList { "(" commaSep<LabeledArgument | expression> ")" }
type[@isGroup=Type] { type[@isGroup=Type] {
@specialize[@name=PrimitiveType]< @specialize[@name=PrimitiveType]<
@ -74,6 +76,8 @@ VariableDefinition { identifier }
VariableName { identifier } VariableName { identifier }
ArgumentLabel { identifier }
@skip { whitespace | LineComment | BlockComment } @skip { whitespace | LineComment | BlockComment }
kw<term> { @specialize[@name={term}]<identifier, term> } kw<term> { @specialize[@name={term}]<identifier, term> }

View File

@ -0,0 +1,85 @@
# empty
f()
==>
Program(ExpressionStatement(CallExpression(VariableName,
ArgumentList)))
# single anon arg
f(1)
==>
Program(ExpressionStatement(CallExpression(VariableName,
ArgumentList(Number))))
# deprecated multiple anon args
f(1, 2)
==>
Program(ExpressionStatement(CallExpression(VariableName,
ArgumentList(Number,
Number))))
# deprecated trailing %
startSketchOn('XY')
|> line([thickness, 0], %)
==>
Program(ExpressionStatement(PipeExpression(CallExpression(VariableName,
ArgumentList(String)),
PipeOperator,
CallExpression(VariableName,
ArgumentList(ArrayExpression(VariableName,
Number),
PipeSubstitution)))))
# % and named arg
startSketchOn('XY')
|> line(%, end = [thickness, 0])
==>
Program(ExpressionStatement(PipeExpression(CallExpression(VariableName,
ArgumentList(String)),
PipeOperator,
CallExpression(VariableName,
ArgumentList(PipeSubstitution,
LabeledArgument(ArgumentLabel,
Equals,
ArrayExpression(VariableName,
Number)))))))
# implied % and named arg
startSketchOn('XY')
|> line(end = [thickness, 0])
==>
Program(ExpressionStatement(PipeExpression(CallExpression(VariableName,
ArgumentList(String)),
PipeOperator,
CallExpression(VariableName,
ArgumentList(LabeledArgument(ArgumentLabel,
Equals,
ArrayExpression(VariableName,
Number)))))))
# multiple named arg
ngon(plane = "XY", numSides = 5, radius = pentR)
==>
Program(ExpressionStatement(CallExpression(VariableName,
ArgumentList(LabeledArgument(ArgumentLabel,
Equals,
String),
LabeledArgument(ArgumentLabel,
Equals,
Number),
LabeledArgument(ArgumentLabel,
Equals,
VariableName)))))

View File

@ -1 +1,212 @@
404: Not Found [
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "80-20-rail/main.kcl",
"multipleFiles": false,
"title": "80/20 Rail",
"description": "An 80/20 extruded aluminum linear rail. T-slot profile adjustable by profile height, rail length, and origin position"
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "a-parametric-bearing-pillow-block/main.kcl",
"multipleFiles": false,
"title": "A Parametric Bearing Pillow Block",
"description": "A bearing pillow block, also known as a plummer block or pillow block bearing, is a pedestal used to provide support for a rotating shaft with the help of compatible bearings and various accessories. Housing a bearing, the pillow block provides a secure and stable foundation that allows the shaft to rotate smoothly within its machinery setup. These components are essential in a wide range of mechanical systems and machinery, playing a key role in reducing friction and supporting radial and axial loads."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "ball-bearing/main.kcl",
"multipleFiles": false,
"title": "Ball Bearing",
"description": "A ball bearing is a type of rolling-element bearing that uses balls to maintain the separation between the bearing races. The primary purpose of a ball bearing is to reduce rotational friction and support radial and axial loads."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "bracket/main.kcl",
"multipleFiles": false,
"title": "Shelf Bracket",
"description": "This is a bracket that holds a shelf. It is made of aluminum and is designed to hold a force of 300 lbs. The bracket is 6 inches wide and the force is applied at the end of the shelf, 12 inches from the wall. The bracket has a factor of safety of 1.2. The legs of the bracket are 5 inches and 2 inches long. The thickness of the bracket is calculated from the constraints provided."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "car-wheel-assembly/main.kcl",
"multipleFiles": true,
"title": "Car Wheel Assembly",
"description": "A car wheel assembly with a rotor, tire, and lug nuts."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "dodecahedron/main.kcl",
"multipleFiles": false,
"title": "Hollow Dodecahedron",
"description": "A regular dodecahedron or pentagonal dodecahedron is a dodecahedron composed of regular pentagonal faces, three meeting at each vertex. This example shows constructing the individual faces of the dodecahedron and extruding inwards."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "enclosure/main.kcl",
"multipleFiles": false,
"title": "Enclosure",
"description": "An enclosure body and sealing lid for storing items"
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "flange-with-patterns/main.kcl",
"multipleFiles": false,
"title": "Flange",
"description": "A flange is a flat rim, collar, or rib, typically forged or cast, that is used to strengthen an object, guide it, or attach it to another object. Flanges are known for their use in various applications, including piping, plumbing, and mechanical engineering, among others."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "flange-xy/main.kcl",
"multipleFiles": false,
"title": "Flange with XY coordinates",
"description": "A flange is a flat rim, collar, or rib, typically forged or cast, that is used to strengthen an object, guide it, or attach it to another object. Flanges are known for their use in various applications, including piping, plumbing, and mechanical engineering, among others."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "focusrite-scarlett-mounting-bracket/main.kcl",
"multipleFiles": false,
"title": "A mounting bracket for the Focusrite Scarlett Solo audio interface",
"description": "This is a bracket that holds an audio device underneath a desk or shelf. The audio device has dimensions of 144mm wide, 80mm length and 45mm depth with fillets of 6mm. This mounting bracket is designed to be 3D printed with PLA material"
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "food-service-spatula/main.kcl",
"multipleFiles": false,
"title": "Food Service Spatula",
"description": "Use these spatulas for mixing, flipping, and scraping."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "french-press/main.kcl",
"multipleFiles": false,
"title": "French Press",
"description": "A french press immersion coffee maker"
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "gear/main.kcl",
"multipleFiles": false,
"title": "Spur Gear",
"description": "A rotating machine part having cut teeth or, in the case of a cogwheel, inserted teeth (called cogs), which mesh with another toothed part to transmit torque. Geared devices can change the speed, torque, and direction of a power source. The two elements that define a gear are its circular shape and the teeth that are integrated into its outer edge, which are designed to fit into the teeth of another gear."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "gear-rack/main.kcl",
"multipleFiles": false,
"title": "100mm Gear Rack",
"description": "A flat bar or rail that is engraved with teeth along its length. These teeth are designed to mesh with the teeth of a gear, known as a pinion. When the pinion, a small cylindrical gear, rotates, its teeth engage with the teeth on the rack, causing the rack to move linearly. Conversely, linear motion applied to the rack will cause the pinion to rotate."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "hex-nut/main.kcl",
"multipleFiles": false,
"title": "Hex nut",
"description": "A hex nut is a type of fastener with a threaded hole and a hexagonal outer shape, used in a wide variety of applications to secure parts together. The hexagonal shape allows for a greater torque to be applied with wrenches or tools, making it one of the most common nut types in hardware."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "i-beam/main.kcl",
"multipleFiles": false,
"title": "I-beam",
"description": "A structural metal beam with an I shaped cross section. Often used in construction"
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "kitt/main.kcl",
"multipleFiles": false,
"title": "Kitt",
"description": "The beloved KittyCAD mascot in a voxelized style."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "lego/main.kcl",
"multipleFiles": false,
"title": "Lego Brick",
"description": "A standard Lego brick. This is a small, plastic construction block toy that can be interlocked with other blocks to build various structures, models, and figures. There are a lot of hacks used in this code."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "mounting-plate/main.kcl",
"multipleFiles": false,
"title": "Mounting Plate",
"description": "A flat piece of material, often metal or plastic, that serves as a support or base for attaching, securing, or mounting various types of equipment, devices, or components."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "multi-axis-robot/main.kcl",
"multipleFiles": true,
"title": "Robot Arm",
"description": "A 4 axis robotic arm for industrial use. These machines can be used for assembly, packaging, organization of goods, and quality inspection processes"
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "pipe/main.kcl",
"multipleFiles": false,
"title": "Pipe",
"description": "A tubular section or hollow cylinder, usually but not necessarily of circular cross-section, used mainly to convey substances that can flow."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "pipe-flange-assembly/main.kcl",
"multipleFiles": false,
"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": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "pipe-with-bend/main.kcl",
"multipleFiles": false,
"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": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "poopy-shoe/main.kcl",
"multipleFiles": false,
"title": "Poopy Shoe",
"description": "poop shute for bambu labs printer - optimized for printing."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "router-template-cross-bar/main.kcl",
"multipleFiles": false,
"title": "Router template for a cross bar",
"description": "A guide for routing a notch into a cross bar."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "router-template-slate/main.kcl",
"multipleFiles": false,
"title": "Router template for a slate",
"description": "A guide for routing a slate for a cross bar."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "sheet-metal-bracket/main.kcl",
"multipleFiles": false,
"title": "Sheet Metal Bracket",
"description": "A component typically made from flat sheet metal through various manufacturing processes such as bending, punching, cutting, and forming. These brackets are used to support, attach, or mount other hardware components, often providing a structural or functional base for assembly."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "socket-head-cap-screw/main.kcl",
"multipleFiles": false,
"title": "Socket Head Cap Screw",
"description": "This is for a #10-24 screw that is 1.00 inches long. A socket head cap screw is a type of fastener that is widely used in a variety of applications requiring a high strength fastening solution. It is characterized by its cylindrical head and internal hexagonal drive, which allows for tightening with an Allen wrench or hex key."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "walkie-talkie/main.kcl",
"multipleFiles": true,
"title": "Walkie Talkie",
"description": "A portable, handheld two-way radio device that allows users to communicate wirelessly over short to medium distances. It operates on specific radio frequencies and features a push-to-talk button for transmitting messages, making it ideal for quick and reliable communication in outdoor, work, or emergency settings."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "washer/main.kcl",
"multipleFiles": false,
"title": "Washer",
"description": "A small, typically disk-shaped component with a hole in the middle, used in a wide range of applications, primarily in conjunction with fasteners like bolts and screws. Washers distribute the load of a fastener across a broader area. This is especially important when the fastening surface is soft or uneven, as it helps to prevent damage to the surface and ensures the load is evenly distributed, reducing the risk of the fastener becoming loose over time."
}
]

View File

@ -57,7 +57,7 @@ function CommandComboBox({
onKeyDown={(event) => { onKeyDown={(event) => {
if ( if (
(event.metaKey && event.key === 'k') || (event.metaKey && event.key === 'k') ||
(event.key === 'Backspace' && !event.currentTarget.value) event.key === 'Escape'
) { ) {
event.preventDefault() event.preventDefault()
commandBarActor.send({ type: 'Close' }) commandBarActor.send({ type: 'Close' })

View File

@ -2,11 +2,15 @@ import { Dialog } from '@headlessui/react'
import { ActionButton } from './ActionButton' import { ActionButton } from './ActionButton'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useState } from 'react' import { useState } from 'react'
import { useSearchParams } from 'react-router-dom'
import { CREATE_FILE_URL_PARAM } from 'lib/constants'
const DownloadAppBanner = () => { const DownloadAppBanner = () => {
const [searchParams] = useSearchParams()
const hasCreateFileParam = searchParams.has(CREATE_FILE_URL_PARAM)
const { settings } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
const [isBannerDismissed, setIsBannerDismissed] = useState( const [isBannerDismissed, setIsBannerDismissed] = useState(
settings.context.app.dismissWebBanner.current settings.context.app.dismissWebBanner.current || hasCreateFileParam
) )
return ( return (

View File

@ -168,7 +168,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
height: 'auto', height: 'auto',
}} }}
minWidth={200} minWidth={200}
maxWidth={800} maxWidth={window.innerWidth - 10}
handleWrapperClass="sidebar-resize-handles" handleWrapperClass="sidebar-resize-handles"
handleClasses={{ handleClasses={{
right: right:

View File

@ -104,7 +104,7 @@ function ProjectMenuPopover({
const location = useLocation() const location = useLocation()
const navigate = useNavigate() const navigate = useNavigate()
const filePath = useAbsoluteFilePath() const filePath = useAbsoluteFilePath()
const { settings } = useSettingsAuthContext() useSettingsAuthContext()
const token = useToken() const token = useToken()
const machineManager = useContext(MachineManagerContext) const machineManager = useContext(MachineManagerContext)
const commands = useSelector(commandBarActor, commandsSelector) const commands = useSelector(commandBarActor, commandsSelector)
@ -193,14 +193,13 @@ function ProjectMenuPopover({
{ {
id: 'share-link', id: 'share-link',
Element: 'button', Element: 'button',
children: 'Share link to file', children: 'Share current part (via Zoo link)',
disabled: IS_NIGHTLY_OR_DEBUG || !findCommand(shareCommandInfo), disabled: !(IS_NIGHTLY_OR_DEBUG && findCommand(shareCommandInfo)),
onClick: async () => { onClick: async () => {
await copyFileShareLink({ await copyFileShareLink({
token: token ?? '', token: token ?? '',
code: codeManager.code, code: codeManager.code,
name: project?.name || '', name: project?.name || '',
units: settings.context.modeling.defaultUnit.current,
}) })
}, },
}, },
@ -263,7 +262,7 @@ function ProjectMenuPopover({
as={Fragment} as={Fragment}
> >
<Popover.Panel <Popover.Panel
className={`z-10 absolute top-full left-0 mt-1 pb-1 w-48 bg-chalkboard-10 dark:bg-chalkboard-90 className={`z-10 absolute top-full left-0 mt-1 pb-1 w-52 bg-chalkboard-10 dark:bg-chalkboard-90
border border-solid border-chalkboard-20 dark:border-chalkboard-90 rounded border border-solid border-chalkboard-20 dark:border-chalkboard-90 rounded
shadow-lg`} shadow-lg`}
> >

View File

@ -30,15 +30,7 @@ import {
FILE_EXT, FILE_EXT,
PROJECT_ENTRYPOINT, PROJECT_ENTRYPOINT,
} from 'lib/constants' } from 'lib/constants'
import { DeepPartial } from 'lib/types' import { codeManager, kclManager } from 'lib/singletons'
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import { codeManager } from 'lib/singletons'
import {
loadAndValidateSettings,
projectConfigurationToSettingsPayload,
saveSettings,
setSettingsAtLevel,
} from 'lib/settings/settingsUtils'
import { Project } from 'lib/project' import { Project } from 'lib/project'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
@ -86,7 +78,7 @@ const ProjectsContextWeb = ({ children }: { children: React.ReactNode }) => {
setSearchParams(searchParams) setSearchParams(searchParams)
}, [searchParams, setSearchParams]) }, [searchParams, setSearchParams])
const { const {
settings: { context: settings, send: settingsSend }, settings: { context: settings },
} = useSettingsAuthContext() } = useSettingsAuthContext()
const [state, send, actor] = useMachine( const [state, send, actor] = useMachine(
@ -132,17 +124,10 @@ const ProjectsContextWeb = ({ children }: { children: React.ReactNode }) => {
clearImportSearchParams() clearImportSearchParams()
codeManager.updateCodeStateEditor(input.code || '') codeManager.updateCodeStateEditor(input.code || '')
await codeManager.writeToFile() await codeManager.writeToFile()
await kclManager.executeCode(true)
settingsSend({
type: 'set.modeling.defaultUnit',
data: {
level: 'project',
value: input.units,
},
})
return { return {
message: 'File and units overwritten successfully', message: 'File overwritten successfully',
fileName: input.name, fileName: input.name,
projectName: '', projectName: '',
} }
@ -392,16 +377,6 @@ const ProjectsContextDesktop = ({
? input.name ? input.name
: input.name + FILE_EXT : input.name + FILE_EXT
let message = 'File created successfully' let message = 'File created successfully'
const unitsConfiguration: DeepPartial<Configuration> = {
settings: {
project: {
directory: settings.app.projectDirectory.current,
},
modeling: {
base_unit: input.units,
},
},
}
const needsInterpolated = doesProjectNameNeedInterpolated(projectName) const needsInterpolated = doesProjectNameNeedInterpolated(projectName)
if (needsInterpolated) { if (needsInterpolated) {
@ -414,28 +389,10 @@ const ProjectsContextDesktop = ({
// Create the project around the file if newProject // Create the project around the file if newProject
if (input.method === 'newProject') { if (input.method === 'newProject') {
await createNewProjectDirectory( await createNewProjectDirectory(projectName, input.code)
projectName,
input.code,
unitsConfiguration
)
message = `Project "${projectName}" created successfully with link contents` message = `Project "${projectName}" created successfully with link contents`
} else { } else {
let projectPath = window.electron.join(
settings.app.projectDirectory.current,
projectName
)
message = `File "${fileName}" created successfully` message = `File "${fileName}" created successfully`
const existingConfiguration = await loadAndValidateSettings(
projectPath
)
const settingsToSave = setSettingsAtLevel(
existingConfiguration.settings,
'project',
projectConfigurationToSettingsPayload(unitsConfiguration)
)
await saveSettings(settingsToSave, projectPath)
} }
// Create the file // Create the file

View File

@ -1,9 +1,31 @@
import { Popover } from '@headlessui/react' import { Popover } from '@headlessui/react'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { changeKclSettings, unitLengthToUnitLen } from 'lang/wasm'
import { baseUnitLabels, baseUnitsUnion } from 'lib/settings/settingsTypes' import { baseUnitLabels, baseUnitsUnion } from 'lib/settings/settingsTypes'
import { codeManager, kclManager } from 'lib/singletons'
import { err, reportRejection } from 'lib/trap'
import { useEffect, useState } from 'react'
import toast from 'react-hot-toast'
export function UnitsMenu() { export function UnitsMenu() {
const { settings } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
const [hasPerFileLengthUnit, setHasPerFileLengthUnit] = useState(
Boolean(kclManager.fileSettings.defaultLengthUnit)
)
const [lengthSetting, setLengthSetting] = useState(
kclManager.fileSettings.defaultLengthUnit ||
settings.context.modeling.defaultUnit.current
)
useEffect(() => {
setHasPerFileLengthUnit(Boolean(kclManager.fileSettings.defaultLengthUnit))
setLengthSetting(
kclManager.fileSettings.defaultLengthUnit ||
settings.context.modeling.defaultUnit.current
)
}, [
kclManager.fileSettings.defaultLengthUnit,
settings.context.modeling.defaultUnit.current,
])
return ( return (
<Popover className="relative pointer-events-auto"> <Popover className="relative pointer-events-auto">
{({ close }) => ( {({ close }) => (
@ -18,7 +40,7 @@ export function UnitsMenu() {
<div className="absolute w-[1px] h-[1em] bg-primary right-0 top-1/2 -translate-y-1/2"></div> <div className="absolute w-[1px] h-[1em] bg-primary right-0 top-1/2 -translate-y-1/2"></div>
</div> </div>
<span className="sr-only">Current units are:&nbsp;</span> <span className="sr-only">Current units are:&nbsp;</span>
{settings.context.modeling.defaultUnit.current} {lengthSetting}
</Popover.Button> </Popover.Button>
<Popover.Panel <Popover.Panel
className={`absolute bottom-full right-0 mb-2 w-48 bg-chalkboard-10 dark:bg-chalkboard-90 className={`absolute bottom-full right-0 mb-2 w-48 bg-chalkboard-10 dark:bg-chalkboard-90
@ -31,18 +53,40 @@ export function UnitsMenu() {
<button <button
className="flex items-center gap-2 m-0 py-1.5 px-2 cursor-pointer hover:bg-chalkboard-20 dark:hover:bg-chalkboard-80 border-none text-left" className="flex items-center gap-2 m-0 py-1.5 px-2 cursor-pointer hover:bg-chalkboard-20 dark:hover:bg-chalkboard-80 border-none text-left"
onClick={() => { onClick={() => {
settings.send({ if (hasPerFileLengthUnit) {
type: 'set.modeling.defaultUnit', const newCode = changeKclSettings(codeManager.code, {
data: { defaultLengthUnits: unitLengthToUnitLen(unit),
level: 'project', defaultAngleUnits: { type: 'Degrees' },
value: unit, })
}, if (err(newCode)) {
}) toast.error(
`Failed to set per-file units: ${newCode.message}`
)
} else {
codeManager.updateCodeStateEditor(newCode)
Promise.all([
codeManager.writeToFile(),
kclManager.executeCode(),
])
.then(() => {
toast.success(`Updated per-file units to ${unit}`)
})
.catch(reportRejection)
}
} else {
settings.send({
type: 'set.modeling.defaultUnit',
data: {
level: 'project',
value: unit,
},
})
}
close() close()
}} }}
> >
<span className="flex-1">{baseUnitLabels[unit]}</span> <span className="flex-1">{baseUnitLabels[unit]}</span>
{unit === settings.context.modeling.defaultUnit.current && ( {unit === lengthSetting && (
<span className="text-chalkboard-60">current</span> <span className="text-chalkboard-60">current</span>
)} )}
</button> </button>

View File

@ -6,7 +6,6 @@ import { useSettingsAuthContext } from './useSettingsAuthContext'
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
import { FileLinkParams } from 'lib/links' import { FileLinkParams } from 'lib/links'
import { ProjectsCommandSchema } from 'lib/commandBarConfigs/projectsCommandConfig' import { ProjectsCommandSchema } from 'lib/commandBarConfigs/projectsCommandConfig'
import { baseUnitsUnion } from 'lib/settings/settingsTypes'
// For initializing the command arguments, we actually want `method` to be undefined // For initializing the command arguments, we actually want `method` to be undefined
// so that we don't skip it in the command palette. // so that we don't skip it in the command palette.
@ -37,13 +36,7 @@ export function useCreateFileLinkQuery(
code: base64ToString( code: base64ToString(
decodeURIComponent(searchParams.get('code') ?? '') decodeURIComponent(searchParams.get('code') ?? '')
), ),
name: searchParams.get('name') ?? DEFAULT_FILE_NAME, name: searchParams.get('name') ?? DEFAULT_FILE_NAME,
units:
(baseUnitsUnion.find((unit) => searchParams.get('units') === unit) ||
settings.context.modeling.defaultUnit.default) ??
settings.context.modeling.defaultUnit.current,
} }
const argDefaultValues: CreateFileSchemaMethodOptional = { const argDefaultValues: CreateFileSchemaMethodOptional = {
@ -55,7 +48,6 @@ export function useCreateFileLinkQuery(
? settings.context.projects.defaultProjectName.current ? settings.context.projects.defaultProjectName.current
: DEFAULT_FILE_NAME, : DEFAULT_FILE_NAME,
code: params.code || '', code: params.code || '',
units: params.units,
method: isDesktop() ? undefined : 'existingProject', method: isDesktop() ? undefined : 'existingProject',
} }

View File

@ -25,7 +25,7 @@ import {
SourceRange, SourceRange,
topLevelRange, topLevelRange,
} from 'lang/wasm' } from 'lang/wasm'
import { getNodeFromPath } from './queryAst' import { getNodeFromPath, getSettingsAnnotation } from './queryAst'
import { codeManager, editorManager, sceneInfra } from 'lib/singletons' import { codeManager, editorManager, sceneInfra } from 'lib/singletons'
import { Diagnostic } from '@codemirror/lint' import { Diagnostic } from '@codemirror/lint'
import { markOnce } from 'lib/performance' import { markOnce } from 'lib/performance'
@ -35,6 +35,7 @@ import {
ModelingCmdReq_type, ModelingCmdReq_type,
} from '@kittycad/lib/dist/types/src/models' } from '@kittycad/lib/dist/types/src/models'
import { Operation } from 'wasm-lib/kcl/bindings/Operation' import { Operation } from 'wasm-lib/kcl/bindings/Operation'
import { KclSettingsAnnotation } from 'lib/settings/settingsTypes'
interface ExecuteArgs { interface ExecuteArgs {
ast?: Node<Program> ast?: Node<Program>
@ -70,6 +71,7 @@ export class KclManager {
private _wasmInitFailed = true private _wasmInitFailed = true
private _hasErrors = false private _hasErrors = false
private _switchedFiles = false private _switchedFiles = false
private _fileSettings: KclSettingsAnnotation = {}
engineCommandManager: EngineCommandManager engineCommandManager: EngineCommandManager
@ -368,6 +370,13 @@ export class KclManager {
await this.disableSketchMode() await this.disableSketchMode()
} }
let fileSettings = getSettingsAnnotation(ast)
if (err(fileSettings)) {
console.error(fileSettings)
fileSettings = {}
}
this.fileSettings = fileSettings
this.logs = logs this.logs = logs
this.errors = errors this.errors = errors
// Do not add the errors since the program was interrupted and the error is not a real KCL error // Do not add the errors since the program was interrupted and the error is not a real KCL error
@ -413,14 +422,7 @@ export class KclManager {
// NOTE: this always updates the code state and editor. // NOTE: this always updates the code state and editor.
// DO NOT CALL THIS from codemirror ever. // DO NOT CALL THIS from codemirror ever.
async executeAstMock( async executeAstMock(ast: Program = this._ast) {
ast: Program = this._ast,
{
updates,
}: {
updates: 'none' | 'artifactRanges'
} = { updates: 'none' }
) {
await this.ensureWasmInit() await this.ensureWasmInit()
const newCode = recast(ast) const newCode = recast(ast)
@ -450,34 +452,6 @@ export class KclManager {
this.lastSuccessfulProgramMemory = execState.memory this.lastSuccessfulProgramMemory = execState.memory
this.lastSuccessfulOperations = execState.operations this.lastSuccessfulOperations = execState.operations
} }
if (updates !== 'artifactRanges') return
// TODO the below seems like a work around, I wish there's a comment explaining exactly what
// problem this solves, but either way we should strive to remove it.
Array.from(this.engineCommandManager.artifactGraph).forEach(
([commandId, artifact]) => {
if (!('codeRef' in artifact && artifact.codeRef)) return
const _node1 = getNodeFromPath<Node<CallExpression | CallExpressionKw>>(
this.ast,
artifact.codeRef.pathToNode,
['CallExpression', 'CallExpressionKw']
)
if (err(_node1)) return
const { node } = _node1
if (node.type !== 'CallExpression' && node.type !== 'CallExpressionKw')
return
const [oldStart, oldEnd] = artifact.codeRef.range
if (oldStart === 0 && oldEnd === 0) return
if (oldStart === node.start && oldEnd === node.end) return
this.engineCommandManager.artifactGraph.set(commandId, {
...artifact,
codeRef: {
...artifact.codeRef,
range: topLevelRange(node.start, node.end),
},
})
}
)
} }
cancelAllExecutions() { cancelAllExecutions() {
this._cancelTokens.forEach((_, key) => { this._cancelTokens.forEach((_, key) => {
@ -699,6 +673,14 @@ export class KclManager {
_isAstEmpty(ast: Node<Program>) { _isAstEmpty(ast: Node<Program>) {
return ast.start === 0 && ast.end === 0 && ast.body.length === 0 return ast.start === 0 && ast.end === 0 && ast.body.length === 0
} }
get fileSettings() {
return this._fileSettings
}
set fileSettings(settings: KclSettingsAnnotation) {
this._fileSettings = settings
}
} }
const defaultSelectionFilter: EntityType_type[] = [ const defaultSelectionFilter: EntityType_type[] = [

View File

@ -55,6 +55,7 @@ const mySketch001 = startSketchOn('XY')
], ],
id: expect.any(String), id: expect.any(String),
artifactId: expect.any(String), artifactId: expect.any(String),
originalId: expect.any(String),
units: { units: {
type: 'Mm', type: 'Mm',
}, },
@ -98,6 +99,7 @@ const mySketch001 = startSketchOn('XY')
], ],
sketch: { sketch: {
id: expect.any(String), id: expect.any(String),
originalId: expect.any(String),
artifactId: expect.any(String), artifactId: expect.any(String),
units: { units: {
type: 'Mm', type: 'Mm',
@ -203,6 +205,7 @@ const sk2 = startSketchOn('XY')
], ],
sketch: { sketch: {
id: expect.any(String), id: expect.any(String),
originalId: expect.any(String),
artifactId: expect.any(String), artifactId: expect.any(String),
__meta: expect.any(Array), __meta: expect.any(Array),
on: expect.any(Object), on: expect.any(Object),
@ -308,6 +311,7 @@ const sk2 = startSketchOn('XY')
], ],
sketch: { sketch: {
id: expect.any(String), id: expect.any(String),
originalId: expect.any(String),
artifactId: expect.any(String), artifactId: expect.any(String),
units: { units: {
type: 'Mm', type: 'Mm',

View File

@ -221,6 +221,7 @@ const newVar = myVar + 1`
}, },
], ],
id: expect.any(String), id: expect.any(String),
originalId: expect.any(String),
artifactId: expect.any(String), artifactId: expect.any(String),
units: { units: {
type: 'Mm', type: 'Mm',

View File

@ -28,7 +28,14 @@ try {
console.log(e) console.log(e)
} }
child_process.spawnSync('git', ['clone', URL_GIT_KCL_SAMPLES, DIR_KCL_SAMPLES]) child_process.spawnSync('git', [
'clone',
'--single-branch',
'--branch',
'achalmers/kw-appearance',
URL_GIT_KCL_SAMPLES,
DIR_KCL_SAMPLES,
])
// @ts-expect-error // @ts-expect-error
let files = await fs.readdir(DIR_KCL_SAMPLES) let files = await fs.readdir(DIR_KCL_SAMPLES)

View File

@ -431,10 +431,11 @@ export function addSweep(
} { } {
const modifiedAst = structuredClone(node) const modifiedAst = structuredClone(node)
const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.SWEEP) const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.SWEEP)
const sweep = createCallExpressionStdLib('sweep', [ const sweep = createCallExpressionStdLibKw(
createObjectExpression({ path: createIdentifier(pathDeclarator.id.name) }), 'sweep',
createIdentifier(profileDeclarator.id.name), createIdentifier(profileDeclarator.id.name),
]) [createLabeledArg('path', createIdentifier(pathDeclarator.id.name))]
)
const declaration = createVariableDeclaration(name, sweep) const declaration = createVariableDeclaration(name, sweep)
modifiedAst.body.push(declaration) modifiedAst.body.push(declaration)
const pathToNode: PathToNode = [ const pathToNode: PathToNode = [
@ -442,8 +443,9 @@ export function addSweep(
[modifiedAst.body.length - 1, 'index'], [modifiedAst.body.length - 1, 'index'],
['declaration', 'VariableDeclaration'], ['declaration', 'VariableDeclaration'],
['init', 'VariableDeclarator'], ['init', 'VariableDeclarator'],
['arguments', 'CallExpression'], ['arguments', 'CallExpressionKw'],
[0, 'index'], [0, ARG_INDEX_FIELD],
['arg', LABELED_ARG_FIELD],
] ]
return { return {
@ -683,6 +685,63 @@ export function addOffsetPlane({
} }
} }
/**
* Append a helix to the AST
*/
export function addHelix({
node,
revolutions,
angleStart,
counterClockWise,
radius,
axis,
length,
}: {
node: Node<Program>
revolutions: Expr
angleStart: Expr
counterClockWise: boolean
radius: Expr
axis: string
length: Expr
}): { modifiedAst: Node<Program>; pathToNode: PathToNode } {
const modifiedAst = structuredClone(node)
const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.HELIX)
const variable = createVariableDeclaration(
name,
createCallExpressionStdLibKw(
'helix',
null, // Not in a pipeline
[
createLabeledArg('revolutions', revolutions),
createLabeledArg('angleStart', angleStart),
createLabeledArg('counterClockWise', createLiteral(counterClockWise)),
createLabeledArg('radius', radius),
createLabeledArg('axis', createLiteral(axis)),
createLabeledArg('length', length),
]
)
)
// TODO: figure out smart insertion than just appending at the end
const argIndex = 0
modifiedAst.body.push(variable)
const pathToNode: PathToNode = [
['body', ''],
[modifiedAst.body.length - 1, 'index'],
['declaration', 'VariableDeclaration'],
['init', 'VariableDeclarator'],
['arguments', 'CallExpressionKw'],
[argIndex, ARG_INDEX_FIELD],
['arg', LABELED_ARG_FIELD],
]
return {
modifiedAst,
pathToNode,
}
}
/** /**
* Return a modified clone of an AST with a named constant inserted into the body * Return a modified clone of an AST with a named constant inserted into the body
*/ */

View File

@ -17,6 +17,8 @@ import {
createObjectExpression, createObjectExpression,
createArrayExpression, createArrayExpression,
createVariableDeclaration, createVariableDeclaration,
createCallExpressionStdLibKw,
createLabeledArg,
} from 'lang/modifyAst' } from 'lang/modifyAst'
import { KCL_DEFAULT_CONSTANT_PREFIXES } from 'lib/constants' import { KCL_DEFAULT_CONSTANT_PREFIXES } from 'lib/constants'
import { KclManager } from 'lang/KclSingleton' import { KclManager } from 'lang/KclSingleton'
@ -121,13 +123,14 @@ export function addShell({
} }
const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.SHELL) const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.SHELL)
const shell = createCallExpressionStdLib('shell', [ const shell = createCallExpressionStdLibKw(
createObjectExpression({ 'shell',
faces: createArrayExpression(expressions),
thickness,
}),
createIdentifier(extrudeNode.node.id.name), createIdentifier(extrudeNode.node.id.name),
]) [
createLabeledArg('faces', createArrayExpression(expressions)),
createLabeledArg('thickness', thickness),
]
)
const declaration = createVariableDeclaration(name, shell) const declaration = createVariableDeclaration(name, shell)
// TODO: check if we should append at the end like here or right after the extrude // TODO: check if we should append at the end like here or right after the extrude
@ -137,8 +140,7 @@ export function addShell({
[modifiedAst.body.length - 1, 'index'], [modifiedAst.body.length - 1, 'index'],
['declaration', 'VariableDeclaration'], ['declaration', 'VariableDeclaration'],
['init', 'VariableDeclarator'], ['init', 'VariableDeclarator'],
['arguments', 'CallExpression'], ['unlabeled', 'CallExpressionKw'],
[0, 'index'],
] ]
return { return {
modifiedAst, modifiedAst,

View File

@ -23,6 +23,9 @@ import {
VariableDeclaration, VariableDeclaration,
VariableDeclarator, VariableDeclarator,
recast, recast,
kclSettings,
unitLenToUnitLength,
unitAngToUnitAngle,
} from './wasm' } from './wasm'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils' import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { createIdentifier, splitPathAtLastIndex } from './modifyAst' import { createIdentifier, splitPathAtLastIndex } from './modifyAst'
@ -38,6 +41,7 @@ import { ImportStatement } from 'wasm-lib/kcl/bindings/ImportStatement'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
import { findKwArg } from './util' import { findKwArg } from './util'
import { codeRefFromRange } from './std/artifactGraph' import { codeRefFromRange } from './std/artifactGraph'
import { KclSettingsAnnotation } from 'lib/settings/settingsTypes'
export const LABELED_ARG_FIELD = 'LabeledArg -> Arg' export const LABELED_ARG_FIELD = 'LabeledArg -> Arg'
export const ARG_INDEX_FIELD = 'arg index' export const ARG_INDEX_FIELD = 'arg index'
@ -866,3 +870,24 @@ export function getObjExprProperty(
if (index === -1) return null if (index === -1) return null
return { expr: node.properties[index].value, index } return { expr: node.properties[index].value, index }
} }
/**
* Given KCL, returns the settings annotation object if it exists.
*/
export function getSettingsAnnotation(
kcl: string | Node<Program>
): KclSettingsAnnotation | Error {
const metaSettings = kclSettings(kcl)
if (err(metaSettings)) return metaSettings
const settings: KclSettingsAnnotation = {}
// No settings in the KCL.
if (!metaSettings) return settings
settings.defaultLengthUnit = unitLenToUnitLength(
metaSettings.defaultLengthUnits
)
settings.defaultAngleUnit = unitAngToUnitAngle(metaSettings.defaultAngleUnits)
return settings
}

View File

@ -1406,6 +1406,8 @@ export class EngineCommandManager extends EventTarget {
commandId: string commandId: string
} }
settings: SettingsViaQueryString settings: SettingsViaQueryString
width: number = 1337
height: number = 1337
/** /**
* Export intent traxcks the intent of the export. If it is null there is no * Export intent traxcks the intent of the export. If it is null there is no
@ -1511,6 +1513,9 @@ export class EngineCommandManager extends EventTarget {
return return
} }
this.width = width
this.height = height
// If we already have an engine connection, just need to resize the stream. // If we already have an engine connection, just need to resize the stream.
if (this.engineConnection) { if (this.engineConnection) {
this.handleResize({ this.handleResize({
@ -1823,6 +1828,9 @@ export class EngineCommandManager extends EventTarget {
return return
} }
this.width = streamWidth
this.height = streamHeight
const resizeCmd: EngineCommand = { const resizeCmd: EngineCommand = {
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd_id: uuidv4(), cmd_id: uuidv4(),

View File

@ -18,6 +18,7 @@ import {
default_project_settings, default_project_settings,
base64_decode, base64_decode,
clear_scene_and_bust_cache, clear_scene_and_bust_cache,
kcl_settings,
change_kcl_settings, change_kcl_settings,
reloadModule, reloadModule,
} from 'lib/wasm_lib_wrapper' } from 'lib/wasm_lib_wrapper'
@ -58,6 +59,9 @@ import { Artifact } from './std/artifactGraph'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils' import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { NumericSuffix } from 'wasm-lib/kcl/bindings/NumericSuffix' import { NumericSuffix } from 'wasm-lib/kcl/bindings/NumericSuffix'
import { MetaSettings } from 'wasm-lib/kcl/bindings/MetaSettings' import { MetaSettings } from 'wasm-lib/kcl/bindings/MetaSettings'
import { UnitAngle, UnitLength } from 'wasm-lib/kcl/bindings/ModelingCmd'
import { UnitLen } from 'wasm-lib/kcl/bindings/UnitLen'
import { UnitAngle as UnitAng } from 'wasm-lib/kcl/bindings/UnitAngle'
export type { Artifact } from 'wasm-lib/kcl/bindings/Artifact' export type { Artifact } from 'wasm-lib/kcl/bindings/Artifact'
export type { ArtifactCommand } from 'wasm-lib/kcl/bindings/Artifact' export type { ArtifactCommand } from 'wasm-lib/kcl/bindings/Artifact'
@ -857,8 +861,35 @@ export function base64Decode(base64: string): ArrayBuffer | Error {
} }
} }
/// Change the meta settings for the kcl file. /**
/// Returns the new kcl string with the updated settings. * Get the meta settings for the KCL. If no settings were set in the file,
* returns null.
*/
export function kclSettings(
kcl: string | Node<Program>
): MetaSettings | null | Error {
let program: Node<Program>
if (typeof kcl === 'string') {
const parseResult = parse(kcl)
if (err(parseResult)) return parseResult
if (!resultIsOk(parseResult)) {
return new Error(`parse result had errors`, { cause: parseResult })
}
program = parseResult.program
} else {
program = kcl
}
try {
return kcl_settings(JSON.stringify(program))
} catch (e) {
return new Error('Caught error getting kcl settings', { cause: e })
}
}
/**
* Change the meta settings for the kcl file.
* @returns the new kcl string with the updated settings.
*/
export function changeKclSettings( export function changeKclSettings(
kcl: string, kcl: string,
settings: MetaSettings settings: MetaSettings
@ -866,7 +897,59 @@ export function changeKclSettings(
try { try {
return change_kcl_settings(kcl, JSON.stringify(settings)) return change_kcl_settings(kcl, JSON.stringify(settings))
} catch (e) { } catch (e) {
console.error('Caught error changing kcl settings: ' + e) console.error('Caught error changing kcl settings', e)
return new Error('Caught error changing kcl settings: ' + e) return new Error('Caught error changing kcl settings', { cause: e })
}
}
/**
* Convert a `UnitLength_type` to a `UnitLen`
*/
export function unitLengthToUnitLen(input: UnitLength): UnitLen {
switch (input) {
case 'm':
return { type: 'M' }
case 'cm':
return { type: 'Cm' }
case 'yd':
return { type: 'Yards' }
case 'ft':
return { type: 'Feet' }
case 'in':
return { type: 'Inches' }
default:
return { type: 'Mm' }
}
}
/**
* Convert `UnitLen` to `UnitLength_type`.
*/
export function unitLenToUnitLength(input: UnitLen): UnitLength {
switch (input.type) {
case 'M':
return 'm'
case 'Cm':
return 'cm'
case 'Yards':
return 'yd'
case 'Feet':
return 'ft'
case 'Inches':
return 'in'
default:
return 'mm'
}
}
/**
* Convert `UnitAngle` to `UnitAngle_type`.
*/
export function unitAngToUnitAngle(input: UnitAng): UnitAngle {
switch (input.type) {
case 'Radians':
return 'radians'
default:
return 'degrees'
} }
} }

View File

@ -76,6 +76,14 @@ export type ModelingCommandSchema = {
plane: Selections plane: Selections
distance: KclCommandValue distance: KclCommandValue
} }
Helix: {
revolutions: KclCommandValue
angleStart: KclCommandValue
counterClockWise: boolean
radius: KclCommandValue
axis: string
length: KclCommandValue
}
'change tool': { 'change tool': {
tool: SketchTool tool: SketchTool
} }
@ -447,6 +455,53 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
}, },
}, },
}, },
Helix: {
description: 'Create a helix or spiral in 3D about an axis.',
icon: 'helix',
status: 'development',
needsReview: true,
args: {
revolutions: {
inputType: 'kcl',
defaultValue: '1',
required: true,
warningMessage:
'The helix workflow is new and under tested. Please break it and report issues.',
},
angleStart: {
inputType: 'kcl',
defaultValue: KCL_DEFAULT_DEGREE,
required: true,
},
counterClockWise: {
inputType: 'options',
required: true,
options: [
{ name: 'True', isCurrent: false, value: true },
{ name: 'False', isCurrent: true, value: false },
],
},
radius: {
inputType: 'kcl',
defaultValue: KCL_DEFAULT_LENGTH,
required: true,
},
axis: {
inputType: 'options',
required: true,
options: [
{ name: 'X Axis', isCurrent: true, value: 'X' },
{ name: 'Y Axis', isCurrent: false, value: 'Y' },
{ name: 'Z Axis', isCurrent: false, value: 'Z' },
],
},
length: {
inputType: 'kcl',
defaultValue: KCL_DEFAULT_LENGTH,
required: true,
},
},
},
Fillet: { Fillet: {
description: 'Fillet edge', description: 'Fillet edge',
icon: 'fillet3d', icon: 'fillet3d',

View File

@ -1,8 +1,6 @@
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarning' import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarning'
import { StateMachineCommandSetConfig } from 'lib/commandTypes' import { StateMachineCommandSetConfig } from 'lib/commandTypes'
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
import { baseUnitLabels, baseUnitsUnion } from 'lib/settings/settingsTypes'
import { projectsMachine } from 'machines/projectsMachine' import { projectsMachine } from 'machines/projectsMachine'
export type ProjectsCommandSchema = { export type ProjectsCommandSchema = {
@ -23,7 +21,6 @@ export type ProjectsCommandSchema = {
'Import file from URL': { 'Import file from URL': {
name: string name: string
code?: string code?: string
units: UnitLength_type
method: 'newProject' | 'existingProject' method: 'newProject' | 'existingProject'
projectName?: string projectName?: string
} }
@ -157,15 +154,6 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
return `${lineCount} line${lineCount === 1 ? '' : 's'}` return `${lineCount} line${lineCount === 1 ? '' : 's'}`
}, },
}, },
units: {
inputType: 'options',
required: false,
skip: true,
options: baseUnitsUnion.map((unit) => ({
name: baseUnitLabels[unit],
value: unit,
})),
},
}, },
reviewMessage(commandBarContext) { reviewMessage(commandBarContext) {
return isDesktop() return isDesktop()

View File

@ -58,6 +58,7 @@ export const KCL_DEFAULT_CONSTANT_PREFIXES = {
SEGMENT: 'seg', SEGMENT: 'seg',
REVOLVE: 'revolve', REVOLVE: 'revolve',
PLANE: 'plane', PLANE: 'plane',
HELIX: 'helix',
} as const } as const
/** The default KCL length expression */ /** The default KCL length expression */
export const KCL_DEFAULT_LENGTH = `5` export const KCL_DEFAULT_LENGTH = `5`

View File

@ -12,104 +12,68 @@ wallMountL = 2 // inches
shelfDepth = 12 // Shelf is 12 inches in depth from the wall shelfDepth = 12 // Shelf is 12 inches in depth from the wall
moment = shelfDepth * p // assume the force is applied at the end of the shelf to be conservative (lb-in) moment = shelfDepth * p // assume the force is applied at the end of the shelf to be conservative (lb-in)
filletRadius = .375 // inches
extFilletRadius = .25 // inches
mountingHoleDiameter = 0.5 // inches
// Calculate required thickness of bracket // Calculate required thickness of bracket
thickness = sqrt(moment * factorOfSafety * 6 / (sigmaAllow * width)) // this is the calculation of two brackets holding up the shelf (inches) thickness = sqrt(moment * factorOfSafety * 6 / (sigmaAllow * width)) // this is the calculation of two brackets holding up the shelf (inches)
filletRadius = .25
extFilletRadius = filletRadius + thickness
mountingHoleDiameter = 0.5
// Sketch the bracket body and fillet the inner and outer edges of the bend sketch001 = startSketchOn('XZ')
bracketLeg1Sketch = startSketchOn('XY')
|> startProfileAt([0, 0], %) |> startProfileAt([0, 0], %)
|> line(end = [shelfMountL - filletRadius, 0], tag = $fillet1) |> xLine(shelfMountL - thickness, %, $seg01)
|> line(end = [0, width], tag = $fillet2) |> yLine(thickness, %, $seg02)
|> line(end = [-shelfMountL + filletRadius, 0]) |> xLine(-shelfMountL, %, $seg03)
|> yLine(-wallMountL, %, $seg04)
|> xLine(thickness, %, $seg05)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg06)
|> close() |> close()
|> hole(circle({ |> extrude(%, length = width)
center = [1, 1],
radius = mountingHoleDiameter / 2
}, %), %)
|> hole(circle({
center = [shelfMountL - 1.5, width - 1],
radius = mountingHoleDiameter / 2
}, %), %)
|> hole(circle({
center = [1, width - 1],
radius = mountingHoleDiameter / 2
}, %), %)
|> hole(circle({
center = [shelfMountL - 1.5, 1],
radius = mountingHoleDiameter / 2
}, %), %)
// Extrude the leg 2 bracket sketch
bracketLeg1Extrude = extrude(bracketLeg1Sketch, length = thickness)
|> fillet({ |> fillet({
radius = extFilletRadius, radius = extFilletRadius,
tags = [ tags = [getNextAdjacentEdge(seg03)]
getNextAdjacentEdge(fillet1),
getNextAdjacentEdge(fillet2)
]
}, %) }, %)
// Sketch the fillet arc
filletSketch = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> line(end = [0, thickness])
|> arc({
angleEnd = 180,
angleStart = 90,
radius = filletRadius + thickness
}, %)
|> line(end = [thickness, 0])
|> arc({
angleEnd = 90,
angleStart = 180,
radius = filletRadius
}, %)
// Sketch the bend
filletExtrude = extrude(filletSketch, length = -width)
// Create a custom plane for the leg that sits on the wall
customPlane = {
plane = {
origin = { x = -filletRadius, y = 0, z = 0 },
xAxis = { x = 0, y = 1, z = 0 },
yAxis = { x = 0, y = 0, z = 1 },
zAxis = { x = 1, y = 0, z = 0 }
}
}
// Create a sketch for the second leg
bracketLeg2Sketch = startSketchOn(customPlane)
|> startProfileAt([0, -filletRadius], %)
|> line(end = [width, 0])
|> line(end = [0, -wallMountL], tag = $fillet3)
|> line(end = [-width, 0], tag = $fillet4)
|> close()
|> hole(circle({
center = [1, -1.5],
radius = mountingHoleDiameter / 2
}, %), %)
|> hole(circle({
center = [5, -1.5],
radius = mountingHoleDiameter / 2
}, %), %)
// Extrude the second leg
bracketLeg2Extrude = extrude(bracketLeg2Sketch, length = -thickness)
|> fillet({ |> fillet({
radius = extFilletRadius, radius = filletRadius,
tags = [ tags = [getNextAdjacentEdge(seg06)]
getNextAdjacentEdge(fillet3),
getNextAdjacentEdge(fillet4)
]
}, %) }, %)
|> fillet({
radius = filletRadius,
tags = [seg02, getOppositeEdge(seg02)],
}, %)
|> fillet({
radius = filletRadius,
tags = [seg05, getOppositeEdge(seg05)],
}, %)
sketch002 = startSketchOn(sketch001, seg03)
|> circle({
center = [-1.25, 1],
radius = mountingHoleDiameter / 2,
}, %)
|> patternLinear2d({
instances = 2,
distance = 2.5,
axis = [-1, 0],
}, %)
|> patternLinear2d({
instances = 2,
distance = 4,
axis = [0, 1],
}, %)
|> extrude(%, length = -thickness-.01)
sketch003 = startSketchOn(sketch001, seg04)
|> circle({
center = [1, -1],
radius = mountingHoleDiameter / 2,
}, %)
|> patternLinear2d({
instances = 2,
distance = 4,
axis = [1, 0],
}, %)
|> extrude(%, length = -thickness-0.1)
` `
/** /**

View File

@ -136,7 +136,7 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] {
}, },
{ {
name: 'share-file-link', name: 'share-file-link',
displayName: 'Share file', displayName: 'Share current part (via Zoo link)',
hide: IS_NIGHTLY_OR_DEBUG ? undefined : 'desktop', hide: IS_NIGHTLY_OR_DEBUG ? undefined : 'desktop',
description: 'Create a link that contains a copy of the current file.', description: 'Create a link that contains a copy of the current file.',
groupId: 'code', groupId: 'code',
@ -147,7 +147,6 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] {
token: commandProps.authToken, token: commandProps.authToken,
code: codeManager.code, code: codeManager.code,
name: commandProps.projectData.project?.name || '', name: commandProps.projectData.project?.name || '',
units: commandProps.settings.defaultUnit,
}).catch(reportRejection) }).catch(reportRejection)
}, },
}, },

View File

@ -5,13 +5,12 @@ describe(`link creation tests`, () => {
test(`createCreateFileUrl happy path`, async () => { test(`createCreateFileUrl happy path`, async () => {
const code = `extrusionDistance = 12` const code = `extrusionDistance = 12`
const name = `test` const name = `test`
const units = `mm`
// Converted with external online tools // Converted with external online tools
const expectedEncodedCode = `ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg%3D%3D` const expectedEncodedCode = `ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg%3D%3D`
const expectedLink = `${VITE_KC_SITE_APP_URL}/?create-file=true&name=test&units=mm&code=${expectedEncodedCode}&ask-open-desktop=true` const expectedLink = `${VITE_KC_SITE_APP_URL}/?create-file=true&name=test&code=${expectedEncodedCode}&ask-open-desktop=true`
const result = createCreateFileUrl({ code, name, units }) const result = createCreateFileUrl({ code, name })
expect(result.toString()).toBe(expectedLink) expect(result.toString()).toBe(expectedLink)
}) })
}) })

View File

@ -1,4 +1,3 @@
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
import { ASK_TO_OPEN_QUERY_PARAM, CREATE_FILE_URL_PARAM } from './constants' import { ASK_TO_OPEN_QUERY_PARAM, CREATE_FILE_URL_PARAM } from './constants'
import { stringToBase64 } from './base64' import { stringToBase64 } from './base64'
import { VITE_KC_API_BASE_URL, VITE_KC_SITE_APP_URL } from 'env' import { VITE_KC_API_BASE_URL, VITE_KC_SITE_APP_URL } from 'env'
@ -7,7 +6,6 @@ import { err } from './trap'
export interface FileLinkParams { export interface FileLinkParams {
code: string code: string
name: string name: string
units: UnitLength_type
} }
export async function copyFileShareLink( export async function copyFileShareLink(
@ -46,12 +44,11 @@ export async function copyFileShareLink(
* With the additional step of asking the user if they want to * With the additional step of asking the user if they want to
* open the URL in the desktop app. * open the URL in the desktop app.
*/ */
export function createCreateFileUrl({ code, name, units }: FileLinkParams) { export function createCreateFileUrl({ code, name }: FileLinkParams) {
let origin = VITE_KC_SITE_APP_URL let origin = VITE_KC_SITE_APP_URL
const searchParams = new URLSearchParams({ const searchParams = new URLSearchParams({
[CREATE_FILE_URL_PARAM]: String(true), [CREATE_FILE_URL_PARAM]: String(true),
name, name,
units,
code: stringToBase64(code), code: stringToBase64(code),
[ASK_TO_OPEN_QUERY_PARAM]: String(true), [ASK_TO_OPEN_QUERY_PARAM]: String(true),
}) })

View File

@ -819,8 +819,8 @@ export async function sendSelectEventToEngine(
clientX: e.clientX, clientX: e.clientX,
clientY: e.clientY, clientY: e.clientY,
el, el,
streamWidth: el.clientWidth, streamWidth: engineCommandManager.width,
streamHeight: el.clientHeight, streamHeight: engineCommandManager.height,
}) })
const res = await engineCommandManager.sendSceneCommand({ const res = await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',

View File

@ -4,6 +4,10 @@ import { AtLeast, PathValue, Paths } from 'lib/types'
import { CommandArgumentConfig } from 'lib/commandTypes' import { CommandArgumentConfig } from 'lib/commandTypes'
import { Themes } from 'lib/theme' import { Themes } from 'lib/theme'
import { CameraProjectionType } from 'wasm-lib/kcl/bindings/CameraProjectionType' import { CameraProjectionType } from 'wasm-lib/kcl/bindings/CameraProjectionType'
import {
UnitAngle_type,
UnitLength_type,
} from '@kittycad/lib/dist/types/src/models'
import { CameraOrbitType } from 'wasm-lib/kcl/bindings/CameraOrbitType' import { CameraOrbitType } from 'wasm-lib/kcl/bindings/CameraOrbitType'
export interface SettingsViaQueryString { export interface SettingsViaQueryString {
@ -138,3 +142,12 @@ type RecursiveSettingsPayloads<T> = {
} }
export type SaveSettingsPayload = RecursiveSettingsPayloads<typeof settings> export type SaveSettingsPayload = RecursiveSettingsPayloads<typeof settings>
/**
* Annotation names for default units are defined on rust side in
* src/wasm-lib/kcl/src/execution/annotations.rs
*/
export interface KclSettingsAnnotation {
defaultLengthUnit?: UnitLength_type
defaultAngleUnit?: UnitAngle_type
}

View File

@ -290,9 +290,15 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
], ],
{ {
id: 'helix', id: 'helix',
onClick: () => console.error('Helix not yet implemented'), onClick: () => {
commandBarActor.send({
type: 'Find and select command',
data: { name: 'Helix', groupId: 'modeling' },
})
},
hotkey: 'H',
icon: 'helix', icon: 'helix',
status: 'kcl-only', status: DEV || IS_NIGHTLY_OR_DEBUG ? 'available' : 'kcl-only',
title: 'Helix', title: 'Helix',
description: 'Create a helix or spiral in 3D about an axis.', description: 'Create a helix or spiral in 3D about an axis.',
links: [{ label: 'KCL docs', url: 'https://zoo.dev/docs/kcl/helix' }], links: [{ label: 'KCL docs', url: 'https://zoo.dev/docs/kcl/helix' }],

View File

@ -26,6 +26,7 @@ import {
default_project_settings as DefaultProjectSettings, default_project_settings as DefaultProjectSettings,
base64_decode as Base64Decode, base64_decode as Base64Decode,
clear_scene_and_bust_cache as ClearSceneAndBustCache, clear_scene_and_bust_cache as ClearSceneAndBustCache,
kcl_settings as KclSettings,
change_kcl_settings as ChangeKclSettings, change_kcl_settings as ChangeKclSettings,
} from '../wasm-lib/pkg/wasm_lib' } from '../wasm-lib/pkg/wasm_lib'
@ -111,6 +112,9 @@ export const clear_scene_and_bust_cache: typeof ClearSceneAndBustCache = (
) => { ) => {
return getModule().clear_scene_and_bust_cache(...args) return getModule().clear_scene_and_bust_cache(...args)
} }
export const kcl_settings: typeof KclSettings = (...args) => {
return getModule().kcl_settings(...args)
}
export const change_kcl_settings: typeof ChangeKclSettings = (...args) => { export const change_kcl_settings: typeof ChangeKclSettings = (...args) => {
return getModule().change_kcl_settings(...args) return getModule().change_kcl_settings(...args)
} }

View File

@ -42,6 +42,7 @@ import {
} from 'components/Toolbar/EqualLength' } from 'components/Toolbar/EqualLength'
import { revolveSketch } from 'lang/modifyAst/addRevolve' import { revolveSketch } from 'lang/modifyAst/addRevolve'
import { import {
addHelix,
addOffsetPlane, addOffsetPlane,
addSweep, addSweep,
deleteFromSelection, deleteFromSelection,
@ -276,6 +277,7 @@ export type ModelingMachineEvent =
| { type: 'Fillet'; data?: ModelingCommandSchema['Fillet'] } | { type: 'Fillet'; data?: ModelingCommandSchema['Fillet'] }
| { type: 'Chamfer'; data?: ModelingCommandSchema['Chamfer'] } | { type: 'Chamfer'; data?: ModelingCommandSchema['Chamfer'] }
| { type: 'Offset plane'; data: ModelingCommandSchema['Offset plane'] } | { type: 'Offset plane'; data: ModelingCommandSchema['Offset plane'] }
| { type: 'Helix'; data: ModelingCommandSchema['Helix'] }
| { type: 'Text-to-CAD'; data: ModelingCommandSchema['Text-to-CAD'] } | { type: 'Text-to-CAD'; data: ModelingCommandSchema['Text-to-CAD'] }
| { type: 'Prompt-to-edit'; data: ModelingCommandSchema['Prompt-to-edit'] } | { type: 'Prompt-to-edit'; data: ModelingCommandSchema['Prompt-to-edit'] }
| { | {
@ -1615,6 +1617,73 @@ export const modelingMachine = setup({
} }
} }
), ),
helixAstMod: fromPromise(
async ({
input,
}: {
input: ModelingCommandSchema['Helix'] | undefined
}) => {
if (!input) return new Error('No input provided')
// Extract inputs
const ast = kclManager.ast
const {
revolutions,
angleStart,
counterClockWise,
radius,
axis,
length,
} = input
for (const variable of [revolutions, angleStart, radius, length]) {
// Insert the variable if it exists
if (
'variableName' in variable &&
variable.variableName &&
variable.insertIndex !== undefined
) {
const newBody = [...ast.body]
newBody.splice(
variable.insertIndex,
0,
variable.variableDeclarationAst
)
ast.body = newBody
}
}
const valueOrVariable = (variable: KclCommandValue) =>
'variableName' in variable
? variable.variableIdentifierAst
: variable.valueAst
const result = addHelix({
node: ast,
revolutions: valueOrVariable(revolutions),
angleStart: valueOrVariable(angleStart),
counterClockWise,
radius: valueOrVariable(radius),
axis,
length: valueOrVariable(length),
})
const updateAstResult = await kclManager.updateAst(
result.modifiedAst,
true,
{
focusPath: [result.pathToNode],
}
)
await codeManager.updateEditorWithAstAndWriteToFile(
updateAstResult.newAst
)
if (updateAstResult?.selections) {
editorManager.selectRange(updateAstResult?.selections)
}
}
),
sweepAstMod: fromPromise( sweepAstMod: fromPromise(
async ({ async ({
input, input,
@ -1974,6 +2043,11 @@ export const modelingMachine = setup({
reenter: true, reenter: true,
}, },
Helix: {
target: 'Applying helix',
reenter: true,
},
'Prompt-to-edit': 'Applying Prompt-to-edit', 'Prompt-to-edit': 'Applying Prompt-to-edit',
}, },
@ -2734,6 +2808,19 @@ export const modelingMachine = setup({
}, },
}, },
'Applying helix': {
invoke: {
src: 'helixAstMod',
id: 'helixAstMod',
input: ({ event }) => {
if (event.type !== 'Helix') return undefined
return event.data
},
onDone: ['idle'],
onError: ['idle'],
},
},
'Applying sweep': { 'Applying sweep': {
invoke: { invoke: {
src: 'sweepAstMod', src: 'sweepAstMod',

View File

@ -306,7 +306,6 @@ export const projectsMachine = setup({
return { return {
code: '', code: '',
name: '', name: '',
units: 'mm',
method: 'existingProject', method: 'existingProject',
projects: context.projects, projects: context.projects,
} }
@ -314,7 +313,6 @@ export const projectsMachine = setup({
return { return {
code: event.data.code || '', code: event.data.code || '',
name: event.data.name, name: event.data.name,
units: event.data.units,
method: event.data.method, method: event.data.method,
projectName: event.data.projectName, projectName: event.data.projectName,
projects: context.projects, projects: context.projects,

View File

@ -329,6 +329,7 @@ ipcMain.handle('kittycad', (event, data) => {
)(data.args) )(data.args)
}) })
// Used to find other devices on the local network, e.g. 3D printers, CNC machines, etc.
ipcMain.handle('find_machine_api', () => { ipcMain.handle('find_machine_api', () => {
const timeoutAfterMs = 5000 const timeoutAfterMs = 5000
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -339,7 +340,6 @@ ipcMain.handle('find_machine_api', () => {
console.error(error) console.error(error)
resolve(null) resolve(null)
}) })
console.log('Looking for machine API...')
bonjourEt.find( bonjourEt.find(
{ protocol: 'tcp', type: 'machine-api' }, { protocol: 'tcp', type: 'machine-api' },
(service: Service) => { (service: Service) => {

View File

@ -26,7 +26,7 @@ export default function Units() {
<div className="fixed inset-0 z-50 grid items-end justify-start px-4 pointer-events-none"> <div className="fixed inset-0 z-50 grid items-end justify-start px-4 pointer-events-none">
<div <div
className={ className={
'pointer-events-auto max-w-2xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded' 'relative pointer-events-auto max-w-2xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
} }
> >
<SettingsSection <SettingsSection
@ -72,9 +72,7 @@ export default function Units() {
</SettingsSection> </SettingsSection>
<OnboardingButtons <OnboardingButtons
currentSlug={onboardingPaths.CAMERA} currentSlug={onboardingPaths.CAMERA}
dismiss={dismiss} dismissClassName="right-auto left-full"
next={next}
nextText="Next: Streaming"
/> />
</div> </div>
</div> </div>

View File

@ -1,19 +1,17 @@
import usePlatform from 'hooks/usePlatform' import usePlatform from 'hooks/usePlatform'
import { OnboardingButtons, kbdClasses, useDismiss, useNextClick } from '.' import { OnboardingButtons, kbdClasses } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths' import { onboardingPaths } from 'routes/Onboarding/paths'
import { hotkeyDisplay } from 'lib/hotkeyWrapper' import { hotkeyDisplay } from 'lib/hotkeyWrapper'
import { COMMAND_PALETTE_HOTKEY } from 'components/CommandBar/CommandBar' import { COMMAND_PALETTE_HOTKEY } from 'components/CommandBar/CommandBar'
export default function CmdK() { export default function CmdK() {
const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.USER_MENU)
const platformName = usePlatform() const platformName = usePlatform()
return ( return (
<div className="fixed inset-0 z-50 grid items-end justify-center pointer-events-none"> <div className="fixed inset-0 z-50 grid items-end justify-center pointer-events-none">
<div <div
className={ className={
'pointer-events-auto max-w-full xl:max-w-4xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded' 'relative pointer-events-auto max-w-full xl:max-w-4xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
} }
> >
<h2 className="text-2xl font-bold">Command Bar</h2> <h2 className="text-2xl font-bold">Command Bar</h2>
@ -38,12 +36,7 @@ export default function CmdK() {
. You can control settings, authentication, and file management from . You can control settings, authentication, and file management from
the command bar, as well as a growing number of modeling commands. the command bar, as well as a growing number of modeling commands.
</p> </p>
<OnboardingButtons <OnboardingButtons currentSlug={onboardingPaths.COMMAND_K} />
currentSlug={onboardingPaths.COMMAND_K}
dismiss={dismiss}
next={next}
nextText="Next: User Menu"
/>
</div> </div>
</div> </div>
) )

View File

@ -1,22 +1,14 @@
import { import { kbdClasses, OnboardingButtons, useDemoCode } from '.'
kbdClasses,
OnboardingButtons,
useDemoCode,
useDismiss,
useNextClick,
} from '.'
import { onboardingPaths } from 'routes/Onboarding/paths' import { onboardingPaths } from 'routes/Onboarding/paths'
export default function OnboardingCodeEditor() { export default function OnboardingCodeEditor() {
useDemoCode() useDemoCode()
const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.PARAMETRIC_MODELING)
return ( return (
<div className="fixed grid justify-end items-center inset-0 z-50 pointer-events-none"> <div className="fixed grid justify-end items-center inset-0 z-50 pointer-events-none">
<div <div
className={ className={
'pointer-events-auto z-10 max-w-xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg h-[75vh] flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded' 'relative pointer-events-auto z-10 max-w-xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg h-[75vh] flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
} }
> >
<section className="flex-1 overflow-y-auto"> <section className="flex-1 overflow-y-auto">
@ -73,12 +65,7 @@ export default function OnboardingCodeEditor() {
pressing <kbd className={kbdClasses}>Shift + C</kbd>. pressing <kbd className={kbdClasses}>Shift + C</kbd>.
</p> </p>
</section> </section>
<OnboardingButtons <OnboardingButtons currentSlug={onboardingPaths.EDITOR} />
currentSlug={onboardingPaths.EDITOR}
dismiss={dismiss}
next={next}
nextText="Next: Parametric Modeling"
/>
</div> </div>
</div> </div>
) )

View File

@ -1,16 +1,13 @@
import { APP_NAME } from 'lib/constants' import { APP_NAME } from 'lib/constants'
import { OnboardingButtons, useDismiss, useNextClick } from '.' import { OnboardingButtons } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths' import { onboardingPaths } from 'routes/Onboarding/paths'
export default function Export() { export default function Export() {
const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.SKETCHING)
return ( return (
<div className="fixed grid justify-center items-end inset-0 z-50 pointer-events-none"> <div className="fixed grid justify-center items-end inset-0 z-50 pointer-events-none">
<div <div
className={ className={
'pointer-events-auto max-w-full xl:max-w-2xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded' 'relative pointer-events-auto max-w-full xl:max-w-2xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
} }
> >
<section className="flex-1"> <section className="flex-1">
@ -52,12 +49,7 @@ export default function Export() {
! !
</p> </p>
</section> </section>
<OnboardingButtons <OnboardingButtons currentSlug={onboardingPaths.EXPORT} />
currentSlug={onboardingPaths.EXPORT}
next={next}
dismiss={dismiss}
nextText="Next: Sketching"
/>
</div> </div>
</div> </div>
) )

View File

@ -1,4 +1,4 @@
import { OnboardingButtons, useDemoCode, useDismiss } from '.' import { OnboardingButtons, useDemoCode } from '.'
import { useEffect } from 'react' import { useEffect } from 'react'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import { APP_NAME } from 'lib/constants' import { APP_NAME } from 'lib/constants'
@ -7,7 +7,6 @@ import { sceneInfra } from 'lib/singletons'
export default function FutureWork() { export default function FutureWork() {
const { send } = useModelingContext() const { send } = useModelingContext()
const dismiss = useDismiss()
// Reset the code, the camera, and the modeling state // Reset the code, the camera, and the modeling state
useDemoCode() useDemoCode()
@ -19,7 +18,7 @@ export default function FutureWork() {
return ( return (
<div className="fixed grid justify-center items-center inset-0 bg-chalkboard-100/50 z-50"> <div className="fixed grid justify-center items-center inset-0 bg-chalkboard-100/50 z-50">
<div className="max-w-full xl:max-w-2xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded"> <div className="relative max-w-full xl:max-w-2xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded">
<h1 className="text-2xl font-bold">Future Work</h1> <h1 className="text-2xl font-bold">Future Work</h1>
<p className="my-4"> <p className="my-4">
We have curves, cuts, multi-profile sketch mode, and many more CAD We have curves, cuts, multi-profile sketch mode, and many more CAD
@ -59,9 +58,6 @@ export default function FutureWork() {
<OnboardingButtons <OnboardingButtons
currentSlug={onboardingPaths.FUTURE_WORK} currentSlug={onboardingPaths.FUTURE_WORK}
className="mt-6" className="mt-6"
dismiss={dismiss}
next={dismiss}
nextText="Finish"
/> />
</div> </div>
</div> </div>

View File

@ -1,23 +1,15 @@
import { import { OnboardingButtons, kbdClasses, useDemoCode } from '.'
OnboardingButtons,
kbdClasses,
useDemoCode,
useDismiss,
useNextClick,
} from '.'
import { onboardingPaths } from 'routes/Onboarding/paths' import { onboardingPaths } from 'routes/Onboarding/paths'
import { bracketWidthConstantLine } from 'lib/exampleKcl' import { bracketWidthConstantLine } from 'lib/exampleKcl'
export default function OnboardingInteractiveNumbers() { export default function OnboardingInteractiveNumbers() {
useDemoCode() useDemoCode()
const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.COMMAND_K)
return ( return (
<div className="fixed grid justify-end items-center inset-0 z-50 pointer-events-none"> <div className="fixed grid justify-end items-center inset-0 z-50 pointer-events-none">
<div <div
className={ className={
'pointer-events-auto z-10 max-w-xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg h-[75vh] flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded' 'relative pointer-events-auto z-10 max-w-xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg h-[75vh] flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
} }
> >
<section className="flex-1 overflow-y-auto mb-6"> <section className="flex-1 overflow-y-auto mb-6">
@ -88,12 +80,7 @@ export default function OnboardingInteractiveNumbers() {
your ideas for how to make it better. your ideas for how to make it better.
</p> </p>
</section> </section>
<OnboardingButtons <OnboardingButtons currentSlug={onboardingPaths.INTERACTIVE_NUMBERS} />
currentSlug={onboardingPaths.INTERACTIVE_NUMBERS}
dismiss={dismiss}
next={next}
nextText="Next: Command Bar"
/>
</div> </div>
</div> </div>
) )

View File

@ -1,4 +1,4 @@
import { OnboardingButtons, useDemoCode, useDismiss, useNextClick } from '.' import { OnboardingButtons, useDemoCode } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths' import { onboardingPaths } from 'routes/Onboarding/paths'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { Themes, getSystemTheme } from 'lib/theme' import { Themes, getSystemTheme } from 'lib/theme'
@ -8,13 +8,12 @@ import { isDesktop } from 'lib/isDesktop'
import { useNavigate, useRouteLoaderData } from 'react-router-dom' import { useNavigate, useRouteLoaderData } from 'react-router-dom'
import { codeManager, kclManager } from 'lib/singletons' import { codeManager, kclManager } from 'lib/singletons'
import { APP_NAME } from 'lib/constants' import { APP_NAME } from 'lib/constants'
import { useState } from 'react' import { useEffect, useState } from 'react'
import { IndexLoaderData } from 'lib/types' import { IndexLoaderData } from 'lib/types'
import { PATHS } from 'lib/paths' import { PATHS } from 'lib/paths'
import { useFileContext } from 'hooks/useFileContext' import { useFileContext } from 'hooks/useFileContext'
import { useLspContext } from 'components/LspProvider' import { useLspContext } from 'components/LspProvider'
import { reportRejection } from 'lib/trap' import { reportRejection } from 'lib/trap'
import { toSync } from 'lib/utils'
/** /**
* Show either a welcome screen or a warning screen * Show either a welcome screen or a warning screen
@ -39,7 +38,7 @@ interface OnboardingResetWarningProps {
function OnboardingResetWarning(props: OnboardingResetWarningProps) { function OnboardingResetWarning(props: OnboardingResetWarningProps) {
return ( return (
<div className="fixed inset-0 z-50 grid place-content-center bg-chalkboard-110/50"> <div className="fixed inset-0 z-50 grid place-content-center bg-chalkboard-110/50">
<div className="max-w-3xl p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90"> <div className="relative max-w-3xl p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
{!isDesktop() ? ( {!isDesktop() ? (
<OnboardingWarningWeb {...props} /> <OnboardingWarningWeb {...props} />
) : ( ) : (
@ -52,7 +51,6 @@ function OnboardingResetWarning(props: OnboardingResetWarningProps) {
function OnboardingWarningDesktop(props: OnboardingResetWarningProps) { function OnboardingWarningDesktop(props: OnboardingResetWarningProps) {
const navigate = useNavigate() const navigate = useNavigate()
const dismiss = useDismiss()
const loaderData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData const loaderData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const { context: fileContext } = useFileContext() const { context: fileContext } = useFileContext()
const { onProjectClose, onProjectOpen } = useLspContext() const { onProjectClose, onProjectOpen } = useLspContext()
@ -81,17 +79,28 @@ function OnboardingWarningDesktop(props: OnboardingResetWarningProps) {
</section> </section>
<OnboardingButtons <OnboardingButtons
className="mt-6" className="mt-6"
dismiss={dismiss} onNextOverride={() => {
next={toSync(onAccept, reportRejection)} onAccept().catch(reportRejection)
nextText="Make a new project" }}
/> />
</> </>
) )
} }
function OnboardingWarningWeb(props: OnboardingResetWarningProps) { function OnboardingWarningWeb(props: OnboardingResetWarningProps) {
const dismiss = useDismiss() useEffect(() => {
async function beforeNavigate() {
// We do want to update both the state and editor here.
codeManager.updateCodeStateEditor(bracket)
await codeManager.writeToFile()
await kclManager.executeCode(true)
props.setShouldShowWarning(false)
}
return () => {
beforeNavigate().catch(reportRejection)
}
}, [])
return ( return (
<> <>
<h1 className="text-3xl font-bold text-warn-80 dark:text-warn-10"> <h1 className="text-3xl font-bold text-warn-80 dark:text-warn-10">
@ -101,19 +110,7 @@ function OnboardingWarningWeb(props: OnboardingResetWarningProps) {
We see you have some of your own code written in this project. Please We see you have some of your own code written in this project. Please
save it somewhere else before continuing the onboarding. save it somewhere else before continuing the onboarding.
</p> </p>
<OnboardingButtons <OnboardingButtons className="mt-6" />
className="mt-6"
dismiss={dismiss}
next={toSync(async () => {
// We do want to update both the state and editor here.
codeManager.updateCodeStateEditor(bracket)
await codeManager.writeToFile()
await kclManager.executeCode(true)
props.setShouldShowWarning(false)
}, reportRejection)}
nextText="Overwrite code and continue"
/>
</> </>
) )
} }
@ -136,12 +133,10 @@ function OnboardingIntroductionInner() {
(theme.current === Themes.System && getSystemTheme() === Themes.Light) (theme.current === Themes.System && getSystemTheme() === Themes.Light)
? '-dark' ? '-dark'
: '' : ''
const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.CAMERA)
return ( return (
<div className="fixed inset-0 z-50 grid place-content-center bg-chalkboard-110/50"> <div className="fixed inset-0 z-50 grid place-content-center bg-chalkboard-110/50">
<div className="max-w-3xl p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90"> <div className="relative max-w-3xl p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
<h1 className="flex flex-wrap items-center gap-4 text-3xl font-bold"> <h1 className="flex flex-wrap items-center gap-4 text-3xl font-bold">
<img <img
src={`${isDesktop() ? '.' : ''}/zma-logomark${getLogoTheme()}.svg`} src={`${isDesktop() ? '.' : ''}/zma-logomark${getLogoTheme()}.svg`}
@ -192,9 +187,6 @@ function OnboardingIntroductionInner() {
<OnboardingButtons <OnboardingButtons
currentSlug={onboardingPaths.INDEX} currentSlug={onboardingPaths.INDEX}
className="mt-6" className="mt-6"
dismiss={dismiss}
next={next}
nextText="Mouse Controls"
/> />
</div> </div>
</div> </div>

View File

@ -1,4 +1,4 @@
import { OnboardingButtons, useDemoCode, useDismiss, useNextClick } from '.' import { OnboardingButtons, useDemoCode } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths' import { onboardingPaths } from 'routes/Onboarding/paths'
import { Themes, getSystemTheme } from 'lib/theme' import { Themes, getSystemTheme } from 'lib/theme'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
@ -21,14 +21,12 @@ export default function OnboardingParametricModeling() {
(theme === Themes.System && getSystemTheme() === Themes.Light) (theme === Themes.System && getSystemTheme() === Themes.Light)
? '-dark' ? '-dark'
: '' : ''
const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.INTERACTIVE_NUMBERS)
return ( return (
<div className="fixed grid justify-end items-center inset-0 z-50 pointer-events-none"> <div className="fixed grid justify-end items-center inset-0 z-50 pointer-events-none">
<div <div
className={ className={
'pointer-events-auto z-10 max-w-xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg h-[75vh] flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded' 'relative pointer-events-auto z-10 max-w-xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg h-[75vh] flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
} }
> >
<section className="flex-1 overflow-y-auto mb-6"> <section className="flex-1 overflow-y-auto mb-6">
@ -77,12 +75,7 @@ export default function OnboardingParametricModeling() {
</figcaption> </figcaption>
</figure> </figure>
</section> </section>
<OnboardingButtons <OnboardingButtons currentSlug={onboardingPaths.PARAMETRIC_MODELING} />
currentSlug={onboardingPaths.PARAMETRIC_MODELING}
dismiss={dismiss}
next={next}
nextText="Next: Interactive Numbers"
/>
</div> </div>
</div> </div>
) )

View File

@ -1,17 +1,15 @@
import { OnboardingButtons, useDismiss, useNextClick } from '.' import { OnboardingButtons } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths' import { onboardingPaths } from 'routes/Onboarding/paths'
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
export default function ProjectMenu() { export default function ProjectMenu() {
const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.EXPORT)
const onDesktop = isDesktop() const onDesktop = isDesktop()
return ( return (
<div className="fixed grid justify-center items-start inset-0 z-50 pointer-events-none"> <div className="fixed grid justify-center items-start inset-0 z-50 pointer-events-none">
<div <div
className={ className={
'pointer-events-auto max-w-xl flex flex-col border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded' 'relative pointer-events-auto max-w-xl flex flex-col border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
} }
> >
<section className="flex-1"> <section className="flex-1">
@ -57,12 +55,7 @@ export default function ProjectMenu() {
</> </>
)} )}
</section> </section>
<OnboardingButtons <OnboardingButtons currentSlug={onboardingPaths.PROJECT_MENU} />
currentSlug={onboardingPaths.PROJECT_MENU}
next={next}
dismiss={dismiss}
nextText="Next: Export"
/>
</div> </div>
</div> </div>
) )

View File

@ -1,12 +1,9 @@
import { OnboardingButtons, useDismiss, useNextClick } from '.' import { OnboardingButtons } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths' import { onboardingPaths } from 'routes/Onboarding/paths'
import { useEffect } from 'react' import { useEffect } from 'react'
import { codeManager, kclManager } from 'lib/singletons' import { codeManager, kclManager } from 'lib/singletons'
export default function Sketching() { export default function Sketching() {
const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.FUTURE_WORK)
useEffect(() => { useEffect(() => {
async function clearEditor() { async function clearEditor() {
// We do want to update both the state and editor here. // We do want to update both the state and editor here.
@ -22,7 +19,7 @@ export default function Sketching() {
<div className="fixed grid justify-center items-end inset-0 z-50 pointer-events-none"> <div className="fixed grid justify-center items-end inset-0 z-50 pointer-events-none">
<div <div
className={ className={
'pointer-events-auto max-w-full xl:max-w-2xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded' 'relative pointer-events-auto max-w-full xl:max-w-2xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
} }
> >
<h1 className="text-2xl font-bold">Sketching</h1> <h1 className="text-2xl font-bold">Sketching</h1>
@ -45,9 +42,6 @@ export default function Sketching() {
<OnboardingButtons <OnboardingButtons
currentSlug={onboardingPaths.SKETCHING} currentSlug={onboardingPaths.SKETCHING}
className="mt-6" className="mt-6"
next={next}
dismiss={dismiss}
nextText="Next: Future Work"
/> />
</div> </div>
</div> </div>

View File

@ -1,15 +1,12 @@
import { OnboardingButtons, useDismiss, useNextClick } from '.' import { OnboardingButtons } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths' import { onboardingPaths } from 'routes/Onboarding/paths'
export default function Streaming() { export default function Streaming() {
const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.EDITOR)
return ( return (
<div className="fixed grid justify-start items-center inset-0 z-50 pointer-events-none"> <div className="fixed grid justify-start items-center inset-0 z-50 pointer-events-none">
<div <div
className={ className={
'pointer-events-auto max-w-xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg h-[75vh] flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded' 'relative pointer-events-auto max-w-xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg h-[75vh] flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
} }
> >
<section className="flex-1 overflow-y-auto"> <section className="flex-1 overflow-y-auto">
@ -44,9 +41,7 @@ export default function Streaming() {
</section> </section>
<OnboardingButtons <OnboardingButtons
currentSlug={onboardingPaths.STREAMING} currentSlug={onboardingPaths.STREAMING}
dismiss={dismiss} dismissClassName="right-auto left-full"
next={next}
nextText="Next: Code Editor"
/> />
</div> </div>
</div> </div>

View File

@ -1,12 +1,10 @@
import { OnboardingButtons, useDismiss, useNextClick } from '.' import { OnboardingButtons } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths' import { onboardingPaths } from 'routes/Onboarding/paths'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useUser } from 'machines/appMachine' import { useUser } from 'machines/appMachine'
export default function UserMenu() { export default function UserMenu() {
const user = useUser() const user = useUser()
const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.PROJECT_MENU)
const [avatarErrored, setAvatarErrored] = useState(false) const [avatarErrored, setAvatarErrored] = useState(false)
const errorOrNoImage = !user?.image || avatarErrored const errorOrNoImage = !user?.image || avatarErrored
@ -32,7 +30,7 @@ export default function UserMenu() {
<div className="fixed grid justify-center items-start inset-0 z-50 pointer-events-none"> <div className="fixed grid justify-center items-start inset-0 z-50 pointer-events-none">
<div <div
className={ className={
'pointer-events-auto max-w-xl flex flex-col border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded' 'relative pointer-events-auto max-w-xl flex flex-col border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
} }
> >
<section className="flex-1"> <section className="flex-1">
@ -48,12 +46,7 @@ export default function UserMenu() {
only apply to the current project. only apply to the current project.
</p> </p>
</section> </section>
<OnboardingButtons <OnboardingButtons currentSlug={onboardingPaths.USER_MENU} />
currentSlug={onboardingPaths.USER_MENU}
dismiss={dismiss}
next={next}
nextText="Next: Project Menu"
/>
</div> </div>
</div> </div>
) )

View File

@ -26,6 +26,9 @@ import { reportRejection } from 'lib/trap'
import { useNetworkContext } from 'hooks/useNetworkContext' import { useNetworkContext } from 'hooks/useNetworkContext'
import { NetworkHealthState } from 'hooks/useNetworkStatus' import { NetworkHealthState } from 'hooks/useNetworkStatus'
import { EngineConnectionStateType } from 'lang/std/engineConnection' import { EngineConnectionStateType } from 'lang/std/engineConnection'
import { CustomIcon } from 'components/CustomIcon'
import Tooltip from 'components/Tooltip'
import { commandBarActor } from 'machines/commandBarMachine'
export const kbdClasses = export const kbdClasses =
'py-0.5 px-1 text-sm rounded bg-chalkboard-10 dark:bg-chalkboard-100 border border-chalkboard-50 border-b-2' 'py-0.5 px-1 text-sm rounded bg-chalkboard-10 dark:bg-chalkboard-100 border border-chalkboard-50 border-b-2'
@ -163,58 +166,99 @@ export function useStepNumber(
: onboardingRoutes.findIndex( : onboardingRoutes.findIndex(
(r) => r.path === makeUrlPathRelative(slug) (r) => r.path === makeUrlPathRelative(slug)
) + 1 ) + 1
: undefined : 1
} }
export function OnboardingButtons({ export function OnboardingButtons({
next,
nextText,
dismiss,
currentSlug, currentSlug,
className, className,
dismissClassName,
onNextOverride,
...props ...props
}: { }: {
next: () => void
nextText?: string
dismiss: () => void
currentSlug?: (typeof onboardingPaths)[keyof typeof onboardingPaths] currentSlug?: (typeof onboardingPaths)[keyof typeof onboardingPaths]
className?: string className?: string
dismissClassName?: string
onNextOverride?: () => void
} & React.HTMLAttributes<HTMLDivElement>) { } & React.HTMLAttributes<HTMLDivElement>) {
const dismiss = useDismiss()
const stepNumber = useStepNumber(currentSlug) const stepNumber = useStepNumber(currentSlug)
const previousStep =
!stepNumber || stepNumber === 0 ? null : onboardingRoutes[stepNumber - 2]
const goToPrevious = useNextClick(
onboardingPaths.INDEX + (previousStep?.path ?? '')
)
const nextStep =
!stepNumber || stepNumber === onboardingRoutes.length
? null
: onboardingRoutes[stepNumber]
const goToNext = useNextClick(onboardingPaths.INDEX + (nextStep?.path ?? ''))
return ( return (
<div <>
className={'flex items-center justify-between ' + (className ?? '')} <button
{...props}
>
<ActionButton
Element="button"
onClick={dismiss} onClick={dismiss}
iconStart={{ className={
icon: 'close', 'group block !absolute left-auto right-full top-[-3px] m-2.5 p-0 border-none bg-transparent hover:bg-transparent ' +
className: 'text-chalkboard-10', dismissClassName
bgClassName: 'bg-destroy-80 group-hover:bg-destroy-80', }
}} data-testid="onboarding-dismiss"
className="hover:border-destroy-40 hover:bg-destroy-10/50 dark:hover:bg-destroy-80/50"
> >
Dismiss <CustomIcon
</ActionButton> name="close"
{stepNumber !== undefined && ( className="w-5 h-5 rounded-sm bg-destroy-10 text-destroy-80 dark:bg-destroy-80 dark:text-destroy-10 group-hover:brightness-110"
<p className="font-mono text-xs text-center m-0"> />
{stepNumber} / {onboardingRoutes.length} <Tooltip position="bottom" delay={500}>
</p> Dismiss <kbd className="hotkey ml-4 dark:!bg-chalkboard-80">esc</kbd>
)} </Tooltip>
<ActionButton </button>
autoFocus <div
Element="button" className={'flex items-center justify-between ' + (className ?? '')}
onClick={next} {...props}
iconStart={{ icon: 'arrowRight', bgClassName: 'dark:bg-chalkboard-80' }}
className="dark:hover:bg-chalkboard-80/50"
data-testid="onboarding-next"
> >
{nextText ?? 'Next'} <ActionButton
</ActionButton> Element="button"
</div> onClick={() =>
previousStep?.path || previousStep?.index
? goToPrevious()
: dismiss()
}
iconStart={{
icon: previousStep ? 'arrowLeft' : 'close',
className: 'text-chalkboard-10',
bgClassName: 'bg-destroy-80 group-hover:bg-destroy-80',
}}
className="hover:border-destroy-40 hover:bg-destroy-10/50 dark:hover:bg-destroy-80/50"
data-testid="onboarding-prev"
>
{previousStep ? `Back` : 'Dismiss'}
</ActionButton>
{stepNumber !== undefined && (
<p className="font-mono text-xs text-center m-0">
{stepNumber} / {onboardingRoutes.length}
</p>
)}
<ActionButton
autoFocus
Element="button"
onClick={() => {
if (nextStep?.path) {
onNextOverride ? onNextOverride() : goToNext()
} else {
dismiss()
}
}}
iconStart={{
icon: nextStep ? 'arrowRight' : 'checkmark',
bgClassName: 'dark:bg-chalkboard-80',
}}
className="dark:hover:bg-chalkboard-80/50"
data-testid="onboarding-next"
>
{nextStep ? `Next` : 'Finish'}
</ActionButton>
</div>
</>
) )
} }

View File

@ -32,7 +32,8 @@ export const PACKAGE_NAME = isDesktop()
export const IS_NIGHTLY = PACKAGE_NAME.indexOf('-nightly') > -1 export const IS_NIGHTLY = PACKAGE_NAME.indexOf('-nightly') > -1
export const IS_NIGHTLY_OR_DEBUG = IS_NIGHTLY || APP_VERSION === '0.0.0' export const IS_NIGHTLY_OR_DEBUG =
IS_NIGHTLY || APP_VERSION === '0.0.0' || APP_VERSION === '11.22.33'
export function getReleaseUrl(version: string = APP_VERSION) { export function getReleaseUrl(version: string = APP_VERSION) {
return `https://github.com/KittyCAD/modeling-app/releases/tag/${ return `https://github.com/KittyCAD/modeling-app/releases/tag/${

View File

@ -2014,9 +2014,9 @@ dependencies = [
[[package]] [[package]]
name = "miette" name = "miette"
version = "7.4.0" version = "7.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "317f146e2eb7021892722af37cf1b971f0a70c8406f487e24952667616192c64" checksum = "1a955165f87b37fd1862df2a59547ac542c77ef6d17c666f619d1ad22dd89484"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"backtrace-ext", "backtrace-ext",
@ -2034,9 +2034,9 @@ dependencies = [
[[package]] [[package]]
name = "miette-derive" name = "miette-derive"
version = "7.4.0" version = "7.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23c9b935fbe1d6cbd1dac857b54a688145e2d93f48db36010514d0f612d0ad67" checksum = "bf45bf44ab49be92fd1227a3be6fc6f617f1a337c06af54981048574d8783147"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View File

@ -37,7 +37,7 @@ kittycad = { workspace = true }
kittycad-modeling-cmds = { workspace = true } kittycad-modeling-cmds = { workspace = true }
lazy_static = "1.5.0" lazy_static = "1.5.0"
measurements = "0.11.0" measurements = "0.11.0"
miette = "7.2.0" miette = "7.5.0"
mime_guess = "2.0.5" mime_guess = "2.0.5"
parse-display = "0.9.1" parse-display = "0.9.1"
pyo3 = { version = "0.22.6", optional = true } pyo3 = { version = "0.22.6", optional = true }
@ -116,7 +116,7 @@ expectorate = "1.1.0"
handlebars = "6.3.0" handlebars = "6.3.0"
image = { version = "0.25.5", default-features = false, features = ["png"] } image = { version = "0.25.5", default-features = false, features = ["png"] }
insta = { version = "1.41.1", features = ["json", "filters", "redactions"] } insta = { version = "1.41.1", features = ["json", "filters", "redactions"] }
miette = { version = "7.2.0", features = ["fancy"] } miette = { version = "7.5.0", features = ["fancy"] }
pretty_assertions = "1.4.1" pretty_assertions = "1.4.1"
tokio = { version = "1.41.1", features = ["rt-multi-thread", "macros", "time"] } tokio = { version = "1.41.1", features = ["rt-multi-thread", "macros", "time"] }
twenty-twenty = "0.8.0" twenty-twenty = "0.8.0"

View File

@ -118,7 +118,7 @@ impl StdLibFnArg {
} else if self.type_ == "KclValue" && self.required { } else if self.type_ == "KclValue" && self.required {
return Ok(Some((index, format!("{label}${{{}:{}}}", index, "3")))); return Ok(Some((index, format!("{label}${{{}:{}}}", index, "3"))));
} }
self.get_autocomplete_snippet_from_schema(&self.schema.schema.clone().into(), index, in_keyword_fn) self.get_autocomplete_snippet_from_schema(&self.schema.schema.clone().into(), index, in_keyword_fn, &self.name)
.map(|maybe| maybe.map(|(index, snippet)| (index, format!("{label}{snippet}")))) .map(|maybe| maybe.map(|(index, snippet)| (index, format!("{label}{snippet}"))))
} }
@ -136,6 +136,7 @@ impl StdLibFnArg {
schema: &schemars::schema::Schema, schema: &schemars::schema::Schema,
index: usize, index: usize,
in_keyword_fn: bool, in_keyword_fn: bool,
name: &str,
) -> Result<Option<(usize, String)>> { ) -> Result<Option<(usize, String)>> {
match schema { match schema {
schemars::schema::Schema::Object(o) => { schemars::schema::Schema::Object(o) => {
@ -149,6 +150,10 @@ impl StdLibFnArg {
return Ok(Some((index, format!("${{{}:sketch{}}}", index, "000")))); return Ok(Some((index, format!("${{{}:sketch{}}}", index, "000"))));
} }
if name == "color" {
let snippet = format!("${{{}:\"#ff0000\"}}", index);
return Ok(Some((index, snippet)));
}
if let Some(serde_json::Value::Bool(nullable)) = o.extensions.get("nullable") { if let Some(serde_json::Value::Bool(nullable)) = o.extensions.get("nullable") {
if (!in_keyword_fn && *nullable) || (in_keyword_fn && !self.include_in_snippet) { if (!in_keyword_fn && *nullable) || (in_keyword_fn && !self.include_in_snippet) {
return Ok(None); return Ok(None);
@ -192,13 +197,9 @@ impl StdLibFnArg {
continue; continue;
} }
if prop_name == "color" { if let Some((new_index, snippet)) =
fn_docs.push_str(&format!("\t{} = ${{{}:\"#ff0000\"}},\n", prop_name, i)); self.get_autocomplete_snippet_from_schema(prop, i, false, name)?
i += 1; {
continue;
}
if let Some((new_index, snippet)) = self.get_autocomplete_snippet_from_schema(prop, i, false)? {
fn_docs.push_str(&format!("\t{} = {},\n", prop_name, snippet)); fn_docs.push_str(&format!("\t{} = {},\n", prop_name, snippet));
i = new_index + 1; i = new_index + 1;
} }
@ -223,7 +224,8 @@ impl StdLibFnArg {
.get_autocomplete_snippet_from_schema( .get_autocomplete_snippet_from_schema(
items, items,
index + (v as usize), index + (v as usize),
in_keyword_fn in_keyword_fn,
name
) )
.unwrap() .unwrap()
.unwrap() .unwrap()
@ -238,7 +240,7 @@ impl StdLibFnArg {
index, index,
format!( format!(
"[{}]", "[{}]",
self.get_autocomplete_snippet_from_schema(items, index, in_keyword_fn)? self.get_autocomplete_snippet_from_schema(items, index, in_keyword_fn, name)?
.ok_or_else(|| anyhow::anyhow!("expected snippet"))? .ok_or_else(|| anyhow::anyhow!("expected snippet"))?
.1 .1
), ),
@ -250,7 +252,7 @@ impl StdLibFnArg {
index, index,
format!( format!(
"[{}]", "[{}]",
self.get_autocomplete_snippet_from_schema(items, index, in_keyword_fn)? self.get_autocomplete_snippet_from_schema(items, index, in_keyword_fn, name)?
.ok_or_else(|| anyhow::anyhow!("expected snippet"))? .ok_or_else(|| anyhow::anyhow!("expected snippet"))?
.1 .1
), ),
@ -293,7 +295,7 @@ impl StdLibFnArg {
return Ok(Some((index, parsed_enum_values[0].to_string()))); return Ok(Some((index, parsed_enum_values[0].to_string())));
} else if let Some(item) = items.iter().next() { } else if let Some(item) = items.iter().next() {
if let Some((new_index, snippet)) = if let Some((new_index, snippet)) =
self.get_autocomplete_snippet_from_schema(item, index, in_keyword_fn)? self.get_autocomplete_snippet_from_schema(item, index, in_keyword_fn, name)?
{ {
i = new_index + 1; i = new_index + 1;
fn_docs.push_str(&snippet); fn_docs.push_str(&snippet);
@ -302,7 +304,7 @@ impl StdLibFnArg {
} else if let Some(items) = &subschemas.any_of { } else if let Some(items) = &subschemas.any_of {
if let Some(item) = items.iter().next() { if let Some(item) = items.iter().next() {
if let Some((new_index, snippet)) = if let Some((new_index, snippet)) =
self.get_autocomplete_snippet_from_schema(item, index, in_keyword_fn)? self.get_autocomplete_snippet_from_schema(item, index, in_keyword_fn, name)?
{ {
i = new_index + 1; i = new_index + 1;
fn_docs.push_str(&snippet); fn_docs.push_str(&snippet);
@ -1018,12 +1020,7 @@ mod tests {
let snippet = appearance_fn.to_autocomplete_snippet().unwrap(); let snippet = appearance_fn.to_autocomplete_snippet().unwrap();
assert_eq!( assert_eq!(
snippet, snippet,
r#"appearance({ r#"appearance(${0:%}, color = ${1:"#.to_owned() + "\"#" + r#"ff0000"})${}"#
color = ${0:"#
.to_owned()
+ "\"#"
+ r#"ff0000"},
}, ${1:%})${}"#
); );
} }
@ -1038,12 +1035,7 @@ mod tests {
fn get_autocomplete_snippet_sweep() { fn get_autocomplete_snippet_sweep() {
let sweep_fn: Box<dyn StdLibFn> = Box::new(crate::std::sweep::Sweep); let sweep_fn: Box<dyn StdLibFn> = Box::new(crate::std::sweep::Sweep);
let snippet = sweep_fn.to_autocomplete_snippet().unwrap(); let snippet = sweep_fn.to_autocomplete_snippet().unwrap();
assert_eq!( assert_eq!(snippet, r#"sweep(${0:%}, path = ${1:sketch000})${}"#);
snippet,
r#"sweep({
path = ${0:sketch000},
}, ${1:%})${}"#
);
} }
#[test] #[test]

View File

@ -242,7 +242,7 @@ firstSketch = startSketchOn('XY')
|> extrude(length = 6) |> extrude(length = 6)
// Remove the end face for the extrusion. // Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#; shell(firstSketch, faces = ['end'], thickness = 0.25)"#;
let (program, ctx, _) = parse_execute(new).await.unwrap(); let (program, ctx, _) = parse_execute(new).await.unwrap();
@ -273,7 +273,7 @@ firstSketch = startSketchOn('XY')
|> extrude(length = 6) |> extrude(length = 6)
// Remove the end face for the extrusion. // Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#; shell(firstSketch, faces = ['end'], thickness = 0.25) "#;
let new = r#"// Remove the end face for the extrusion. let new = r#"// Remove the end face for the extrusion.
firstSketch = startSketchOn('XY') firstSketch = startSketchOn('XY')
@ -285,7 +285,7 @@ firstSketch = startSketchOn('XY')
|> extrude(length = 6) |> extrude(length = 6)
// Remove the end face for the extrusion. // Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#; shell(firstSketch, faces = ['end'], thickness = 0.25)"#;
let (program_old, ctx, _) = parse_execute(old).await.unwrap(); let (program_old, ctx, _) = parse_execute(old).await.unwrap();
@ -318,7 +318,7 @@ firstSketch = startSketchOn('XY')
|> extrude(length = 6) |> extrude(length = 6)
// Remove the end face for the extrusion. // Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#; shell(firstSketch, faces = ['end'], thickness = 0.25) "#;
let new = r#"// Remove the end face for the extrusion. let new = r#"// Remove the end face for the extrusion.
firstSketch = startSketchOn('XY') firstSketch = startSketchOn('XY')
@ -330,7 +330,7 @@ firstSketch = startSketchOn('XY')
|> extrude(length = 6) |> extrude(length = 6)
// Remove the end face for the extrusion. // Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#; shell(firstSketch, faces = ['end'], thickness = 0.25)"#;
let (program, ctx, _) = parse_execute(old).await.unwrap(); let (program, ctx, _) = parse_execute(old).await.unwrap();
@ -363,7 +363,7 @@ firstSketch = startSketchOn('XY')
|> extrude(length = 6) |> extrude(length = 6)
// Remove the end face for the extrusion. // Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#; shell(firstSketch, faces = ['end'], thickness = 0.25) "#;
let new = r#"// Remove the end face for the extrusion. let new = r#"// Remove the end face for the extrusion.
firstSketch = startSketchOn('XY') firstSketch = startSketchOn('XY')
@ -375,7 +375,7 @@ firstSketch = startSketchOn('XY')
|> extrude(length = 6) |> extrude(length = 6)
// Remove the end face for the extrusion. // Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#; shell(firstSketch, faces = ['end'], thickness = 0.25)"#;
let (program, ctx, _) = parse_execute(old).await.unwrap(); let (program, ctx, _) = parse_execute(old).await.unwrap();
@ -409,7 +409,7 @@ firstSketch = startSketchOn('XY')
|> extrude(length = 6) |> extrude(length = 6)
// Remove the end face for the extrusion. // Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#; shell(firstSketch, faces = ['end'], thickness = 0.25)"#;
let (program, mut ctx, _) = parse_execute(new).await.unwrap(); let (program, mut ctx, _) = parse_execute(new).await.unwrap();
@ -451,7 +451,7 @@ firstSketch = startSketchOn('XY')
|> extrude(length = 6) |> extrude(length = 6)
// Remove the end face for the extrusion. // Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#; shell(firstSketch, faces = ['end'], thickness = 0.25)"#;
let (program, mut ctx, _) = parse_execute(new).await.unwrap(); let (program, mut ctx, _) = parse_execute(new).await.unwrap();
@ -486,7 +486,7 @@ firstSketch = startSketchOn('XY')
|> extrude(length = 6) |> extrude(length = 6)
// Remove the end face for the extrusion. // Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#; shell(firstSketch, faces = ['end'], thickness = 0.25)"#;
let (program, mut ctx, _) = parse_execute(new).await.unwrap(); let (program, mut ctx, _) = parse_execute(new).await.unwrap();

View File

@ -32,6 +32,16 @@ impl Geometry {
Geometry::Solid(e) => e.id, Geometry::Solid(e) => e.id,
} }
} }
/// If this geometry is the result of a pattern, then return the ID of
/// the original sketch which was patterned.
/// Equivalent to the `id()` method if this isn't a pattern.
pub fn original_id(&self) -> uuid::Uuid {
match self {
Geometry::Sketch(s) => s.original_id,
Geometry::Solid(e) => e.sketch.original_id,
}
}
} }
/// A set of geometry. /// A set of geometry.
@ -419,6 +429,8 @@ pub struct Sketch {
/// The original id of the sketch. This stays the same even if the sketch is /// The original id of the sketch. This stays the same even if the sketch is
/// is sketched on face etc. /// is sketched on face etc.
pub artifact_id: ArtifactId, pub artifact_id: ArtifactId,
#[ts(skip)]
pub original_id: uuid::Uuid,
pub units: UnitLen, pub units: UnitLen,
/// Metadata. /// Metadata.
#[serde(rename = "__meta")] #[serde(rename = "__meta")]

View File

@ -1590,10 +1590,10 @@ let w = f() + f()
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
|> extrude(length = 40.14) |> extrude(length = 40.14)
|> shell({ |> shell(
faces: [seg01], thickness = 3.14,
thickness: 3.14, faces = [seg01]
}, %) )
"#; "#;
let ctx = crate::test_server::new_context(UnitLength::Mm, true, None) let ctx = crate::test_server::new_context(UnitLength::Mm, true, None)
@ -1620,10 +1620,10 @@ let w = f() + f()
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
|> extrude(length = 40.14) |> extrude(length = 40.14)
|> shell({ |> shell(
faces: [seg01], faces = [seg01],
thickness: 3.14, thickness = 3.14,
}, %) )
"#; "#;
// Execute a slightly different program again. // Execute a slightly different program again.

View File

@ -159,8 +159,8 @@ impl Program {
} }
/// Get the meta settings for the kcl file from the annotations. /// Get the meta settings for the kcl file from the annotations.
pub fn get_meta_settings(&self) -> Result<Option<crate::MetaSettings>, KclError> { pub fn meta_settings(&self) -> Result<Option<crate::MetaSettings>, KclError> {
self.ast.get_meta_settings() self.ast.meta_settings()
} }
/// Change the meta settings for the kcl file. /// Change the meta settings for the kcl file.

View File

@ -269,7 +269,7 @@ impl Node<Program> {
} }
/// Get the annotations for the meta settings from the kcl file. /// Get the annotations for the meta settings from the kcl file.
pub fn get_meta_settings(&self) -> Result<Option<crate::execution::MetaSettings>, KclError> { pub fn meta_settings(&self) -> Result<Option<crate::execution::MetaSettings>, KclError> {
for annotation_node in self.annotations() { for annotation_node in self.annotations() {
let annotation = &annotation_node.value; let annotation = &annotation_node.value;
if annotation.annotation_name() == Some(annotations::SETTINGS) { if annotation.annotation_name() == Some(annotations::SETTINGS) {
@ -3881,7 +3881,7 @@ const cylinder = startSketchOn('-XZ')
startSketchOn('XY')"#; startSketchOn('XY')"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap(); let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let result = program.get_meta_settings().unwrap(); let result = program.meta_settings().unwrap();
assert!(result.is_some()); assert!(result.is_some());
let meta_settings = result.unwrap(); let meta_settings = result.unwrap();
@ -3897,7 +3897,7 @@ startSketchOn('XY')"#;
startSketchOn('XY')"#; startSketchOn('XY')"#;
let mut program = crate::parsing::top_level_parse(some_program_string).unwrap(); let mut program = crate::parsing::top_level_parse(some_program_string).unwrap();
let result = program.get_meta_settings().unwrap(); let result = program.meta_settings().unwrap();
assert!(result.is_some()); assert!(result.is_some());
let meta_settings = result.unwrap(); let meta_settings = result.unwrap();
@ -3914,7 +3914,7 @@ startSketchOn('XY')"#;
}) })
.unwrap(); .unwrap();
let result = new_program.get_meta_settings().unwrap(); let result = new_program.meta_settings().unwrap();
assert!(result.is_some()); assert!(result.is_some());
let meta_settings = result.unwrap(); let meta_settings = result.unwrap();
@ -3939,7 +3939,7 @@ startSketchOn('XY')
async fn test_parse_get_meta_settings_nothing_to_mm() { async fn test_parse_get_meta_settings_nothing_to_mm() {
let some_program_string = r#"startSketchOn('XY')"#; let some_program_string = r#"startSketchOn('XY')"#;
let mut program = crate::parsing::top_level_parse(some_program_string).unwrap(); let mut program = crate::parsing::top_level_parse(some_program_string).unwrap();
let result = program.get_meta_settings().unwrap(); let result = program.meta_settings().unwrap();
assert!(result.is_none()); assert!(result.is_none());
// Edit the ast. // Edit the ast.
@ -3950,7 +3950,7 @@ startSketchOn('XY')
}) })
.unwrap(); .unwrap();
let result = new_program.get_meta_settings().unwrap(); let result = new_program.meta_settings().unwrap();
assert!(result.is_some()); assert!(result.is_some());
let meta_settings = result.unwrap(); let meta_settings = result.unwrap();

View File

@ -24,7 +24,7 @@ lazy_static::lazy_static! {
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Validate)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Validate)]
#[ts(export)] #[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct AppearanceData { struct AppearanceData {
/// Color of the new material, a hex string like "#ff0000". /// Color of the new material, a hex string like "#ff0000".
#[schemars(regex(pattern = "#[0-9a-fA-F]{6}"))] #[schemars(regex(pattern = "#[0-9a-fA-F]{6}"))]
pub color: String, pub color: String,
@ -39,7 +39,16 @@ pub struct AppearanceData {
/// Set the appearance of a solid. This only works on solids, not sketches or individual paths. /// Set the appearance of a solid. This only works on solids, not sketches or individual paths.
pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (data, solid_set): (AppearanceData, SolidSet) = args.get_data_and_solid_set()?; let solid_set: SolidSet = args.get_unlabeled_kw_arg("solidSet")?;
let color: String = args.get_kw_arg("color")?;
let metalness: Option<f64> = args.get_kw_arg_opt("metalness")?;
let roughness: Option<f64> = args.get_kw_arg_opt("roughness")?;
let data = AppearanceData {
color,
metalness,
roughness,
};
// Validate the data. // Validate the data.
data.validate().map_err(|err| { data.validate().map_err(|err| {
@ -57,7 +66,7 @@ pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
})); }));
} }
let result = inner_appearance(data, solid_set, args).await?; let result = inner_appearance(solid_set, data.color, data.metalness, data.roughness, args).await?;
Ok(result.into()) Ok(result.into())
} }
@ -74,7 +83,8 @@ pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
/// |> close() /// |> close()
/// ///
/// example = extrude(exampleSketch, length = 5) /// example = extrude(exampleSketch, length = 5)
/// |> appearance({color= '#ff0000', metalness= 50, roughness= 50}, %) /// // There are other options besides 'color', but they're optional.
/// |> appearance(color='#ff0000')
/// ``` /// ```
/// ///
/// ```no_run /// ```no_run
@ -82,11 +92,11 @@ pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
/// sketch001 = startSketchOn('XY') /// sketch001 = startSketchOn('XY')
/// |> circle({ center = [15, 0], radius = 5 }, %) /// |> circle({ center = [15, 0], radius = 5 }, %)
/// |> revolve({ angle = 360, axis = 'y' }, %) /// |> revolve({ angle = 360, axis = 'y' }, %)
/// |> appearance({ /// |> appearance(
/// color = '#ff0000', /// color = '#ff0000',
/// metalness = 90, /// metalness = 90,
/// roughness = 90 /// roughness = 90
/// }, %) /// )
/// ``` /// ```
/// ///
/// ```no_run /// ```no_run
@ -105,8 +115,8 @@ pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
/// example1 = cube([20, 0]) /// example1 = cube([20, 0])
/// example2 = cube([40, 0]) /// example2 = cube([40, 0])
/// ///
/// appearance({color= '#ff0000', metalness= 50, roughness= 50}, [example0, example1]) /// appearance([example0, example1], color='#ff0000', metalness=50, roughness=50)
/// appearance({color= '#00ff00', metalness= 50, roughness= 50}, example2) /// appearance(example2, color='#00ff00', metalness=50, roughness=50)
/// ``` /// ```
/// ///
/// ```no_run /// ```no_run
@ -120,15 +130,16 @@ pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
/// |> close() /// |> close()
/// |> extrude(length = 6) /// |> extrude(length = 6)
/// ///
/// shell({ /// shell(
/// firstSketch,
/// faces = ['end'], /// faces = ['end'],
/// thickness = 0.25, /// thickness = 0.25,
/// }, firstSketch) /// )
/// |> appearance({ /// |> appearance(
/// color = '#ff0000', /// color = '#ff0000',
/// metalness = 90, /// metalness = 90,
/// roughness = 90 /// roughness = 90
/// }, %) /// )
/// ``` /// ```
/// ///
/// ```no_run /// ```no_run
@ -141,16 +152,17 @@ pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
/// |> line(end = [-24, 0]) /// |> line(end = [-24, 0])
/// |> close() /// |> close()
/// |> extrude(length = 6) /// |> extrude(length = 6)
/// |> appearance({ /// |> appearance(
/// color = '#ff0000', /// color = '#ff0000',
/// metalness = 90, /// metalness = 90,
/// roughness = 90 /// roughness = 90
/// }, %) /// )
/// ///
/// shell({ /// shell(
/// firstSketch,
/// faces = ['end'], /// faces = ['end'],
/// thickness = 0.25, /// thickness = 0.25,
/// }, firstSketch) /// )
/// ``` /// ```
/// ///
/// ```no_run /// ```no_run
@ -164,11 +176,11 @@ pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
/// |> close() /// |> close()
/// ///
/// example = extrude(exampleSketch, length = 1) /// example = extrude(exampleSketch, length = 1)
/// |> appearance({ /// |> appearance(
/// color = '#ff0000', /// color = '#ff0000',
/// metalness = 90, /// metalness = 90,
/// roughness = 90 /// roughness = 90
/// }, %) /// )
/// |> patternLinear3d({ /// |> patternLinear3d({
/// axis = [1, 0, 1], /// axis = [1, 0, 1],
/// instances = 7, /// instances = 7,
@ -192,11 +204,11 @@ pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
/// instances = 7, /// instances = 7,
/// distance = 6 /// distance = 6
/// }, %) /// }, %)
/// |> appearance({ /// |> appearance(
/// color = '#ff0000', /// color = '#ff0000',
/// metalness = 90, /// metalness = 90,
/// roughness = 90 /// roughness = 90
/// }, %) /// )
/// ``` /// ```
/// ///
/// ```no_run /// ```no_run
@ -215,11 +227,11 @@ pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
/// }, %) /// }, %)
/// ///
/// example = extrude(exampleSketch, length = 1) /// example = extrude(exampleSketch, length = 1)
/// |> appearance({ /// |> appearance(
/// color = '#ff0000', /// color = '#ff0000',
/// metalness = 90, /// metalness = 90,
/// roughness = 90 /// roughness = 90
/// }, %) /// )
/// ``` /// ```
/// ///
/// ```no_run /// ```no_run
@ -252,26 +264,38 @@ pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
/// radius = 2, /// radius = 2,
/// }, %) /// }, %)
/// |> hole(pipeHole, %) /// |> hole(pipeHole, %)
/// |> sweep({ /// |> sweep(path = sweepPath)
/// path: sweepPath, /// |> appearance(
/// }, %) /// color = "#ff0000",
/// |> appearance({ /// metalness = 50,
/// color: "#ff0000", /// roughness = 50
/// metalness: 50, /// )
/// roughness: 50
/// }, %)
/// ``` /// ```
#[stdlib { #[stdlib {
name = "appearance", name = "appearance",
keywords = true,
unlabeled_first = true,
args = {
solid_set = { docs = "The solid(s) whose appearance is being set" },
color = { docs = "Color of the new material, a hex string like '#ff0000'"},
metalness = { docs = "Metalness of the new material, a percentage like 95.7." },
roughness = { docs = "Roughness of the new material, a percentage like 95.7." },
}
}] }]
async fn inner_appearance(data: AppearanceData, solid_set: SolidSet, args: Args) -> Result<SolidSet, KclError> { async fn inner_appearance(
solid_set: SolidSet,
color: String,
metalness: Option<f64>,
roughness: Option<f64>,
args: Args,
) -> Result<SolidSet, KclError> {
let solids: Vec<Box<Solid>> = solid_set.into(); let solids: Vec<Box<Solid>> = solid_set.into();
for solid in &solids { for solid in &solids {
// Set the material properties. // Set the material properties.
let rgb = rgba_simple::RGB::<f32>::from_hex(&data.color).map_err(|err| { let rgb = rgba_simple::RGB::<f32>::from_hex(&color).map_err(|err| {
KclError::Semantic(KclErrorDetails { KclError::Semantic(KclErrorDetails {
message: format!("Invalid hex color (`{}`): {}", data.color, err), message: format!("Invalid hex color (`{color}`): {err}"),
source_ranges: vec![args.source_range], source_ranges: vec![args.source_range],
}) })
})?; })?;
@ -288,8 +312,8 @@ async fn inner_appearance(data: AppearanceData, solid_set: SolidSet, args: Args)
ModelingCmd::from(mcmd::ObjectSetMaterialParamsPbr { ModelingCmd::from(mcmd::ObjectSetMaterialParamsPbr {
object_id: solid.id, object_id: solid.id,
color, color,
metalness: data.metalness.unwrap_or_default() as f32 / 100.0, metalness: metalness.unwrap_or_default() as f32 / 100.0,
roughness: data.roughness.unwrap_or_default() as f32 / 100.0, roughness: roughness.unwrap_or_default() as f32 / 100.0,
ambient_occlusion: 0.0, ambient_occlusion: 0.0,
}), }),
) )

View File

@ -356,7 +356,7 @@ impl Args {
Ok(numbers) Ok(numbers)
} }
pub(crate) fn get_pattern_transform_args(&self) -> Result<(u32, FnAsArg<'_>, SolidSet), KclError> { pub(crate) fn get_pattern_transform_args(&self) -> Result<(u32, FnAsArg<'_>, SolidSet, Option<bool>), KclError> {
FromArgs::from_args(self, 0) FromArgs::from_args(self, 0)
} }
@ -764,6 +764,10 @@ macro_rules! let_field_of {
($obj:ident, $field:ident?) => { ($obj:ident, $field:ident?) => {
let $field = $obj.get(stringify!($field)).and_then(FromKclValue::from_kcl_val); let $field = $obj.get(stringify!($field)).and_then(FromKclValue::from_kcl_val);
}; };
// Optional field but with a different string used as the key
($obj:ident, $field:ident? $key:literal) => {
let $field = $obj.get($key).and_then(FromKclValue::from_kcl_val);
};
// Mandatory field, but with a different string used as the key. // Mandatory field, but with a different string used as the key.
($obj:ident, $field:ident $key:literal) => { ($obj:ident, $field:ident $key:literal) => {
let $field = $obj.get($key).and_then(FromKclValue::from_kcl_val)?; let $field = $obj.get($key).and_then(FromKclValue::from_kcl_val)?;
@ -947,12 +951,14 @@ impl<'a> FromKclValue<'a> for super::patterns::CircularPattern3dData {
let_field_of!(obj, rotate_duplicates "rotateDuplicates"); let_field_of!(obj, rotate_duplicates "rotateDuplicates");
let_field_of!(obj, axis); let_field_of!(obj, axis);
let_field_of!(obj, center); let_field_of!(obj, center);
let_field_of!(obj, use_original? "useOriginal");
Some(Self { Some(Self {
instances, instances,
axis, axis,
center, center,
arc_degrees, arc_degrees,
rotate_duplicates, rotate_duplicates,
use_original,
}) })
} }
} }
@ -964,11 +970,13 @@ impl<'a> FromKclValue<'a> for super::patterns::CircularPattern2dData {
let_field_of!(obj, arc_degrees "arcDegrees"); let_field_of!(obj, arc_degrees "arcDegrees");
let_field_of!(obj, rotate_duplicates "rotateDuplicates"); let_field_of!(obj, rotate_duplicates "rotateDuplicates");
let_field_of!(obj, center); let_field_of!(obj, center);
let_field_of!(obj, use_original? "useOriginal");
Some(Self { Some(Self {
instances, instances,
center, center,
arc_degrees, arc_degrees,
rotate_duplicates, rotate_duplicates,
use_original,
}) })
} }
} }
@ -1011,15 +1019,6 @@ impl<'a> FromKclValue<'a> for super::sketch::BezierData {
} }
} }
impl<'a> FromKclValue<'a> for super::shell::ShellData {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
let obj = arg.as_object()?;
let_field_of!(obj, thickness);
let_field_of!(obj, faces);
Some(Self { thickness, faces })
}
}
impl<'a> FromKclValue<'a> for super::chamfer::ChamferData { impl<'a> FromKclValue<'a> for super::chamfer::ChamferData {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> { fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
let obj = arg.as_object()?; let obj = arg.as_object()?;
@ -1043,34 +1042,6 @@ impl<'a> FromKclValue<'a> for super::fillet::FilletData {
} }
} }
impl<'a> FromKclValue<'a> for super::sweep::SweepData {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
let obj = arg.as_object()?;
let_field_of!(obj, path);
let_field_of!(obj, sectional?);
let_field_of!(obj, tolerance?);
Some(Self {
path,
sectional,
tolerance,
})
}
}
impl<'a> FromKclValue<'a> for super::appearance::AppearanceData {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
let obj = arg.as_object()?;
let_field_of!(obj, color);
let_field_of!(obj, metalness?);
let_field_of!(obj, roughness?);
Some(Self {
color,
metalness,
roughness,
})
}
}
impl<'a> FromKclValue<'a> for super::helix::HelixRevolutionsData { impl<'a> FromKclValue<'a> for super::helix::HelixRevolutionsData {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> { fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
let obj = arg.as_object()?; let obj = arg.as_object()?;

View File

@ -43,7 +43,7 @@ pub async fn helix(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
/// // Create a spring by sweeping around the helix path. /// // Create a spring by sweeping around the helix path.
/// springSketch = startSketchOn('YZ') /// springSketch = startSketchOn('YZ')
/// |> circle({ center = [0, 0], radius = 0.5 }, %) /// |> circle({ center = [0, 0], radius = 0.5 }, %)
/// |> sweep({ path = helixPath }, %) /// |> sweep(path = helixPath)
/// ``` /// ```
/// ///
/// ```no_run /// ```no_run
@ -64,7 +64,7 @@ pub async fn helix(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
/// // Create a spring by sweeping around the helix path. /// // Create a spring by sweeping around the helix path.
/// springSketch = startSketchOn('XY') /// springSketch = startSketchOn('XY')
/// |> circle({ center = [0, 0], radius = 0.5 }, %) /// |> circle({ center = [0, 0], radius = 0.5 }, %)
/// |> sweep({ path = helixPath }, %) /// |> sweep(path = helixPath)
/// ``` /// ```
/// ///
/// ```no_run /// ```no_run
@ -86,7 +86,7 @@ pub async fn helix(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
/// // Create a spring by sweeping around the helix path. /// // Create a spring by sweeping around the helix path.
/// springSketch = startSketchOn('XY') /// springSketch = startSketchOn('XY')
/// |> circle({ center = [0, 0], radius = 1 }, %) /// |> circle({ center = [0, 0], radius = 1 }, %)
/// |> sweep({ path = helixPath }, %) /// |> sweep(path = helixPath)
/// ``` /// ```
#[stdlib { #[stdlib {
name = "helix", name = "helix",

View File

@ -63,7 +63,7 @@ pub struct LinearPattern3dData {
/// Repeat some 3D solid, changing each repetition slightly. /// Repeat some 3D solid, changing each repetition slightly.
pub async fn pattern_transform(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn pattern_transform(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (num_repetitions, transform, extr) = args.get_pattern_transform_args()?; let (num_repetitions, transform, extr, use_original) = args.get_pattern_transform_args()?;
let solids = inner_pattern_transform( let solids = inner_pattern_transform(
num_repetitions, num_repetitions,
@ -75,6 +75,7 @@ pub async fn pattern_transform(exec_state: &mut ExecState, args: Args) -> Result
memory: *transform.memory, memory: *transform.memory,
}, },
extr, extr,
use_original,
exec_state, exec_state,
&args, &args,
) )
@ -84,7 +85,7 @@ pub async fn pattern_transform(exec_state: &mut ExecState, args: Args) -> Result
/// Repeat some 2D sketch, changing each repetition slightly. /// Repeat some 2D sketch, changing each repetition slightly.
pub async fn pattern_transform_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn pattern_transform_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (num_repetitions, transform, sketch): (u32, super::FnAsArg<'_>, SketchSet) = let (num_repetitions, transform, sketch, use_original): (u32, super::FnAsArg<'_>, SketchSet, Option<bool>) =
super::args::FromArgs::from_args(&args, 0)?; super::args::FromArgs::from_args(&args, 0)?;
let sketches = inner_pattern_transform_2d( let sketches = inner_pattern_transform_2d(
@ -97,6 +98,7 @@ pub async fn pattern_transform_2d(exec_state: &mut ExecState, args: Args) -> Res
memory: *transform.memory, memory: *transform.memory,
}, },
sketch, sketch,
use_original,
exec_state, exec_state,
&args, &args,
) )
@ -295,6 +297,7 @@ async fn inner_pattern_transform<'a>(
total_instances: u32, total_instances: u32,
transform_function: FunctionParam<'a>, transform_function: FunctionParam<'a>,
solid_set: SolidSet, solid_set: SolidSet,
use_original: Option<bool>,
exec_state: &mut ExecState, exec_state: &mut ExecState,
args: &'a Args, args: &'a Args,
) -> Result<Vec<Box<Solid>>, KclError> { ) -> Result<Vec<Box<Solid>>, KclError> {
@ -310,7 +313,7 @@ async fn inner_pattern_transform<'a>(
let t = make_transform::<Box<Solid>>(i, &transform_function, args.source_range, exec_state).await?; let t = make_transform::<Box<Solid>>(i, &transform_function, args.source_range, exec_state).await?;
transform.push(t); transform.push(t);
} }
execute_pattern_transform(transform, solid_set, exec_state, args).await execute_pattern_transform(transform, solid_set, use_original.unwrap_or_default(), exec_state, args).await
} }
/// Just like patternTransform, but works on 2D sketches not 3D solids. /// Just like patternTransform, but works on 2D sketches not 3D solids.
@ -332,6 +335,7 @@ async fn inner_pattern_transform_2d<'a>(
total_instances: u32, total_instances: u32,
transform_function: FunctionParam<'a>, transform_function: FunctionParam<'a>,
solid_set: SketchSet, solid_set: SketchSet,
use_original: Option<bool>,
exec_state: &mut ExecState, exec_state: &mut ExecState,
args: &'a Args, args: &'a Args,
) -> Result<Vec<Box<Sketch>>, KclError> { ) -> Result<Vec<Box<Sketch>>, KclError> {
@ -347,12 +351,13 @@ async fn inner_pattern_transform_2d<'a>(
let t = make_transform::<Box<Sketch>>(i, &transform_function, args.source_range, exec_state).await?; let t = make_transform::<Box<Sketch>>(i, &transform_function, args.source_range, exec_state).await?;
transform.push(t); transform.push(t);
} }
execute_pattern_transform(transform, solid_set, exec_state, args).await execute_pattern_transform(transform, solid_set, use_original.unwrap_or_default(), exec_state, args).await
} }
async fn execute_pattern_transform<T: GeometryTrait>( async fn execute_pattern_transform<T: GeometryTrait>(
transforms: Vec<Vec<Transform>>, transforms: Vec<Vec<Transform>>,
geo_set: T::Set, geo_set: T::Set,
use_original: bool,
exec_state: &mut ExecState, exec_state: &mut ExecState,
args: &Args, args: &Args,
) -> Result<Vec<T>, KclError> { ) -> Result<Vec<T>, KclError> {
@ -368,7 +373,7 @@ async fn execute_pattern_transform<T: GeometryTrait>(
let mut output = Vec::new(); let mut output = Vec::new();
for geo in starting { for geo in starting {
let new = send_pattern_transform(transforms.clone(), &geo, exec_state, args).await?; let new = send_pattern_transform(transforms.clone(), &geo, use_original, exec_state, args).await?;
output.extend(new) output.extend(new)
} }
Ok(output) Ok(output)
@ -379,6 +384,7 @@ async fn send_pattern_transform<T: GeometryTrait>(
// https://github.com/KittyCAD/modeling-app/issues/2821 // https://github.com/KittyCAD/modeling-app/issues/2821
transforms: Vec<Vec<Transform>>, transforms: Vec<Vec<Transform>>,
solid: &T, solid: &T,
use_original: bool,
exec_state: &mut ExecState, exec_state: &mut ExecState,
args: &Args, args: &Args,
) -> Result<Vec<T>, KclError> { ) -> Result<Vec<T>, KclError> {
@ -388,7 +394,7 @@ async fn send_pattern_transform<T: GeometryTrait>(
.send_modeling_cmd( .send_modeling_cmd(
id, id,
ModelingCmd::from(mcmd::EntityLinearPatternTransform { ModelingCmd::from(mcmd::EntityLinearPatternTransform {
entity_id: solid.id(), entity_id: if use_original { solid.original_id() } else { solid.id() },
transform: Default::default(), transform: Default::default(),
transforms, transforms,
}), }),
@ -602,6 +608,7 @@ fn array_to_point2d(val: &KclValue, source_ranges: Vec<SourceRange>) -> Result<P
trait GeometryTrait: Clone { trait GeometryTrait: Clone {
type Set: Into<Vec<Self>> + Clone; type Set: Into<Vec<Self>> + Clone;
fn id(&self) -> Uuid; fn id(&self) -> Uuid;
fn original_id(&self) -> Uuid;
fn set_id(&mut self, id: Uuid); fn set_id(&mut self, id: Uuid);
fn array_to_point3d(val: &KclValue, source_ranges: Vec<SourceRange>) -> Result<Point3d, KclError>; fn array_to_point3d(val: &KclValue, source_ranges: Vec<SourceRange>) -> Result<Point3d, KclError>;
async fn flush_batch(args: &Args, exec_state: &mut ExecState, set: Self::Set) -> Result<(), KclError>; async fn flush_batch(args: &Args, exec_state: &mut ExecState, set: Self::Set) -> Result<(), KclError>;
@ -615,6 +622,9 @@ impl GeometryTrait for Box<Sketch> {
fn id(&self) -> Uuid { fn id(&self) -> Uuid {
self.id self.id
} }
fn original_id(&self) -> Uuid {
self.original_id
}
fn array_to_point3d(val: &KclValue, source_ranges: Vec<SourceRange>) -> Result<Point3d, KclError> { fn array_to_point3d(val: &KclValue, source_ranges: Vec<SourceRange>) -> Result<Point3d, KclError> {
let Point2d { x, y } = array_to_point2d(val, source_ranges)?; let Point2d { x, y } = array_to_point2d(val, source_ranges)?;
Ok(Point3d { x, y, z: 0.0 }) Ok(Point3d { x, y, z: 0.0 })
@ -634,6 +644,11 @@ impl GeometryTrait for Box<Solid> {
fn id(&self) -> Uuid { fn id(&self) -> Uuid {
self.id self.id
} }
fn original_id(&self) -> Uuid {
self.sketch.original_id
}
fn array_to_point3d(val: &KclValue, source_ranges: Vec<SourceRange>) -> Result<Point3d, KclError> { fn array_to_point3d(val: &KclValue, source_ranges: Vec<SourceRange>) -> Result<Point3d, KclError> {
array_to_point3d(val, source_ranges) array_to_point3d(val, source_ranges)
} }
@ -674,7 +689,8 @@ mod tests {
/// A linear pattern on a 2D sketch. /// A linear pattern on a 2D sketch.
pub async fn pattern_linear_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn pattern_linear_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (data, sketch_set): (LinearPattern2dData, SketchSet) = args.get_data_and_sketch_set()?; let (data, sketch_set, use_original): (LinearPattern2dData, SketchSet, Option<bool>) =
super::args::FromArgs::from_args(&args, 0)?;
if data.axis == [0.0, 0.0] { if data.axis == [0.0, 0.0] {
return Err(KclError::Semantic(KclErrorDetails { return Err(KclError::Semantic(KclErrorDetails {
@ -685,7 +701,7 @@ pub async fn pattern_linear_2d(exec_state: &mut ExecState, args: Args) -> Result
})); }));
} }
let sketches = inner_pattern_linear_2d(data, sketch_set, exec_state, args).await?; let sketches = inner_pattern_linear_2d(data, sketch_set, use_original, exec_state, args).await?;
Ok(sketches.into()) Ok(sketches.into())
} }
@ -709,6 +725,7 @@ pub async fn pattern_linear_2d(exec_state: &mut ExecState, args: Args) -> Result
async fn inner_pattern_linear_2d( async fn inner_pattern_linear_2d(
data: LinearPattern2dData, data: LinearPattern2dData,
sketch_set: SketchSet, sketch_set: SketchSet,
use_original: Option<bool>,
exec_state: &mut ExecState, exec_state: &mut ExecState,
args: Args, args: Args,
) -> Result<Vec<Box<Sketch>>, KclError> { ) -> Result<Vec<Box<Sketch>>, KclError> {
@ -726,12 +743,20 @@ async fn inner_pattern_linear_2d(
}] }]
}) })
.collect(); .collect();
execute_pattern_transform(transforms, sketch_set, exec_state, &args).await execute_pattern_transform(
transforms,
sketch_set,
use_original.unwrap_or_default(),
exec_state,
&args,
)
.await
} }
/// A linear pattern on a 3D model. /// A linear pattern on a 3D model.
pub async fn pattern_linear_3d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn pattern_linear_3d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (data, solid_set): (LinearPattern3dData, SolidSet) = args.get_data_and_solid_set()?; let (data, solid_set, use_original): (LinearPattern3dData, SolidSet, Option<bool>) =
super::args::FromArgs::from_args(&args, 0)?;
if data.axis == [0.0, 0.0, 0.0] { if data.axis == [0.0, 0.0, 0.0] {
return Err(KclError::Semantic(KclErrorDetails { return Err(KclError::Semantic(KclErrorDetails {
@ -742,7 +767,7 @@ pub async fn pattern_linear_3d(exec_state: &mut ExecState, args: Args) -> Result
})); }));
} }
let solids = inner_pattern_linear_3d(data, solid_set, exec_state, args).await?; let solids = inner_pattern_linear_3d(data, solid_set, use_original, exec_state, args).await?;
Ok(solids.into()) Ok(solids.into())
} }
@ -771,6 +796,7 @@ pub async fn pattern_linear_3d(exec_state: &mut ExecState, args: Args) -> Result
async fn inner_pattern_linear_3d( async fn inner_pattern_linear_3d(
data: LinearPattern3dData, data: LinearPattern3dData,
solid_set: SolidSet, solid_set: SolidSet,
use_original: Option<bool>,
exec_state: &mut ExecState, exec_state: &mut ExecState,
args: Args, args: Args,
) -> Result<Vec<Box<Solid>>, KclError> { ) -> Result<Vec<Box<Solid>>, KclError> {
@ -788,7 +814,14 @@ async fn inner_pattern_linear_3d(
}] }]
}) })
.collect(); .collect();
execute_pattern_transform(transforms, solid_set, exec_state, &args).await execute_pattern_transform(
transforms,
solid_set,
use_original.unwrap_or_default(),
exec_state,
&args,
)
.await
} }
/// Data for a circular pattern on a 2D sketch. /// Data for a circular pattern on a 2D sketch.
@ -807,6 +840,10 @@ pub struct CircularPattern2dData {
pub arc_degrees: f64, pub arc_degrees: f64,
/// Whether or not to rotate the duplicates as they are copied. /// Whether or not to rotate the duplicates as they are copied.
pub rotate_duplicates: bool, pub rotate_duplicates: bool,
/// If the target being patterned is itself a pattern, then, should you use the original solid,
/// or the pattern?
#[serde(default)]
pub use_original: Option<bool>,
} }
/// Data for a circular pattern on a 3D model. /// Data for a circular pattern on a 3D model.
@ -827,6 +864,10 @@ pub struct CircularPattern3dData {
pub arc_degrees: f64, pub arc_degrees: f64,
/// Whether or not to rotate the duplicates as they are copied. /// Whether or not to rotate the duplicates as they are copied.
pub rotate_duplicates: bool, pub rotate_duplicates: bool,
/// If the target being patterned is itself a pattern, then, should you use the original solid,
/// or the pattern?
#[serde(default)]
pub use_original: Option<bool>,
} }
pub enum CircularPattern { pub enum CircularPattern {
@ -889,6 +930,13 @@ impl CircularPattern {
CircularPattern::ThreeD(lp) => lp.rotate_duplicates, CircularPattern::ThreeD(lp) => lp.rotate_duplicates,
} }
} }
pub fn use_original(&self) -> bool {
match self {
CircularPattern::TwoD(lp) => lp.use_original.unwrap_or_default(),
CircularPattern::ThreeD(lp) => lp.use_original.unwrap_or_default(),
}
}
} }
/// A circular pattern on a 2D sketch. /// A circular pattern on a 2D sketch.
@ -1055,7 +1103,11 @@ async fn pattern_circular(
id, id,
ModelingCmd::from(mcmd::EntityCircularPattern { ModelingCmd::from(mcmd::EntityCircularPattern {
axis: kcmc::shared::Point3d::from(data.axis()), axis: kcmc::shared::Point3d::from(data.axis()),
entity_id: geometry.id(), entity_id: if data.use_original() {
geometry.original_id()
} else {
geometry.id()
},
center: kcmc::shared::Point3d { center: kcmc::shared::Point3d {
x: LengthUnit(center[0]), x: LengthUnit(center[0]),
y: LengthUnit(center[1]), y: LengthUnit(center[1]),

View File

@ -4,8 +4,6 @@ use anyhow::Result;
use derive_docs::stdlib; use derive_docs::stdlib;
use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, ModelingCmd}; use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, ModelingCmd};
use kittycad_modeling_cmds as kcmc; use kittycad_modeling_cmds as kcmc;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
@ -13,22 +11,13 @@ use crate::{
std::{sketch::FaceTag, Args}, std::{sketch::FaceTag, Args},
}; };
/// Data for shells.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct ShellData {
/// The thickness of the shell.
pub thickness: f64,
/// The faces you want removed.
pub faces: Vec<FaceTag>,
}
/// Create a shell. /// Create a shell.
pub async fn shell(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn shell(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (data, solid_set): (ShellData, SolidSet) = args.get_data_and_solid_set()?; let solid_set = args.get_unlabeled_kw_arg("solidSet")?;
let thickness = args.get_kw_arg("thickness")?;
let faces = args.get_kw_arg("faces")?;
let result = inner_shell(data, solid_set, exec_state, args).await?; let result = inner_shell(solid_set, thickness, faces, exec_state, args).await?;
Ok(result.into()) Ok(result.into())
} }
@ -47,10 +36,11 @@ pub async fn shell(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
/// |> extrude(length = 6) /// |> extrude(length = 6)
/// ///
/// // Remove the end face for the extrusion. /// // Remove the end face for the extrusion.
/// shell({ /// shell(
/// firstSketch,
/// faces = ['end'], /// faces = ['end'],
/// thickness = 0.25, /// thickness = 0.25,
/// }, firstSketch) /// )
/// ``` /// ```
/// ///
/// ```no_run /// ```no_run
@ -64,10 +54,11 @@ pub async fn shell(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
/// |> extrude(length = 6) /// |> extrude(length = 6)
/// ///
/// // Remove the start face for the extrusion. /// // Remove the start face for the extrusion.
/// shell({ /// shell(
/// firstSketch,
/// faces = ['start'], /// faces = ['start'],
/// thickness = 0.25, /// thickness = 0.25,
/// }, firstSketch) /// )
/// ``` /// ```
/// ///
/// ```no_run /// ```no_run
@ -81,10 +72,11 @@ pub async fn shell(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
/// |> extrude(length = 6) /// |> extrude(length = 6)
/// ///
/// // Remove a tagged face for the extrusion. /// // Remove a tagged face for the extrusion.
/// shell({ /// shell(
/// firstSketch,
/// faces = [myTag], /// faces = [myTag],
/// thickness = 0.25, /// thickness = 0.25,
/// }, firstSketch) /// )
/// ``` /// ```
/// ///
/// ```no_run /// ```no_run
@ -98,10 +90,11 @@ pub async fn shell(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
/// |> extrude(length = 6) /// |> extrude(length = 6)
/// ///
/// // Remove a tagged face and the end face for the extrusion. /// // Remove a tagged face and the end face for the extrusion.
/// shell({ /// shell(
/// firstSketch,
/// faces = [myTag, 'end'], /// faces = [myTag, 'end'],
/// thickness = 0.25, /// thickness = 0.25,
/// }, firstSketch) /// )
/// ``` /// ```
/// ///
/// ```no_run /// ```no_run
@ -124,7 +117,7 @@ pub async fn shell(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
/// |> extrude(length = 50) /// |> extrude(length = 50)
/// ///
/// // We put "case" in the shell function to shell the entire object. /// // We put "case" in the shell function to shell the entire object.
/// shell({ faces = ['start'], thickness = 5 }, case) /// shell(case, faces = ['start'], thickness = 5)
/// ``` /// ```
/// ///
/// ```no_run /// ```no_run
@ -147,7 +140,7 @@ pub async fn shell(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
/// |> extrude(length = 50) /// |> extrude(length = 50)
/// ///
/// // We put "thing1" in the shell function to shell the end face of the object. /// // We put "thing1" in the shell function to shell the end face of the object.
/// shell({ faces = ['end'], thickness = 5 }, thing1) /// shell(thing1, faces = ['end'], thickness = 5)
/// ``` /// ```
/// ///
/// ```no_run /// ```no_run
@ -172,21 +165,29 @@ pub async fn shell(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
/// |> extrude(length = 50) /// |> extrude(length = 50)
/// ///
/// // We put "thing1" and "thing2" in the shell function to shell the end face of the object. /// // We put "thing1" and "thing2" in the shell function to shell the end face of the object.
/// shell({ faces = ['end'], thickness = 5 }, [thing1, thing2]) /// shell([thing1, thing2], faces = ['end'], thickness = 5)
/// ``` /// ```
#[stdlib { #[stdlib {
name = "shell", name = "shell",
feature_tree_operation = true, feature_tree_operation = true,
keywords = true,
unlabeled_first = true,
args = {
solid_set = { docs = "Which solid (or solids) to shell out"},
thickness = {docs = "The thickness of the shell"},
faces = {docs = "The faces you want removed"},
}
}] }]
async fn inner_shell( async fn inner_shell(
data: ShellData,
solid_set: SolidSet, solid_set: SolidSet,
thickness: f64,
faces: Vec<FaceTag>,
exec_state: &mut ExecState, exec_state: &mut ExecState,
args: Args, args: Args,
) -> Result<SolidSet, KclError> { ) -> Result<SolidSet, KclError> {
if data.faces.is_empty() { if faces.is_empty() {
return Err(KclError::Type(KclErrorDetails { return Err(KclError::Type(KclErrorDetails {
message: "Expected at least one face".to_string(), message: "You must shell at least one face".to_string(),
source_ranges: vec![args.source_range], source_ranges: vec![args.source_range],
})); }));
} }
@ -194,7 +195,7 @@ async fn inner_shell(
let solids: Vec<Box<Solid>> = solid_set.clone().into(); let solids: Vec<Box<Solid>> = solid_set.clone().into();
if solids.is_empty() { if solids.is_empty() {
return Err(KclError::Type(KclErrorDetails { return Err(KclError::Type(KclErrorDetails {
message: "Expected at least one solid".to_string(), message: "You must shell at least one solid".to_string(),
source_ranges: vec![args.source_range], source_ranges: vec![args.source_range],
})); }));
} }
@ -205,7 +206,7 @@ async fn inner_shell(
// If we do not do these for sketch on face, things will fail with face does not exist. // If we do not do these for sketch on face, things will fail with face does not exist.
args.flush_batch_for_solid_set(exec_state, solid.clone().into()).await?; args.flush_batch_for_solid_set(exec_state, solid.clone().into()).await?;
for tag in &data.faces { for tag in &faces {
let extrude_plane_id = tag.get_face_id(solid, exec_state, &args, false).await?; let extrude_plane_id = tag.get_face_id(solid, exec_state, &args, false).await?;
face_ids.push(extrude_plane_id); face_ids.push(extrude_plane_id);
@ -235,7 +236,7 @@ async fn inner_shell(
hollow: false, hollow: false,
face_ids, face_ids,
object_id: solids[0].id, object_id: solids[0].id,
shell_thickness: LengthUnit(data.thickness), shell_thickness: LengthUnit(thickness),
}), }),
) )
.await?; .await?;

View File

@ -1360,6 +1360,7 @@ pub(crate) async fn inner_start_profile_at(
let sketch = Sketch { let sketch = Sketch {
id: path_id, id: path_id,
original_id: path_id,
artifact_id: path_id.into(), artifact_id: path_id.into(),
on: sketch_surface.clone(), on: sketch_surface.clone(),
paths: vec![], paths: vec![],

View File

@ -22,24 +22,14 @@ pub enum SweepPath {
Helix(Box<Helix>), Helix(Box<Helix>),
} }
/// Data for a sweep.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct SweepData {
/// The path to sweep along.
pub path: SweepPath,
/// If true, the sweep will be broken up into sub-sweeps (extrusions, revolves, sweeps) based on the trajectory path components.
pub sectional: Option<bool>,
/// Tolerance for the sweep operation.
#[serde(default)]
pub tolerance: Option<f64>,
}
/// Extrude a sketch along a path. /// Extrude a sketch along a path.
pub async fn sweep(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn sweep(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (data, sketch): (SweepData, Sketch) = args.get_data_and_sketch()?; let sketch = args.get_unlabeled_kw_arg("sketch")?;
let path: SweepPath = args.get_kw_arg("path")?;
let sectional = args.get_kw_arg_opt("sectional")?;
let tolerance = args.get_kw_arg_opt("tolerance")?;
let value = inner_sweep(data, sketch, exec_state, args).await?; let value = inner_sweep(sketch, path, sectional, tolerance, exec_state, args).await?;
Ok(KclValue::Solid { value }) Ok(KclValue::Solid { value })
} }
@ -82,9 +72,7 @@ pub async fn sweep(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
/// radius = 2, /// radius = 2,
/// }, %) /// }, %)
/// |> hole(pipeHole, %) /// |> hole(pipeHole, %)
/// |> sweep({ /// |> sweep(path = sweepPath)
/// path: sweepPath,
/// }, %)
/// ``` /// ```
/// ///
/// ```no_run /// ```no_run
@ -104,15 +92,25 @@ pub async fn sweep(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
/// // Create a spring by sweeping around the helix path. /// // Create a spring by sweeping around the helix path.
/// springSketch = startSketchOn('YZ') /// springSketch = startSketchOn('YZ')
/// |> circle({ center = [0, 0], radius = 1 }, %) /// |> circle({ center = [0, 0], radius = 1 }, %)
/// |> sweep({ path = helixPath }, %) /// |> sweep(path = helixPath)
/// ``` /// ```
#[stdlib { #[stdlib {
name = "sweep", name = "sweep",
feature_tree_operation = true, feature_tree_operation = true,
keywords = true,
unlabeled_first = true,
args = {
sketch = { docs = "The sketch that should be swept in space" },
path = { docs = "The path to sweep the sketch along" },
sectional = { docs = "If true, the sweep will be broken up into sub-sweeps (extrusions, revolves, sweeps) based on the trajectory path components." },
tolerance = { docs = "Tolerance for this operation" },
}
}] }]
async fn inner_sweep( async fn inner_sweep(
data: SweepData,
sketch: Sketch, sketch: Sketch,
path: SweepPath,
sectional: Option<bool>,
tolerance: Option<f64>,
exec_state: &mut ExecState, exec_state: &mut ExecState,
args: Args, args: Args,
) -> Result<Box<Solid>, KclError> { ) -> Result<Box<Solid>, KclError> {
@ -121,12 +119,12 @@ async fn inner_sweep(
id, id,
ModelingCmd::from(mcmd::Sweep { ModelingCmd::from(mcmd::Sweep {
target: sketch.id.into(), target: sketch.id.into(),
trajectory: match data.path { trajectory: match path {
SweepPath::Sketch(sketch) => sketch.id.into(), SweepPath::Sketch(sketch) => sketch.id.into(),
SweepPath::Helix(helix) => helix.value.into(), SweepPath::Helix(helix) => helix.value.into(),
}, },
sectional: data.sectional.unwrap_or(false), sectional: sectional.unwrap_or(false),
tolerance: LengthUnit(data.tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units))), tolerance: LengthUnit(tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units))),
}), }),
) )
.await?; .await?;

View File

@ -1,6 +1,7 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl/src/simulation_tests.rs
description: Program memory after executing angled_line.kcl description: Program memory after executing angled_line.kcl
snapshot_kind: text
--- ---
{ {
"environments": [ "environments": [
@ -346,6 +347,7 @@ description: Program memory after executing angled_line.kcl
} }
}, },
"artifactId": "[uuid]", "artifactId": "[uuid]",
"originalId": "[uuid]",
"units": { "units": {
"type": "Mm" "type": "Mm"
}, },

View File

@ -374,6 +374,7 @@ snapshot_kind: text
} }
}, },
"artifactId": "[uuid]", "artifactId": "[uuid]",
"originalId": "[uuid]",
"units": { "units": {
"type": "Mm" "type": "Mm"
}, },
@ -904,6 +905,7 @@ snapshot_kind: text
} }
}, },
"artifactId": "[uuid]", "artifactId": "[uuid]",
"originalId": "[uuid]",
"units": { "units": {
"type": "Mm" "type": "Mm"
}, },
@ -975,6 +977,7 @@ snapshot_kind: text
} }
}, },
"artifactId": "[uuid]", "artifactId": "[uuid]",
"originalId": "[uuid]",
"units": { "units": {
"type": "Mm" "type": "Mm"
}, },
@ -1412,6 +1415,7 @@ snapshot_kind: text
} }
}, },
"artifactId": "[uuid]", "artifactId": "[uuid]",
"originalId": "[uuid]",
"units": { "units": {
"type": "Mm" "type": "Mm"
}, },
@ -1878,6 +1882,7 @@ snapshot_kind: text
} }
}, },
"artifactId": "[uuid]", "artifactId": "[uuid]",
"originalId": "[uuid]",
"units": { "units": {
"type": "Mm" "type": "Mm"
}, },
@ -1949,6 +1954,7 @@ snapshot_kind: text
} }
}, },
"artifactId": "[uuid]", "artifactId": "[uuid]",
"originalId": "[uuid]",
"units": { "units": {
"type": "Mm" "type": "Mm"
}, },

View File

@ -468,6 +468,7 @@ snapshot_kind: text
} }
}, },
"artifactId": "[uuid]", "artifactId": "[uuid]",
"originalId": "[uuid]",
"units": { "units": {
"type": "Mm" "type": "Mm"
}, },
@ -609,6 +610,7 @@ snapshot_kind: text
} }
}, },
"artifactId": "[uuid]", "artifactId": "[uuid]",
"originalId": "[uuid]",
"units": { "units": {
"type": "Mm" "type": "Mm"
}, },

View File

@ -200,6 +200,7 @@ snapshot_kind: text
} }
}, },
"artifactId": "[uuid]", "artifactId": "[uuid]",
"originalId": "[uuid]",
"units": { "units": {
"type": "Mm" "type": "Mm"
}, },

View File

@ -274,6 +274,7 @@ snapshot_kind: text
} }
}, },
"artifactId": "[uuid]", "artifactId": "[uuid]",
"originalId": "[uuid]",
"units": { "units": {
"type": "Mm" "type": "Mm"
}, },
@ -695,6 +696,7 @@ snapshot_kind: text
} }
}, },
"artifactId": "[uuid]", "artifactId": "[uuid]",
"originalId": "[uuid]",
"units": { "units": {
"type": "Mm" "type": "Mm"
}, },
@ -757,6 +759,7 @@ snapshot_kind: text
} }
}, },
"artifactId": "[uuid]", "artifactId": "[uuid]",
"originalId": "[uuid]",
"units": { "units": {
"type": "Mm" "type": "Mm"
}, },
@ -1332,6 +1335,7 @@ snapshot_kind: text
} }
}, },
"artifactId": "[uuid]", "artifactId": "[uuid]",
"originalId": "[uuid]",
"units": { "units": {
"type": "Mm" "type": "Mm"
}, },
@ -1394,6 +1398,7 @@ snapshot_kind: text
} }
}, },
"artifactId": "[uuid]", "artifactId": "[uuid]",
"originalId": "[uuid]",
"units": { "units": {
"type": "Mm" "type": "Mm"
}, },
@ -1517,6 +1522,7 @@ snapshot_kind: text
} }
}, },
"artifactId": "[uuid]", "artifactId": "[uuid]",
"originalId": "[uuid]",
"units": { "units": {
"type": "Mm" "type": "Mm"
}, },
@ -2236,6 +2242,7 @@ snapshot_kind: text
} }
}, },
"artifactId": "[uuid]", "artifactId": "[uuid]",
"originalId": "[uuid]",
"units": { "units": {
"type": "Mm" "type": "Mm"
}, },
@ -2298,6 +2305,7 @@ snapshot_kind: text
} }
}, },
"artifactId": "[uuid]", "artifactId": "[uuid]",
"originalId": "[uuid]",
"units": { "units": {
"type": "Mm" "type": "Mm"
}, },
@ -2421,6 +2429,7 @@ snapshot_kind: text
} }
}, },
"artifactId": "[uuid]", "artifactId": "[uuid]",
"originalId": "[uuid]",
"units": { "units": {
"type": "Mm" "type": "Mm"
}, },
@ -2483,6 +2492,7 @@ snapshot_kind: text
} }
}, },
"artifactId": "[uuid]", "artifactId": "[uuid]",
"originalId": "[uuid]",
"units": { "units": {
"type": "Mm" "type": "Mm"
}, },
@ -2836,6 +2846,7 @@ snapshot_kind: text
} }
}, },
"artifactId": "[uuid]", "artifactId": "[uuid]",
"originalId": "[uuid]",
"units": { "units": {
"type": "Mm" "type": "Mm"
}, },
@ -3202,6 +3213,7 @@ snapshot_kind: text
} }
}, },
"artifactId": "[uuid]", "artifactId": "[uuid]",
"originalId": "[uuid]",
"units": { "units": {
"type": "Mm" "type": "Mm"
}, },
@ -3264,6 +3276,7 @@ snapshot_kind: text
} }
}, },
"artifactId": "[uuid]", "artifactId": "[uuid]",
"originalId": "[uuid]",
"units": { "units": {
"type": "Mm" "type": "Mm"
}, },
@ -3779,6 +3792,7 @@ snapshot_kind: text
} }
}, },
"artifactId": "[uuid]", "artifactId": "[uuid]",
"originalId": "[uuid]",
"units": { "units": {
"type": "Mm" "type": "Mm"
}, },
@ -3841,6 +3855,7 @@ snapshot_kind: text
} }
}, },
"artifactId": "[uuid]", "artifactId": "[uuid]",
"originalId": "[uuid]",
"units": { "units": {
"type": "Mm" "type": "Mm"
}, },
@ -3964,6 +3979,7 @@ snapshot_kind: text
} }
}, },
"artifactId": "[uuid]", "artifactId": "[uuid]",
"originalId": "[uuid]",
"units": { "units": {
"type": "Mm" "type": "Mm"
}, },
@ -4628,6 +4644,7 @@ snapshot_kind: text
} }
}, },
"artifactId": "[uuid]", "artifactId": "[uuid]",
"originalId": "[uuid]",
"units": { "units": {
"type": "Mm" "type": "Mm"
}, },
@ -4690,6 +4707,7 @@ snapshot_kind: text
} }
}, },
"artifactId": "[uuid]", "artifactId": "[uuid]",
"originalId": "[uuid]",
"units": { "units": {
"type": "Mm" "type": "Mm"
}, },
@ -4813,6 +4831,7 @@ snapshot_kind: text
} }
}, },
"artifactId": "[uuid]", "artifactId": "[uuid]",
"originalId": "[uuid]",
"units": { "units": {
"type": "Mm" "type": "Mm"
}, },
@ -4875,6 +4894,7 @@ snapshot_kind: text
} }
}, },
"artifactId": "[uuid]", "artifactId": "[uuid]",
"originalId": "[uuid]",
"units": { "units": {
"type": "Mm" "type": "Mm"
}, },

View File

@ -1,6 +1,7 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl/src/simulation_tests.rs
description: Program memory after executing basic_fillet_cube_close_opposite.kcl description: Program memory after executing basic_fillet_cube_close_opposite.kcl
snapshot_kind: text
--- ---
{ {
"environments": [ "environments": [
@ -422,6 +423,7 @@ description: Program memory after executing basic_fillet_cube_close_opposite.kcl
} }
}, },
"artifactId": "[uuid]", "artifactId": "[uuid]",
"originalId": "[uuid]",
"units": { "units": {
"type": "Mm" "type": "Mm"
}, },

View File

@ -1,6 +1,7 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl/src/simulation_tests.rs
description: Program memory after executing basic_fillet_cube_end.kcl description: Program memory after executing basic_fillet_cube_end.kcl
snapshot_kind: text
--- ---
{ {
"environments": [ "environments": [
@ -353,6 +354,7 @@ description: Program memory after executing basic_fillet_cube_end.kcl
} }
}, },
"artifactId": "[uuid]", "artifactId": "[uuid]",
"originalId": "[uuid]",
"units": { "units": {
"type": "Mm" "type": "Mm"
}, },

View File

@ -1,6 +1,7 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl/src/simulation_tests.rs
description: Program memory after executing basic_fillet_cube_next_adjacent.kcl description: Program memory after executing basic_fillet_cube_next_adjacent.kcl
snapshot_kind: text
--- ---
{ {
"environments": [ "environments": [
@ -491,6 +492,7 @@ description: Program memory after executing basic_fillet_cube_next_adjacent.kcl
} }
}, },
"artifactId": "[uuid]", "artifactId": "[uuid]",
"originalId": "[uuid]",
"units": { "units": {
"type": "Mm" "type": "Mm"
}, },

View File

@ -1,6 +1,7 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl/src/simulation_tests.rs
description: Program memory after executing basic_fillet_cube_previous_adjacent.kcl description: Program memory after executing basic_fillet_cube_previous_adjacent.kcl
snapshot_kind: text
--- ---
{ {
"environments": [ "environments": [
@ -491,6 +492,7 @@ description: Program memory after executing basic_fillet_cube_previous_adjacent.
} }
}, },
"artifactId": "[uuid]", "artifactId": "[uuid]",
"originalId": "[uuid]",
"units": { "units": {
"type": "Mm" "type": "Mm"
}, },

View File

@ -1,6 +1,7 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl/src/simulation_tests.rs
description: Program memory after executing basic_fillet_cube_start.kcl description: Program memory after executing basic_fillet_cube_start.kcl
snapshot_kind: text
--- ---
{ {
"environments": [ "environments": [
@ -353,6 +354,7 @@ description: Program memory after executing basic_fillet_cube_start.kcl
} }
}, },
"artifactId": "[uuid]", "artifactId": "[uuid]",
"originalId": "[uuid]",
"units": { "units": {
"type": "Mm" "type": "Mm"
}, },

View File

@ -1,6 +1,7 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl/src/simulation_tests.rs
description: Program memory after executing big_number_angle_to_match_length_x.kcl description: Program memory after executing big_number_angle_to_match_length_x.kcl
snapshot_kind: text
--- ---
{ {
"environments": [ "environments": [
@ -253,6 +254,7 @@ description: Program memory after executing big_number_angle_to_match_length_x.k
} }
}, },
"artifactId": "[uuid]", "artifactId": "[uuid]",
"originalId": "[uuid]",
"units": { "units": {
"type": "Mm" "type": "Mm"
}, },

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