Compare commits

...

18 Commits

Author SHA1 Message Date
19a8a2bba8 add script to build on mac mini 2025-03-14 15:36:01 -04:00
e9806b83d7 Clip deltaY to +/- 100 (#5793)
* Clip deltaY to +/- 100
Should fix #5120

* Lint

* Fix the clip with @nickmccleery's suggestion
2025-03-14 13:17:10 -04:00
0229105158 fix: add better errors for missing commas in arrays and objects (#5210)
* fix: add better errors for missing commas in arrays and objects

* chore: add object prop shorthand missing comma test

* fix: wording on unexpected character in arrays and objects

* fix: don't eagerly evaluate whether there is a closing brace/bracket

* feat: exit early when detecting missing commas if encountering invalid tokens

* 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: updated reserved word in test to replace removed one

---------

Co-authored-by: Tom Pridham <pridham.tom@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-03-14 16:52:10 +00:00
9e37e13b6b Further unfrig playwright (#5794)
* Bail on cleanup after 10s; move setup even higher

* Give macos time before starting an electron instance

* Try it again

* Again

* Remove stale TODO

* Use Kolmogorov complexity instead of text for ai assertions

* Help out prompt-to-edit test with being more explicit

* Try to give Mac more time to open a window

* Fix the other var change for prompt-to-edit test

* Forgot this

* Try some crazier shit

* 🤦 I forgot the return.

* Ok things were actually just not working well at all

* Fix export test to expect smaller size

* yarn tsc && yarn lint && yarn fmt
2025-03-14 10:19:58 +00:00
58e0c0e916 Improve KCL Samples (#5767)
* improve KCL Samples & .gitignore

* update block and car wheel assembly

* update flange and lego, delete flange xy

* artifacts

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

* updates

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

* updates

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

* scale

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

* updates

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

* docs

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

* updates

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

* updates

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

* updates

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Jess Frazelle <github@jessfraz.com>
2025-03-13 23:38:51 -07:00
dd99c27d56 bump all the rust things (#5806)
* updates

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

* getrandom

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

* updates

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

* simlimk

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

* Revert "simlimk"

This reverts commit 3f5221db7e.

* terst

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-03-13 23:38:41 -07:00
3cff26b987 make sure all enter sketch mode are with the stuff they need in the same batch order always (#5646)
* updates

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

* updates

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

* updates

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

* updates

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

* updates

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

* updates

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

* updates

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

* updates

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

* comment out

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

* updates

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

* update artifacts

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

* small

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

* updates

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

* updates

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

* last of the artifacts

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

* update playwirght

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

* updates

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

* add crazy multi-profile test

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

* updates

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

* steps

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

* fix artifact graph

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

* updates

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

* cleanup

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

* updates
;

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

* more artifact grph

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

* turn back on playwright

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

* fmt

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

* playwright fixes

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

* playwright fixes

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-03-13 21:59:39 -07:00
78ac5b0a11 make deterministic date cleaner (#5777)
* make deterministic date cleaner

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

* update all the steps

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

* more steps

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

* steps

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-03-13 18:01:47 -07:00
24d0b14668 change runner (#5800)
Revert "try codspeed runner (#5796)"

This reverts commit ec64daa01f.
2025-03-13 13:57:24 -07:00
6fb32eeff2 fix python bindings building on linux (#5795)
* updates

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

* updates;

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-03-13 12:05:45 -07:00
ec64daa01f try codspeed runner (#5796)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-03-13 12:05:15 -07:00
e8886bb358 hide program memory source ranges from the app (#5759)
* hide program memory source ranges from the app

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

fix unit tests

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

fix memory

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

remove from paths

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

updates

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

fixes

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

updates

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

updates

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

updates

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

updates

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

updates

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

updates

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

* fix program memory source ranges

* updates

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

* updates

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

* updates

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

* updates

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
2025-03-13 11:13:33 -07:00
05a6313d97 try execute bench with walltime (#5772)
* try execute bench

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

* timeout longer

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

* timeout longer

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

* updates

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

* updates

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-03-13 10:33:00 -07:00
80f78e1c61 Revolve/Sweep multiple sketches at once (#5779)
* revolve multiple sketches at once

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

* updates

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

* updates

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

* updates

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

* updates

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

* do the same for swweep

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

* updates

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

* do the same for swweep

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-03-13 09:38:22 -07:00
c441a3ab1c Fix playwright mental model, don't make me psycho, thanks (#5680)
* Fix flakey tests with new toolbar.exitSketch

* tsc && lint && fmt

* Disable pw electron thing again

* Unfrig Playwright-Electron a ton; fix another ton of flakes.

* More deflaky

* Fix a ton of tests and playwright related hell

* Run jess's magic incantation to build rust kcl things

* yarn tsc

* yarn lint

* yarn fmt

* Remove double logs

* Revert to old settings spreads momentarily

* Expect error *in the fixtureSetup*, does not circumvent typechecking for regular usage

* Fix unit tests
2025-03-13 10:54:00 -04:00
e894242768 Add warning when using a module with no return value (#5771)
* Add warning when using a module with no return value

* Update output files since changing source range of the pipeline argument

* Change wording of error message to not use the term unlabeled
2025-03-12 18:05:18 +00:00
d8dff03746 Add cache for wasm in build-apps (#5758)
* WIP: break out e2e snapshots

* Separate prepare-wasm job

* Force e2e to run on pierremtb/adhoc/break-out-snapshots

* Quick fix

* Quick fix2

* Quick fix3

* Quick fix4

* Quick fix5

* Remove ifs for flow tests

* Clean up for review

* Cache wasm on PR for build-apps

* Add checks on build wasm tings

* Clean up for review
2025-03-12 13:53:30 -04:00
60aee7ddba Bump the security group with 2 updates (#5773)
Bumps the security group with 2 updates: [@babel/helpers](https://github.com/babel/babel/tree/HEAD/packages/babel-helpers) and [@babel/runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-runtime).


Updates `@babel/helpers` from 7.25.0 to 7.26.10
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.26.10/packages/babel-helpers)

Updates `@babel/runtime` from 7.25.0 to 7.26.10
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.26.10/packages/babel-runtime)

---
updated-dependencies:
- dependency-name: "@babel/helpers"
  dependency-type: indirect
  dependency-group: security
- dependency-name: "@babel/runtime"
  dependency-type: indirect
  dependency-group: security
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-12 10:03:14 -07:00
618 changed files with 154064 additions and 178574 deletions

View File

@ -33,26 +33,63 @@ jobs:
- run: yarn install
- id: filter
name: Check for Rust changes
uses: dorny/paths-filter@v3
with:
filters: |
rust:
- 'rust/**'
- name: Download Wasm Cache
id: download-wasm
if: ${{ github.event_name == 'pull_request' && steps.filter.outputs.rust == 'false' }}
uses: dawidd6/action-download-artifact@v7
continue-on-error: true
with:
github_token: ${{secrets.GITHUB_TOKEN}}
name: wasm-bundle
workflow: build-and-store-wasm.yml
branch: main
path: rust/kcl-wasm-lib/pkg
- name: Build WASM condition
id: wasm
run: |
set -euox pipefail
# Build wasm if this is a push to main or tag, there are Rust changes, or
# downloading from the wasm cache failed.
if [[ ${{github.event_name}} == 'push' || ${{steps.filter.outputs.rust}} == 'true' || ${{steps.download-wasm.outcome}} == 'failure' ]]; then
echo "should-build-wasm=true" >> $GITHUB_OUTPUT
else
echo "should-build-wasm=false" >> $GITHUB_OUTPUT
fi
- name: Use correct Rust toolchain
if: ${{ steps.wasm.outputs.should-build-wasm == 'true' }}
shell: bash
run: |
[ -e rust-toolchain.toml ] || cp rust/rust-toolchain.toml ./
- name: Install rust
if: ${{ steps.wasm.outputs.should-build-wasm == 'true' }}
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache: false # Configured below.
# TODO: see if we can fetch from main instead if no diff at rust
- uses: taiki-e/install-action@955a6ff1416eae278c9f833008a9beb4b7f9afe3
if: ${{ steps.wasm.outputs.should-build-wasm == 'true' }}
with:
tool: wasm-pack
- name: Rust Cache
if: ${{ steps.wasm.outputs.should-build-wasm == 'true' }}
uses: Swatinem/rust-cache@v2
with:
workspaces: rust
- name: Run build:wasm
if: ${{ steps.wasm.outputs.should-build-wasm == 'true' }}
run: "yarn build:wasm"
- name: Set nightly version, product name, release notes, and icons

View File

@ -50,12 +50,13 @@ jobs:
- name: Build the benchmark target(s)
run: |
cd rust
cargo codspeed build
cargo codspeed build --measurement-mode walltime
- name: Run the benchmarks
uses: CodSpeedHQ/action@v3
with:
working-directory: rust
run: cargo codspeed run
token: ${{ secrets.CODSPEED_TOKEN }}
mode: walltime
env:
KITTYCAD_API_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN }}

3
.gitignore vendored
View File

@ -53,13 +53,14 @@ e2e/playwright/export-snapshots/*
/public/kcl-samples.zip
/public/kcl-samples/.github
/public/kcl-samples/screenshots/main.kcl
/public/kcl-samples/step/main.kcl
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/src/lang/std/artifactMapCache
## generated files
src/**/*.typegen.ts

File diff suppressed because one or more lines are too long

View File

@ -6,7 +6,7 @@ layout: manual
Extend a 2-dimensional sketch through a third dimension in order to create new 3-dimensional volume, or if extruded into an existing volume, cut into an existing solid.
You can provide more than one sketch to extrude, and they will all be extruded in the same direction.
```js
extrude(
@ -20,7 +20,7 @@ extrude(
| Name | Type | Description | Required |
|----------|------|-------------|----------|
| `sketchSet` | [`SketchSet`](/docs/kcl/types/SketchSet) | Which sketches should be extruded | Yes |
| `sketchSet` | [`SketchSet`](/docs/kcl/types/SketchSet) | Which sketch or set of sketches should be extruded | Yes |
| `length` | [`number`](/docs/kcl/types/number) | How far to extrude the given sketches | Yes |
### Returns

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

View File

@ -24,6 +24,5 @@ A face.
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
| `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A unit of length. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |

View File

@ -22,6 +22,5 @@ A helix.
| `angleStart` |[`number`](/docs/kcl/types/number)| Start angle (in degrees). | No |
| `ccw` |`boolean`| Is the helix rotation counter clockwise? | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A unit of length. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |

View File

@ -22,6 +22,5 @@ A helix.
| `angleStart` |[`number`](/docs/kcl/types/number)| Start angle (in degrees). | No |
| `ccw` |`boolean`| Is the helix rotation counter clockwise? | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A unit of length. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |

View File

@ -18,6 +18,5 @@ Data for an imported geometry.
|----------|------|-------------|----------|
| `id` |[`string`](/docs/kcl/types/string)| The ID of the imported geometry. | No |
| `value` |`[` [`string`](/docs/kcl/types/string) `]`| The original file paths. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |

View File

@ -25,7 +25,6 @@ Any KCL value.
|----------|------|-------------|----------|
| `type` |enum: `Uuid`| | No |
| `value` |[`string`](/docs/kcl/types/string)| | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
@ -42,7 +41,6 @@ Any KCL value.
|----------|------|-------------|----------|
| `type` |enum: `Bool`| | No |
| `value` |`boolean`| | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
@ -60,7 +58,6 @@ Any KCL value.
| `type` |enum: `Number`| | No |
| `value` |[`number`](/docs/kcl/types/number)| | No |
| `ty` |[`NumericType`](/docs/kcl/types/NumericType)| | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
@ -77,7 +74,6 @@ Any KCL value.
|----------|------|-------------|----------|
| `type` |enum: `String`| | No |
| `value` |[`string`](/docs/kcl/types/string)| | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
@ -94,7 +90,6 @@ Any KCL value.
|----------|------|-------------|----------|
| `type` |enum: `MixedArray`| | No |
| `value` |`[` [`KclValue`](/docs/kcl/types/KclValue) `]`| | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
@ -111,7 +106,6 @@ Any KCL value.
|----------|------|-------------|----------|
| `type` |enum: `Object`| | No |
| `value` |`object`| | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
@ -129,7 +123,6 @@ Any KCL value.
| `type` |enum: [`TagIdentifier`](/docs/kcl/types#tag-identifier)| | No |
| `value` |[`string`](/docs/kcl/types/string)| | No |
| `info` |[`TagEngineInfo`](/docs/kcl/types/TagEngineInfo)| | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
@ -279,7 +272,6 @@ Data for an imported geometry.
| `type` |enum: [`ImportedGeometry`](/docs/kcl/types/ImportedGeometry)| | No |
| `id` |[`string`](/docs/kcl/types/string)| The ID of the imported geometry. | No |
| `value` |`[` [`string`](/docs/kcl/types/string) `]`| The original file paths. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
@ -295,7 +287,6 @@ Data for an imported geometry.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Function`| | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
@ -312,7 +303,6 @@ Data for an imported geometry.
|----------|------|-------------|----------|
| `type` |enum: `Module`| | No |
| `value` |[`ModuleId`](/docs/kcl/types/ModuleId)| Identifier of a source file. Uses a u32 to keep the size small. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
@ -328,7 +318,6 @@ Data for an imported geometry.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Type`| | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
@ -345,7 +334,6 @@ Data for an imported geometry.
|----------|------|-------------|----------|
| `type` |enum: [`KclNone`](/docs/kcl/types/KclNone)| | No |
| `value` |[`KclNone`](/docs/kcl/types/KclNone)| KCL value for an optional parameter which was not given an argument. (remember, parameters are in the function declaration, arguments are in the function call/application). | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
@ -362,7 +350,6 @@ Data for an imported geometry.
|----------|------|-------------|----------|
| `type` |enum: `Tombstone`| | No |
| `value` |`null`| | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----

View File

@ -32,7 +32,6 @@ A sketch or a group of sketches.
| `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`](/docs/kcl/types/string)| | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A unit of length. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |
----

View File

@ -32,7 +32,6 @@ A sketch type.
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane's Y axis be? | No |
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A unit of length. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
@ -57,7 +56,6 @@ A face.
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
| `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A unit of length. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----

View File

@ -33,7 +33,6 @@ Data for a solid or an imported geometry.
| `endCapId` |[`string`](/docs/kcl/types/string)| The id of the extrusion end cap | No |
| `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A unit of length. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |
----
@ -52,7 +51,21 @@ Data for an imported geometry.
| `type` |enum: `importedGeometry`| | No |
| `id` |[`string`](/docs/kcl/types/string)| The ID of the imported geometry. | No |
| `value` |`[` [`string`](/docs/kcl/types/string) `]`| The original file paths. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
**Type:** `[object, array]`
`[` [`Solid`](/docs/kcl/types/Solid) `]`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `solidSet`| | No |
----

View File

@ -33,7 +33,6 @@ A solid or a group of solids.
| `endCapId` |[`string`](/docs/kcl/types/string)| The id of the extrusion end cap | No |
| `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A unit of length. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |
----

View File

@ -1,4 +1,5 @@
import { test, expect, Page } from './zoo-test'
import { Page } from '@playwright/test'
import { test, expect } from './zoo-test'
import {
getUtils,
TEST_COLORS,

View File

@ -1,4 +1,5 @@
import { test, expect, Page } from './zoo-test'
import { Page } from '@playwright/test'
import { test, expect } from './zoo-test'
import { HomePageFixture } from './fixtures/homePageFixture'
import { getUtils } from './test-utils'
import { EngineCommand } from 'lang/std/artifactGraph'

View File

@ -10,7 +10,11 @@ import fsp from 'fs/promises'
test(
'export works on the first try',
{ tag: ['@electron', '@skipLocalEngine'] },
async ({ page, context, scene }, testInfo) => {
async ({ page, context, scene, tronApp }, testInfo) => {
if (!tronApp) {
fail()
}
await context.folderSetupFn(async (dir) => {
const bracketDir = path.join(dir, 'bracket')
await Promise.all([fsp.mkdir(bracketDir, { recursive: true })])
@ -86,7 +90,7 @@ test(
await expect(exportingToastMessage).not.toBeVisible()
const firstFileFullPath = path.resolve(
getPlaywrightDownloadDir(page),
getPlaywrightDownloadDir(tronApp.projectDirName),
exportFileName
)
await test.step('Check the export size', async () => {
@ -165,7 +169,7 @@ test(
]))
const secondFileFullPath = path.resolve(
getPlaywrightDownloadDir(page),
getPlaywrightDownloadDir(tronApp.projectDirName),
exportFileName
)
await test.step('Check the export size', async () => {
@ -181,7 +185,7 @@ test(
},
{ timeout: 15_000 }
)
.toBeGreaterThan(100_000)
.toBeGreaterThan(70_000)
})
})
}

View File

@ -158,11 +158,14 @@ test.describe('when using the file tree to', () => {
await createNewFile('lee')
await test.step('Postcondition: there are 5 new lee-*.kcl files', async () => {
await expect(
page
.locator('[data-testid="file-pane-scroll-container"] button')
.filter({ hasText: /lee[-]?[0-5]?/ })
).toHaveCount(5)
await expect
.poll(() =>
page
.locator('[data-testid="file-pane-scroll-container"] button')
.filter({ hasText: /lee[-]?[0-5]?/ })
.count()
)
.toEqual(5)
})
}
)

View File

@ -27,28 +27,19 @@ type CmdBarSerialised =
export class CmdBarFixture {
public page: Page
get cmdBarOpenBtn() {
return this.page.getByTestId('command-bar-open-button')
}
get cmdBarElement() {
return this.page.getByTestId('command-bar')
}
public cmdBarOpenBtn!: Locator
public cmdBarElement!: Locator
constructor(page: Page) {
this.page = page
this.cmdBarOpenBtn = this.page.getByTestId('command-bar-open-button')
this.cmdBarElement = this.page.getByTestId('command-bar')
}
get currentArgumentInput() {
return this.page.getByTestId('cmd-bar-arg-value')
}
// Put all selectors here because this method is re-run on fixture creation.
reConstruct = (page: Page) => {
this.page = page
}
private _serialiseCmdBar = async (): Promise<CmdBarSerialised> => {
if (!(await this.page.getByTestId('command-bar-wrapper').isVisible())) {
return { stage: 'commandBarClosed' }

View File

@ -24,11 +24,6 @@ export class EditorFixture {
constructor(page: Page) {
this.page = page
this.reConstruct(page)
}
reConstruct = (page: Page) => {
this.page = page
this.codeContent = page.locator('.cm-content[data-language="kcl"]')
this.diagnosticsTooltip = page.locator('.cm-tooltip-lint')
this.diagnosticsGutterIcon = page.locator('.cm-lint-marker-error')
@ -87,6 +82,30 @@ export class EditorFixture {
toContain: this._expectEditorToContain(),
not: { toContain: this._expectEditorToContain(true) },
}
snapshot = async (options?: { timeout?: number; name?: string }) => {
const wasPaneOpen = await this.checkIfPaneIsOpen()
if (!wasPaneOpen) {
await this.openPane()
}
try {
// Use expect.poll to implement retry logic
await expect
.poll(
async () => {
const code = await this.codeContent.textContent()
return code || ''
},
{ timeout: options?.timeout || 5000 }
)
.toMatchSnapshot(options?.name || 'editor-content')
} finally {
// Reset pane state if needed
if (!wasPaneOpen) {
await this.closePane()
}
}
}
private _serialiseDiagnostics = async (): Promise<Array<string>> => {
const diagnostics = await this.diagnosticsGutterIcon.all()
const diagnosticsContent: string[] = []

View File

@ -1,13 +1,31 @@
/* eslint-disable react-hooks/rules-of-hooks */
import type {
BrowserContext,
ElectronApplication,
Fixtures as PlaywrightFixtures,
TestInfo,
Page,
} from '@playwright/test'
import { getUtils, setup, setupElectron } from '../test-utils'
import {
_electron as electron,
PlaywrightTestArgs,
PlaywrightWorkerArgs,
} from '@playwright/test'
import * as TOML from '@iarna/toml'
import {
TEST_SETTINGS_KEY,
TEST_SETTINGS_CORRUPTED,
TEST_SETTINGS,
TEST_SETTINGS_DEFAULT_THEME,
} from '../storageStates'
import { SETTINGS_FILE_NAME, PROJECT_SETTINGS_FILE_NAME } from 'lib/constants'
import { getUtils, setup } from '../test-utils'
import fsp from 'fs/promises'
import { join } from 'path'
import fs from 'node:fs'
import path from 'path'
import { CmdBarFixture } from './cmdBarFixture'
import { EditorFixture } from './editorFixture'
import { ToolbarFixture } from './toolbarFixture'
@ -23,7 +41,7 @@ export class AuthenticatedApp {
public readonly testInfo: TestInfo
public readonly viewPortSize = { width: 1200, height: 500 }
public electronApp: undefined | ElectronApplication
public dir: string = ''
public projectDirName: string = ''
constructor(context: BrowserContext, page: Page, testInfo: TestInfo) {
this.context = context
@ -46,7 +64,7 @@ export class AuthenticatedApp {
}
getInputFile = (fileName: string) => {
return fsp.readFile(
join('rust', 'kcl-lib', 'e2e', 'executor', 'inputs', fileName),
path.join('rust', 'kcl-lib', 'e2e', 'executor', 'inputs', fileName),
'utf-8'
)
}
@ -59,101 +77,326 @@ export interface Fixtures {
scene: SceneFixture
homePage: HomePageFixture
}
export class AuthenticatedTronApp {
public originalPage: Page
public page: Page
public browserContext: BrowserContext
public context: BrowserContext
public readonly testInfo: TestInfo
public electronApp: ElectronApplication | undefined
public readonly viewPortSize = { width: 1200, height: 500 }
public dir: string = ''
constructor(
browserContext: BrowserContext,
originalPage: Page,
testInfo: TestInfo
) {
this.page = originalPage
this.originalPage = originalPage
this.browserContext = browserContext
// Will be overwritten in the initializer
this.context = browserContext
this.testInfo = testInfo
}
async initialise(
arg: {
fixtures: Partial<Fixtures>
folderSetupFn?: (projectDirName: string) => Promise<void>
cleanProjectDir?: boolean
appSettings?: DeepPartial<Settings>
} = { fixtures: {} }
) {
const { electronApp, page, context, dir } = await setupElectron({
testInfo: this.testInfo,
folderSetupFn: arg.folderSetupFn,
cleanProjectDir: arg.cleanProjectDir,
appSettings: arg.appSettings,
viewport: this.viewPortSize,
export class ElectronZoo {
public available: boolean = true
public electron!: ElectronApplication
public firstUrl = ''
public viewPortSize = { width: 1200, height: 500 }
public projectDirName = ''
public page!: Page
public context!: BrowserContext
constructor() {}
// Help remote end by signaling we're done with the connection.
// If it takes longer than 10s to stop, just resolve.
async makeAvailableAgain() {
await this.page.evaluate(async () => {
return new Promise((resolve) => {
if (!window.engineCommandManager.engineConnection?.state?.type) {
return resolve(undefined)
}
window.engineCommandManager.tearDown()
// Keep polling (per js event tick) until state is Disconnected.
const timeA = Date.now()
const checkDisconnected = () => {
// It's possible we never even created an engineConnection
// e.g. never left Projects view.
if (
window.engineCommandManager?.engineConnection?.state.type ===
'disconnected'
) {
return resolve(undefined)
}
if (Date.now() - timeA > 10000) {
return resolve(undefined)
}
setTimeout(checkDisconnected, 0)
}
checkDisconnected()
})
})
this.page = page
// These assignments "fix" some brokenness in the Playwright Workbench when
// running against electron applications.
// The timeline is still broken but failure screenshots work again.
this.context = context
// TODO: try to get this to work again for screenshots, but it messed with test ends when enabled
// Object.assign(this.browserContext, this.context)
await this.context.tracing.stopChunk({ path: 'trace.zip' })
this.electronApp = electronApp
this.dir = dir
// Only after cleanup we're ready.
this.available = true
}
// Easier to access throughout utils
this.page.dir = dir
async createInstanceIfMissing(testInfo: TestInfo) {
// Create or otherwise clear the folder.
this.projectDirName = testInfo.outputPath('electron-test-projects-dir')
// Setup localStorage, addCookies, reload
await setup(this.context, this.page, this.testInfo)
// We need to expose this in order for some tests that require folder
// creation and some code below.
const that = this
for (const key of unsafeTypedKeys(arg.fixtures)) {
const fixture = arg.fixtures[key]
if (
!fixture ||
fixture instanceof AuthenticatedApp ||
fixture instanceof AuthenticatedTronApp
)
continue
fixture.reConstruct(page)
const options = {
timeout: 120000,
args: ['.', '--no-sandbox'],
env: {
...process.env,
TEST_SETTINGS_FILE_KEY: this.projectDirName,
IS_PLAYWRIGHT: 'true',
},
...(process.env.ELECTRON_OVERRIDE_DIST_PATH
? {
executablePath:
process.env.ELECTRON_OVERRIDE_DIST_PATH + 'electron',
}
: {}),
...(process.env.PLAYWRIGHT_RECORD_VIDEO
? {
recordVideo: {
dir: testInfo.snapshotPath(),
size: this.viewPortSize,
},
}
: {}),
}
// Do this once and then reuse window on subsequent calls.
if (!this.electron) {
this.electron = await electron.launch(options)
// Mac takes quite a long time to create the first window in CI.
// Turns out we can't trust firstWindow() either. So loop.
let timeoutId: ReturnType<typeof setTimeout>
const tryToGetWindowPage = () =>
new Promise((resolve) => {
const fn = () => {
this.page = this.electron.windows()[0]
timeoutId = setTimeout(() => {
if (this.page) {
clearTimeout(timeoutId)
return resolve(undefined)
}
fn()
}, 0)
}
fn()
})
await tryToGetWindowPage()
this.context = this.electron.context()
await this.context.tracing.start({ screenshots: true, snapshots: true })
}
await this.context.tracing.startChunk()
await setup(this.context, this.page, testInfo)
await this.cleanProjectDir()
// Create a consistent way to resize the page across electron and web.
// (lee) I had to do everything in the book to make electron change its
// damn window size. I succeeded in making it consistently and reliably
// do it after a whole afternoon.
this.page.setBodyDimensions = async function (dims: {
width: number
height: number
}) {
await this.setViewportSize(dims)
await that.electron?.evaluateHandle(async ({ app }, dims) => {
// @ts-ignore sorry jon but see comment in main.ts why this is ignored
await app.resizeWindow(dims.width, dims.height)
}, dims)
return this.evaluate(async (dims: { width: number; height: number }) => {
await window.electron.resizeWindow(dims.width, dims.height)
window.document.body.style.width = dims.width + 'px'
window.document.body.style.height = dims.height + 'px'
window.document.documentElement.style.width = dims.width + 'px'
window.document.documentElement.style.height = dims.height + 'px'
}, dims)
}
await this.page.setBodyDimensions(this.viewPortSize)
this.context.folderSetupFn = async function (fn) {
return fn(that.projectDirName)
.then(() => that.page.reload())
.then(() => ({
dir: that.projectDirName,
}))
}
// We need to patch this because addInitScript will bind too late in our
// electron tests, never running. We need to call reload() after each call
// to guarantee it runs.
const oldContextAddInitScript = this.context.addInitScript
this.context.addInitScript = async function (a, b) {
// @ts-ignore pretty sure way out of tsc's type checking capabilities.
// This code works perfectly fine.
await oldContextAddInitScript.apply(this, [a, b])
await that.page.reload()
}
// No idea why we mix and match page and context's addInitScript but we do
const oldPageAddInitScript = this.page.addInitScript
this.page.addInitScript = async function (a: any, b: any) {
// @ts-ignore pretty sure way out of tsc's type checking capabilities.
// This code works perfectly fine.
await oldPageAddInitScript.apply(this, [a, b])
await that.page.reload()
}
if (!this.firstUrl) {
await this.page.getByText('Your Projects').count()
this.firstUrl = this.page.url()
}
// Due to the app controlling its own window context we need to inject new
// options and context here.
// NOTE TO LEE: Seems to destroy page context when calling an electron loadURL.
// await tronApp.electronApp.evaluate(({ app }) => {
// return app.reuseWindowForTest();
// });
await this.electron?.evaluate(({ app }, projectDirName) => {
// @ts-ignore can't declaration merge see main.ts
app.testProperty['TEST_SETTINGS_FILE_KEY'] = projectDirName
}, this.projectDirName)
// Always start at the root view
await this.page.goto(this.firstUrl)
// Force a hard reload, destroying the stream and other state
await this.page.reload()
}
close = async () => {
await this.electronApp?.close?.()
async cleanProjectDir(appSettings?: DeepPartial<Settings>) {
try {
if (fs.existsSync(this.projectDirName)) {
await fsp.rm(this.projectDirName, { recursive: true })
}
} catch (e) {
console.error(e)
}
try {
await fsp.mkdir(this.projectDirName)
} catch (e) {
// Not a problem if it already exists.
}
const tempSettingsFilePath = path.join(
this.projectDirName,
SETTINGS_FILE_NAME
)
let settingsOverridesToml = ''
if (appSettings) {
settingsOverridesToml = TOML.stringify({
// @ts-expect-error
settings: {
...TEST_SETTINGS,
...appSettings,
app: {
...TEST_SETTINGS.app,
project_directory: this.projectDirName,
...appSettings.app,
},
},
})
} else {
settingsOverridesToml = TOML.stringify({
// @ts-expect-error
settings: {
...TEST_SETTINGS,
app: {
...TEST_SETTINGS.app,
project_directory: this.projectDirName,
},
},
})
}
await fsp.writeFile(tempSettingsFilePath, settingsOverridesToml)
}
debugPause = () =>
new Promise(() => {
console.log('UN-RESOLVING PROMISE')
})
}
export const fixtures = {
cmdBar: async ({ page }: { page: Page }, use: any) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
// If yee encounter this, please try to type it.
type FnUse = any
const fixturesForElectron = {
page: async (
{ tronApp }: { tronApp: ElectronZoo },
use: FnUse,
testInfo: TestInfo
) => {
await use(tronApp.page)
},
context: async (
{ tronApp }: { tronApp: ElectronZoo },
use: FnUse,
testInfo: TestInfo
) => {
await use(tronApp.context)
},
}
const fixturesForWeb = {
page: async (
{ page, context }: { page: Page; context: BrowserContext },
use: FnUse,
testInfo: TestInfo
) => {
page.setBodyDimensions = page.setViewportSize
// We do the same thing in ElectronZoo. addInitScript simply doesn't fire
// at the correct time, so we reload the page and it fires appropriately.
const oldPageAddInitScript = page.addInitScript
page.addInitScript = async function (...args) {
// @ts-expect-error
await oldPageAddInitScript.apply(this, args)
await page.reload()
}
const oldContextAddInitScript = context.addInitScript
context.addInitScript = async function (...args) {
// @ts-expect-error
await oldContextAddInitScript.apply(this, args)
await page.reload()
}
const webApp = new AuthenticatedApp(context, page, testInfo)
await webApp.initialise()
await use(page)
},
}
const fixturesBasedOnProcessEnvPlatform = {
cmdBar: async ({ page }: { page: Page }, use: FnUse) => {
await use(new CmdBarFixture(page))
},
editor: async ({ page }: { page: Page }, use: any) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
editor: async ({ page }: { page: Page }, use: FnUse) => {
await use(new EditorFixture(page))
},
toolbar: async ({ page }: { page: Page }, use: any) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
toolbar: async ({ page }: { page: Page }, use: FnUse) => {
await use(new ToolbarFixture(page))
},
scene: async ({ page }: { page: Page }, use: any) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
scene: async ({ page }: { page: Page }, use: FnUse) => {
await use(new SceneFixture(page))
},
homePage: async ({ page }: { page: Page }, use: any) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
homePage: async ({ page }: { page: Page }, use: FnUse) => {
await use(new HomePageFixture(page))
},
}
if (process.env.PLATFORM === 'web') {
Object.assign(fixturesBasedOnProcessEnvPlatform, fixturesForWeb)
} else {
Object.assign(fixturesBasedOnProcessEnvPlatform, fixturesForElectron)
}
export { fixturesBasedOnProcessEnvPlatform }

View File

@ -27,10 +27,6 @@ export class HomePageFixture {
constructor(page: Page) {
this.page = page
this.reConstruct(page)
}
reConstruct = (page: Page) => {
this.page = page
this.projectSection = this.page.getByTestId('home-section')
@ -96,8 +92,12 @@ export class HomePageFixture {
}
}
createAndGoToProject = async (projectTitle = 'project-$nnn') => {
projectsLoaded = async () => {
await expect(this.projectSection).not.toHaveText('Loading your Projects...')
}
createAndGoToProject = async (projectTitle = 'project-$nnn') => {
await this.projectsLoaded()
await this.projectButtonNew.click()
await this.projectTextName.click()
await this.projectTextName.fill(projectTitle)

View File

@ -53,7 +53,12 @@ export class SceneFixture {
constructor(page: Page) {
this.page = page
this.reConstruct(page)
this.streamWrapper = page.getByTestId('stream')
this.networkToggleConnected = page.getByTestId('network-toggle-ok')
this.loadingIndicator = this.streamWrapper.getByTestId('loading')
this.startEditSketchBtn = page
.getByRole('button', { name: 'Start Sketch' })
.or(page.getByRole('button', { name: 'Edit Sketch' }))
}
private _serialiseScene = async (): Promise<SceneSerialised> => {
const camera = await this.getCameraInfo()
@ -72,17 +77,6 @@ export class SceneFixture {
.toEqual(expected)
}
reConstruct = (page: Page) => {
this.page = page
this.streamWrapper = page.getByTestId('stream')
this.networkToggleConnected = page.getByTestId('network-toggle-ok')
this.loadingIndicator = this.streamWrapper.getByTestId('loading')
this.startEditSketchBtn = page
.getByRole('button', { name: 'Start Sketch' })
.or(page.getByRole('button', { name: 'Edit Sketch' }))
}
makeMouseHelpers = (
x: number,
y: number,
@ -253,7 +247,7 @@ export class SceneFixture {
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.clearAndCloseDebugPanel()
await u.closeDebugPanel()
await this.waitForExecutionDone()
await expect(this.startEditSketchBtn).not.toBeDisabled()

View File

@ -37,13 +37,12 @@ export class ToolbarFixture {
featureTreeId = 'feature-tree' as const
/** The pane element for the Feature Tree */
featureTreePane!: Locator
gizmo!: Locator
gizmoDisabled!: Locator
constructor(page: Page) {
this.page = page
this.reConstruct(page)
}
reConstruct = (page: Page) => {
this.page = page
this.extrudeButton = page.getByTestId('extrude')
this.loftButton = page.getByTestId('loft')
this.sweepButton = page.getByTestId('sweep')
@ -67,6 +66,13 @@ export class ToolbarFixture {
this.filePane = page.locator('#files-pane')
this.featureTreePane = page.locator('#feature-tree-pane')
this.fileCreateToast = page.getByText('Successfully created')
// Note to test writers: having two locators like this is preferable to one
// which changes another el property because it means our test "signal" is
// completely decoupled from the elements themselves. It means the same
// element or two different elements can represent these states.
this.gizmo = page.getByTestId('gizmo')
this.gizmoDisabled = page.getByTestId('gizmo-disabled')
}
get editSketchBtn() {
@ -86,6 +92,18 @@ export class ToolbarFixture {
startSketchPlaneSelection = async () =>
doAndWaitForImageDiff(this.page, () => this.startSketchBtn.click(), 500)
waitUntilSketchingReady = async () => {
await expect(this.gizmoDisabled).toBeVisible()
}
startSketchThenCallbackThenWaitUntilReady = async (
cb: () => Promise<void>
) => {
await this.startSketchBtn.click()
await cb()
await this.waitUntilSketchingReady()
}
exitSketch = async () => {
await this.exitSketchBtn.click()
await expect(

View File

@ -21,58 +21,54 @@ import { expectPixelColor } from './fixtures/sceneFixture'
// we must set it to empty for the tests where we want to see the onboarding immediately.
test.describe('Onboarding tests', () => {
test(
'Onboarding code is shown in the editor',
{
appSettings: {
app: {
onboarding_status: '',
},
},
cleanProjectDir: true,
},
async ({ page, homePage }) => {
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
// Test that the onboarding pane loaded
await expect(
page.getByText('Welcome to Modeling App! This')
).toBeVisible()
// Test that the onboarding pane loaded
await expect(
page.getByText('Welcome to Modeling App! This')
).toBeVisible()
// *and* that the code is shown in the editor
await expect(page.locator('.cm-content')).toContainText(
'// Shelf Bracket'
)
// Make sure the model loaded
const XYPlanePoint = { x: 774, y: 116 } as const
const modelColor: [number, number, number] = [45, 45, 45]
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
expect(await u.getGreatestPixDiff(XYPlanePoint, modelColor)).toBeLessThan(
8
)
test('Onboarding code is shown in the editor', async ({
page,
homePage,
tronApp,
}) => {
if (!tronApp) {
fail()
}
)
await tronApp.cleanProjectDir({
app: {
onboarding_status: '',
},
})
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
// Test that the onboarding pane loaded
await expect(page.getByText('Welcome to Modeling App! This')).toBeVisible()
// Test that the onboarding pane loaded
await expect(page.getByText('Welcome to Modeling App! This')).toBeVisible()
// *and* that the code is shown in the editor
await expect(page.locator('.cm-content')).toContainText('// Shelf Bracket')
// Make sure the model loaded
const XYPlanePoint = { x: 774, y: 116 } as const
const modelColor: [number, number, number] = [45, 45, 45]
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
expect(await u.getGreatestPixDiff(XYPlanePoint, modelColor)).toBeLessThan(8)
})
test(
'Desktop: fresh onboarding executes and loads',
{
tag: '@electron',
appSettings: {
},
async ({ page, tronApp }) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: {
onboarding_status: '',
},
},
cleanProjectDir: true,
},
async ({ page }) => {
})
const u = await getUtils(page)
const viewportSize = { width: 1200, height: 500 }
@ -107,223 +103,235 @@ test.describe('Onboarding tests', () => {
}
)
test(
'Code resets after confirmation',
{
cleanProjectDir: true,
},
async ({ context, page, homePage }) => {
const initialCode = `sketch001 = startSketchOn('XZ')`
test('Code resets after confirmation', async ({
context,
page,
homePage,
tronApp,
scene,
cmdBar,
}) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir()
// Load the page up with some code so we see the confirmation warning
// when we go to replay onboarding
await context.addInitScript((code) => {
localStorage.setItem('persistCode', code)
}, initialCode)
const initialCode = `sketch001 = startSketchOn('XZ')`
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
// Load the page up with some code so we see the confirmation warning
// when we go to replay onboarding
await page.addInitScript((code) => {
localStorage.setItem('persistCode', code)
}, initialCode)
// Replay the onboarding
await page.getByRole('link', { name: 'Settings' }).last().click()
const replayButton = page.getByRole('button', {
name: 'Replay onboarding',
})
await expect(replayButton).toBeVisible()
await replayButton.click()
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await scene.connectionEstablished()
// Ensure we see the warning, and that the code has not yet updated
await expect(page.getByText('Would you like to create')).toBeVisible()
await expect(page.locator('.cm-content')).toHaveText(initialCode)
// Replay the onboarding
await page.getByRole('link', { name: 'Settings' }).last().click()
const replayButton = page.getByRole('button', {
name: 'Replay onboarding',
})
await expect(replayButton).toBeVisible()
await replayButton.click()
const nextButton = page.getByTestId('onboarding-next')
// Ensure we see the warning, and that the code has not yet updated
await expect(page.getByText('Would you like to create')).toBeVisible()
await expect(page.locator('.cm-content')).toHaveText(initialCode)
const nextButton = page.getByTestId('onboarding-next')
await nextButton.hover()
await nextButton.click()
// Ensure we see the introduction and that the code has been reset
await expect(page.getByText('Welcome to Modeling App!')).toBeVisible()
await expect(page.locator('.cm-content')).toContainText('// Shelf Bracket')
// There used to be old code here that checked if we stored the reset
// code into localStorage but that isn't the case on desktop. It gets
// saved to the file system, which we have other tests for.
})
test('Click through each onboarding step and back', async ({
context,
page,
homePage,
tronApp,
}) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: {
onboarding_status: '',
},
})
// Override beforeEach test setup
await context.addInitScript(
async ({ settingsKey, settings }) => {
// Give no initial code, so that the onboarding start is shown immediately
localStorage.setItem('persistCode', '')
localStorage.setItem(settingsKey, settings)
},
{
settingsKey: TEST_SETTINGS_KEY,
settings: settingsToToml({
settings: TEST_SETTINGS_ONBOARDING_START,
}),
}
)
await page.setBodyDimensions({ width: 1200, height: 1080 })
await homePage.goToModelingScene()
// Test that the onboarding pane loaded
await expect(page.getByText('Welcome to Modeling App! This')).toBeVisible()
const nextButton = page.getByTestId('onboarding-next')
const prevButton = page.getByTestId('onboarding-prev')
while ((await nextButton.innerText()) !== 'Finish') {
await nextButton.hover()
await nextButton.click()
// Ensure we see the introduction and that the code has been reset
await expect(page.getByText('Welcome to Modeling App!')).toBeVisible()
await expect(page.locator('.cm-content')).toContainText(
'// Shelf Bracket'
)
// There used to be old code here that checked if we stored the reset
// code into localStorage but that isn't the case on desktop. It gets
// saved to the file system, which we have other tests for.
}
)
test(
'Click through each onboarding step and back',
{
appSettings: {
app: {
onboarding_status: '',
},
},
},
async ({ context, page, homePage }) => {
// Override beforeEach test setup
await context.addInitScript(
async ({ settingsKey, settings }) => {
// Give no initial code, so that the onboarding start is shown immediately
localStorage.setItem('persistCode', '')
localStorage.setItem(settingsKey, settings)
},
{
settingsKey: TEST_SETTINGS_KEY,
settings: settingsToToml({
settings: TEST_SETTINGS_ONBOARDING_START,
}),
}
)
await page.setBodyDimensions({ width: 1200, height: 1080 })
await homePage.goToModelingScene()
// Test that the onboarding pane loaded
await expect(
page.getByText('Welcome to Modeling App! This')
).toBeVisible()
const nextButton = page.getByTestId('onboarding-next')
const prevButton = page.getByTestId('onboarding-prev')
while ((await nextButton.innerText()) !== 'Finish') {
await nextButton.hover()
await nextButton.click()
}
while ((await prevButton.innerText()) !== 'Dismiss') {
await prevButton.hover()
await prevButton.click()
}
// Dismiss the onboarding
while ((await prevButton.innerText()) !== 'Dismiss') {
await prevButton.hover()
await prevButton.click()
// Test that the onboarding pane is gone
await expect(page.getByTestId('onboarding-content')).not.toBeVisible()
await expect.poll(() => page.url()).not.toContain('/onboarding')
}
)
test(
'Onboarding redirects and code updating',
{
appSettings: {
app: {
onboarding_status: '/export',
},
// Dismiss the onboarding
await prevButton.hover()
await prevButton.click()
// Test that the onboarding pane is gone
await expect(page.getByTestId('onboarding-content')).not.toBeVisible()
await expect.poll(() => page.url()).not.toContain('/onboarding')
})
test('Onboarding redirects and code updating', async ({
context,
page,
homePage,
tronApp,
}) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: {
onboarding_status: '/export',
},
cleanProjectDir: true,
},
async ({ context, page, homePage }) => {
const originalCode = 'sigmaAllow = 15000'
})
// Override beforeEach test setup
await context.addInitScript(
async ({ settingsKey, settings }) => {
// Give some initial code, so we can test that it's cleared
localStorage.setItem('persistCode', originalCode)
localStorage.setItem(settingsKey, settings)
},
{
settingsKey: TEST_SETTINGS_KEY,
settings: settingsToToml({
settings: TEST_SETTINGS_ONBOARDING_EXPORT,
}),
}
)
const originalCode = 'sigmaAllow = 15000'
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
// Test that the redirect happened
await expect.poll(() => page.url()).toContain('/onboarding/export')
// Test that you come back to this page when you refresh
await page.reload()
await expect.poll(() => page.url()).toContain('/onboarding/export')
// Test that the code changes when you advance to the next step
await page.getByTestId('onboarding-next').hover()
await page.getByTestId('onboarding-next').click()
// Test that the onboarding pane loaded
const title = page.locator('[data-testid="onboarding-content"]')
await expect(title).toBeAttached()
await expect(page.locator('.cm-content')).not.toHaveText(originalCode)
// Test that the code is not empty when you click on the next step
await page.locator('[data-testid="onboarding-next"]').hover()
await page.locator('[data-testid="onboarding-next"]').click()
await expect(page.locator('.cm-content')).toHaveText(/.+/)
}
)
test(
'Onboarding code gets reset to demo on Interactive Numbers step',
{
appSettings: {
app: {
onboarding_status: '/parametric-modeling',
},
// Override beforeEach test setup
await context.addInitScript(
async ({ settingsKey, settings }) => {
// Give some initial code, so we can test that it's cleared
localStorage.setItem('persistCode', originalCode)
localStorage.setItem(settingsKey, settings)
},
cleanProjectDir: true,
},
{
settingsKey: TEST_SETTINGS_KEY,
settings: settingsToToml({
settings: TEST_SETTINGS_ONBOARDING_EXPORT,
}),
}
)
async ({ page, homePage }) => {
const u = await getUtils(page)
const badCode = `// This is bad code we shouldn't see`
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await page.setBodyDimensions({ width: 1200, height: 1080 })
await homePage.goToModelingScene()
// Test that the redirect happened
await expect.poll(() => page.url()).toContain('/onboarding/export')
await expect
.poll(() => page.url())
.toContain(onboardingPaths.PARAMETRIC_MODELING)
// Test that you come back to this page when you refresh
await page.reload()
await expect.poll(() => page.url()).toContain('/onboarding/export')
const bracketNoNewLines = bracket.replace(/\n/g, '')
// Test that the code changes when you advance to the next step
await page.getByTestId('onboarding-next').hover()
await page.getByTestId('onboarding-next').click()
// Check the code got reset on load
await expect(page.locator('#code-pane')).toBeVisible()
await expect(u.codeLocator).toHaveText(bracketNoNewLines, {
timeout: 10_000,
})
// Test that the onboarding pane loaded
const title = page.locator('[data-testid="onboarding-content"]')
await expect(title).toBeAttached()
// Mess with the code again
await u.codeLocator.selectText()
await u.codeLocator.fill(badCode)
await expect(u.codeLocator).toHaveText(badCode)
await expect(page.locator('.cm-content')).not.toHaveText(originalCode)
// Click to the next step
await page.locator('[data-testid="onboarding-next"]').hover()
await page.locator('[data-testid="onboarding-next"]').click()
await page.waitForURL('**' + onboardingPaths.INTERACTIVE_NUMBERS, {
waitUntil: 'domcontentloaded',
})
// Test that the code is not empty when you click on the next step
await page.locator('[data-testid="onboarding-next"]').hover()
await page.locator('[data-testid="onboarding-next"]').click()
await expect(page.locator('.cm-content')).toHaveText(/.+/)
})
// Check that the code has been reset
await expect(u.codeLocator).toHaveText(bracketNoNewLines)
test('Onboarding code gets reset to demo on Interactive Numbers step', async ({
page,
homePage,
tronApp,
}) => {
if (!tronApp) {
fail()
}
)
await tronApp.cleanProjectDir({
app: {
onboarding_status: '/parametric-modeling',
},
})
const u = await getUtils(page)
const badCode = `// This is bad code we shouldn't see`
await page.setBodyDimensions({ width: 1200, height: 1080 })
await homePage.goToModelingScene()
await expect
.poll(() => page.url())
.toContain(onboardingPaths.PARAMETRIC_MODELING)
const bracketNoNewLines = bracket.replace(/\n/g, '')
// Check the code got reset on load
await expect(page.locator('#code-pane')).toBeVisible()
await expect(u.codeLocator).toHaveText(bracketNoNewLines, {
timeout: 10_000,
})
// Mess with the code again
await u.codeLocator.selectText()
await u.codeLocator.fill(badCode)
await expect(u.codeLocator).toHaveText(badCode)
// Click to the next step
await page.locator('[data-testid="onboarding-next"]').hover()
await page.locator('[data-testid="onboarding-next"]').click()
await page.waitForURL('**' + onboardingPaths.INTERACTIVE_NUMBERS, {
waitUntil: 'domcontentloaded',
})
// Check that the code has been reset
await expect(u.codeLocator).toHaveText(bracketNoNewLines)
})
// (lee) The two avatar tests are weird because even on main, we don't have
// anything to do with the avatar inside the onboarding test. Due to the
// low impact of an avatar not showing I'm changing this to fixme.
test.fixme(
'Avatar text updates depending on image load success',
{
appSettings: {
async ({ context, page, homePage, tronApp }) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: {
onboarding_status: '',
},
},
cleanProjectDir: true,
},
async ({ context, page, homePage }) => {
})
// Override beforeEach test setup
await context.addInitScript(
async ({ settingsKey, settings }) => {
@ -388,15 +396,16 @@ test.describe('Onboarding tests', () => {
test.fixme(
"Avatar text doesn't mention avatar when no avatar",
{
appSettings: {
async ({ context, page, homePage, tronApp }) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: {
onboarding_status: '',
},
},
cleanProjectDir: true,
},
async ({ context, page, homePage }) => {
})
// Override beforeEach test setup
await context.addInitScript(
async ({ settingsKey, settings }) => {
@ -444,15 +453,17 @@ test.describe('Onboarding tests', () => {
test.fixme(
'Restarting onboarding on desktop takes one attempt',
{
appSettings: {
async ({ context, page, tronApp }) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: {
onboarding_status: 'dismissed',
},
},
cleanProjectDir: true,
},
async ({ context, page }) => {
})
await context.folderSetupFn(async (dir) => {
const routerTemplateDir = join(dir, 'router-template-slate')
await fsp.mkdir(routerTemplateDir, { recursive: true })

View File

@ -1,4 +1,5 @@
import { test, expect, Page } from './zoo-test'
import { Page } from '@playwright/test'
import { test, expect } from './zoo-test'
import { EditorFixture } from './fixtures/editorFixture'
import { SceneFixture } from './fixtures/sceneFixture'
import { ToolbarFixture } from './fixtures/toolbarFixture'
@ -1407,7 +1408,7 @@ sketch002 = startSketchOn('XZ')
})
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
await scene.expectPixelColor([135, 64, 73], testPoint, 15)
// await scene.expectPixelColor([135, 64, 73], testPoint, 15) // FIXME
await editor.expectEditor.toContain(sweepDeclaration)
await editor.expectState({
diagnostics: [],

View File

@ -163,7 +163,7 @@ test(
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
timeout: 10_000,
})
.toBeLessThan(15)
.toBeLessThan(20)
})
await test.step('Clicking the logo takes us back to the projects page / home', async () => {
@ -464,7 +464,11 @@ test.describe('Can export from electron app', () => {
test(
`Can export using ${method}`,
{ tag: ['@electron', '@skipLocalEngine'] },
async ({ context, page }, testInfo) => {
async ({ context, page, tronApp }, testInfo) => {
if (!tronApp) {
fail()
}
await context.folderSetupFn(async (dir) => {
const bracketDir = path.join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true })
@ -516,6 +520,7 @@ test.describe('Can export from electron app', () => {
storage: 'embedded',
presentation: 'pretty',
},
tronApp.projectDirName,
page,
method
)
@ -523,7 +528,7 @@ test.describe('Can export from electron app', () => {
})
const filepath = path.resolve(
getPlaywrightDownloadDir(page),
getPlaywrightDownloadDir(tronApp.projectDirName),
'main.gltf'
)
@ -781,6 +786,7 @@ test(
page.on('console', console.log)
await expect(page.getByText('router-template-slate')).toBeVisible()
await expect(page.getByText('Loading your Projects...')).not.toBeVisible()
await expect(page.getByText('Your Projects')).toBeVisible()
await page.keyboard.press('Delete')
@ -858,7 +864,7 @@ test.describe(`Project management commands`, () => {
test(
`Delete from project page`,
{ tag: '@electron' },
async ({ context, page }, testInfo) => {
async ({ context, page, scene, cmdBar }, testInfo) => {
const projectName = `my_project_to_delete`
await context.folderSetupFn(async (dir) => {
await fsp.mkdir(`${dir}/${projectName}`, { recursive: true })
@ -887,6 +893,8 @@ test.describe(`Project management commands`, () => {
await projectHomeLink.click()
await u.waitForPageLoad()
await scene.connectionEstablished()
await scene.settled(cmdBar)
})
await test.step(`Run delete command via command palette`, async () => {
@ -909,7 +917,7 @@ test.describe(`Project management commands`, () => {
test(
`Rename from home page`,
{ tag: '@electron' },
async ({ context, page }, testInfo) => {
async ({ context, page, homePage }, testInfo) => {
const projectName = `my_project_to_rename`
await context.folderSetupFn(async (dir) => {
await fsp.mkdir(`${dir}/${projectName}`, { recursive: true })
@ -936,6 +944,7 @@ test.describe(`Project management commands`, () => {
await test.step(`Setup`, async () => {
await page.setBodyDimensions({ width: 1200, height: 500 })
page.on('console', console.log)
await homePage.projectsLoaded()
await expect(projectHomeLink).toBeVisible()
})
@ -1682,7 +1691,11 @@ test(
test(
'You can change the root projects directory and nothing is lost',
{ tag: '@electron' },
async ({ context, page, electronApp }, testInfo) => {
async ({ context, page, tronApp, homePage }, testInfo) => {
if (!tronApp) {
fail()
}
await context.folderSetupFn(async (dir) => {
await Promise.all([
fsp.mkdir(`${dir}/router-template-slate`, { recursive: true }),
@ -1712,6 +1725,8 @@ test(
await fsp.rm(newProjectDirName, { recursive: true })
}
await homePage.projectsLoaded()
await test.step('We can change the root project directory', async () => {
// expect to see the project directory settings link
await expect(
@ -1725,7 +1740,7 @@ test(
.locator('section#projectDirectory input')
.inputValue()
const handleFile = electronApp?.evaluate(
const handleFile = tronApp.electron.evaluate(
async ({ dialog }, filePaths) => {
dialog.showOpenDialog = () =>
Promise.resolve({ canceled: false, filePaths })
@ -1741,6 +1756,8 @@ test(
await page.getByTestId('settings-close-button').click()
await homePage.projectsLoaded()
await expect(page.getByText('No Projects found')).toBeVisible()
await createProject({ name: 'project-000', page, returnHome: true })
await expect(
@ -1755,7 +1772,7 @@ test(
await page.getByTestId('project-directory-settings-link').click()
const handleFile = electronApp?.evaluate(
const handleFile = tronApp.electron.evaluate(
async ({ dialog }, filePaths) => {
dialog.showOpenDialog = () =>
Promise.resolve({ canceled: false, filePaths })
@ -1767,6 +1784,7 @@ test(
await page.getByTestId('project-directory-button').click()
await handleFile
await homePage.projectsLoaded()
await expect(page.locator('section#projectDirectory input')).toHaveValue(
originalProjectDirName
)
@ -2000,8 +2018,8 @@ test(
test(
'Settings persist across restarts',
{ tag: '@electron', cleanProjectDir: true },
async ({ page }, testInfo) => {
{ tag: '@electron' },
async ({ page, scene, cmdBar }, testInfo) => {
await test.step('We can change a user setting like theme', async () => {
await page.setBodyDimensions({ width: 1200, height: 500 })
@ -2014,6 +2032,10 @@ test(
await expect(page.getByTestId('app-theme')).toHaveValue('dark')
await page.getByTestId('app-theme').selectOption('light')
await expect(page.getByTestId('app-theme')).toHaveValue('light')
// Give time to system for writing to a persistent store
await page.waitForTimeout(1000)
})
await test.step('Starting the app again and we can see the same theme', async () => {

View File

@ -233,7 +233,7 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
await cmdBar.openCmdBar('promptToEdit')
await page
.getByTestId('cmd-bar-arg-value')
.fill('Please rename to mySketch')
.fill('Please rename to mySketch001')
await page.waitForTimeout(100)
await cmdBar.progressCmdBar()
await expect(submittingToast).toBeVisible()
@ -244,10 +244,10 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
})
await test.step('verify rename change and accept it', async () => {
await editor.expectEditor.toContain('mySketch = startSketchOn')
await editor.expectEditor.toContain('mySketch001 = startSketchOn')
await editor.expectEditor.not.toContain('sketch002 = startSketchOn')
await editor.expectEditor.toContain(
'extrude002 = extrude(mySketch, length = 50)'
'extrude002 = extrude(mySketch001, length = 50)'
)
await acceptBtn.click()

View File

@ -1,4 +1,5 @@
import { test, expect, Page } from './zoo-test'
import { Page } from '@playwright/test'
import { test, expect } from './zoo-test'
import path from 'path'
import * as fsp from 'fs/promises'
import { getUtils, executorInputPath } from './test-utils'

View File

@ -1,4 +1,5 @@
import { test, expect, Page } from './zoo-test'
import { Page } from '@playwright/test'
import { test, expect } from './zoo-test'
import fs from 'node:fs/promises'
import path from 'node:path'
import { HomePageFixture } from './fixtures/homePageFixture'
@ -2153,6 +2154,8 @@ extrude001 = extrude(profile003, length = 5)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await page.waitForTimeout(5000)
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
@ -2165,7 +2168,7 @@ extrude001 = extrude(profile003, length = 5)
await page.waitForTimeout(600)
await editor.expectEditor.toContain(`sketch001 = startSketchOn('XZ')`)
await toolbar.exitSketchBtn.click()
await toolbar.exitSketch()
await editor.expectEditor.not.toContain(`sketch001 = startSketchOn('XZ')`)
@ -2181,6 +2184,8 @@ extrude001 = extrude(profile003, length = 5)
)`
)
await scene.settled(cmdBar)
await scene.expectPixelColor([255, 255, 255], { x: 633, y: 211 }, 15)
})
})

View File

@ -31,8 +31,7 @@ test.beforeEach(async ({ page, context }) => {
// Help engine-manager: tear shit down.
test.afterEach(async ({ page }) => {
await page.evaluate(() => {
// @ts-expect-error
window.tearDown()
window.engineCommandManager.tearDown()
})
})
@ -45,7 +44,11 @@ test.setTimeout(60_000)
test.skip(
'exports of each format should work',
{ tag: ['@snapshot', '@skipWin', '@skipMacos'] },
async ({ page, context, scene, cmdBar }) => {
async ({ page, context, scene, cmdBar, tronApp }) => {
if (!tronApp) {
fail()
}
// FYI this test doesn't work with only engine running locally
// And you will need to have the KittyCAD CLI installed
const u = await getUtils(page)
@ -134,6 +137,7 @@ part001 = startSketchOn('-XZ')
storage: 'ascii',
units: 'in',
},
tronApp.projectDirName,
page
)
)
@ -146,6 +150,7 @@ part001 = startSketchOn('-XZ')
selection: { type: 'default_scene' },
units: 'in',
},
tronApp.projectDirName,
page
)
)
@ -158,6 +163,7 @@ part001 = startSketchOn('-XZ')
selection: { type: 'default_scene' },
units: 'in',
},
tronApp.projectDirName,
page
)
)
@ -170,6 +176,7 @@ part001 = startSketchOn('-XZ')
units: 'in',
selection: { type: 'default_scene' },
},
tronApp.projectDirName,
page
)
)
@ -182,6 +189,7 @@ part001 = startSketchOn('-XZ')
units: 'in',
selection: { type: 'default_scene' },
},
tronApp.projectDirName,
page
)
)
@ -193,6 +201,7 @@ part001 = startSketchOn('-XZ')
coords: sysType,
units: 'in',
},
tronApp.projectDirName,
page
)
)
@ -203,6 +212,7 @@ part001 = startSketchOn('-XZ')
storage: 'embedded',
presentation: 'pretty',
},
tronApp.projectDirName,
page
)
)
@ -213,6 +223,7 @@ part001 = startSketchOn('-XZ')
storage: 'binary',
presentation: 'pretty',
},
tronApp.projectDirName,
page
)
)
@ -223,6 +234,7 @@ part001 = startSketchOn('-XZ')
storage: 'standard',
presentation: 'pretty',
},
tronApp.projectDirName,
page
)
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 74 KiB

View File

@ -84,7 +84,6 @@ test.describe('Test network and connection issues', () => {
'Engine disconnect & reconnect in sketch mode',
{ tag: '@skipLocalEngine' },
async ({ page, homePage }) => {
// TODO: Don't skip Mac for these. After `window.tearDown` is working in Safari, these should work on webkit
const networkToggle = page.getByTestId('network-toggle')
const u = await getUtils(page)

View File

@ -5,8 +5,9 @@ import {
_electron as electron,
ElectronApplication,
Locator,
Page,
} from '@playwright/test'
import { test, Page } from './zoo-test'
import { test } from './zoo-test'
import { EngineCommand } from 'lang/std/artifactGraph'
import fsp from 'fs/promises'
import fsSync from 'fs'
@ -337,7 +338,7 @@ export const getMovementUtils = (opts: any) => {
async function waitForAuthAndLsp(page: Page) {
const waitForLspPromise = page.waitForEvent('console', {
predicate: async (message) => {
predicate: async (message: any) => {
// it would be better to wait for a message that the kcl lsp has started by looking for the message message.text().includes('[lsp] [window/logMessage]')
// but that doesn't seem to make it to the console for macos/safari :(
if (message.text().includes('start kcl lsp')) {
@ -420,7 +421,7 @@ export async function getUtils(page: Page, test_?: typeof test) {
const overlay = page.locator(locator)
const bbox = await overlay
.boundingBox({ timeout: 5_000 })
.then((box) => ({ ...box, x: box?.x || 0, y: box?.y || 0 }))
.then((box: any) => ({ ...box, x: box?.x || 0, y: box?.y || 0 }))
const angle = Number(await overlay.getAttribute('data-overlay-angle'))
const angleXOffset = Math.cos(((angle - 180) * Math.PI) / 180) * px
const angleYOffset = Math.sin(((angle - 180) * Math.PI) / 180) * px
@ -437,7 +438,7 @@ export async function getUtils(page: Page, test_?: typeof test) {
page
.locator(locator)
.boundingBox({ timeout: 5_000 })
.then((box) => ({ ...box, x: box?.x || 0, y: box?.y || 0 })),
.then((box: any) => ({ ...box, x: box?.x || 0, y: box?.y || 0 })),
codeLocator: page.locator('.cm-content'),
crushKclCodeIntoOneLineAndThenMaybeSome: async () => {
const code = await page.locator('.cm-content').innerText()
@ -504,7 +505,7 @@ export async function getUtils(page: Page, test_?: typeof test) {
) => {
if (cdpSession === null) {
// Use a fail safe if we can't simulate disconnect (on Safari)
return page.evaluate('window.tearDown()')
return page.evaluate('window.engineCommandManager.tearDown()')
}
return cdpSession?.send(
@ -631,7 +632,7 @@ export async function getUtils(page: Page, test_?: typeof test) {
panesOpen: async (paneIds: PaneId[]) => {
return test?.step(`Setting ${paneIds} panes to be open`, async () => {
await page.addInitScript(
({ PERSIST_MODELING_CONTEXT, paneIds }) => {
({ PERSIST_MODELING_CONTEXT, paneIds }: any) => {
localStorage.setItem(
PERSIST_MODELING_CONTEXT,
JSON.stringify({ openPanes: paneIds })
@ -722,14 +723,14 @@ export const makeTemplate: (
const PLAYWRIGHT_DOWNLOAD_DIR = 'downloads-during-playwright'
export const getPlaywrightDownloadDir = (page: Page) => {
return path.resolve(page.dir, PLAYWRIGHT_DOWNLOAD_DIR)
export const getPlaywrightDownloadDir = (rootDir: string) => {
return path.resolve(rootDir, PLAYWRIGHT_DOWNLOAD_DIR)
}
const moveDownloadedFileTo = async (page: Page, toLocation: string) => {
const moveDownloadedFileTo = async (rootDir: string, toLocation: string) => {
await fsp.mkdir(path.dirname(toLocation), { recursive: true })
const downloadDir = getPlaywrightDownloadDir(page)
const downloadDir = getPlaywrightDownloadDir(rootDir)
// Expect there to be at least one file
await expect
@ -756,6 +757,7 @@ export interface Paths {
export const doExport = async (
output: Models['OutputFormat_type'],
rootDir: string,
page: Page,
exportFrom: 'dropdown' | 'sidebarButton' | 'commandBar' = 'dropdown'
): Promise<Paths> => {
@ -836,7 +838,7 @@ export const doExport = async (
// (declared in src/lib/exportSave)
// To remain consistent with our old web tests, we want to move some downloads
// (images) to another directory.
await moveDownloadedFileTo(page, downloadLocation)
await moveDownloadedFileTo(rootDir, downloadLocation)
}
return {
@ -859,12 +861,6 @@ export async function tearDown(page: Page, testInfo: TestInfo) {
downloadThroughput: -1,
uploadThroughput: -1,
})
// It seems it's best to give the browser about 3s to close things
// It's not super reliable but we have no real other choice for now
await page.waitForTimeout(3000)
await testInfo.tronApp?.close()
}
// settingsOverrides may need to be augmented to take more generic items,
@ -936,107 +932,11 @@ let electronApp: ElectronApplication | undefined = undefined
let context: BrowserContext | undefined = undefined
let page: Page | undefined = undefined
export async function setupElectron({
testInfo,
cleanProjectDir = true,
appSettings,
viewport,
}: {
testInfo: TestInfo
folderSetupFn?: (projectDirName: string) => Promise<void>
cleanProjectDir?: boolean
appSettings?: DeepPartial<Settings>
viewport: {
width: number
height: number
}
}): Promise<{
electronApp: ElectronApplication
context: BrowserContext
page: Page
dir: string
}> {
// create or otherwise clear the folder
const projectDirName = testInfo.outputPath('electron-test-projects-dir')
try {
if (fsSync.existsSync(projectDirName) && cleanProjectDir) {
await fsp.rm(projectDirName, { recursive: true })
}
} catch (e) {
console.error(e)
}
if (cleanProjectDir) {
await fsp.mkdir(projectDirName)
}
const options = {
args: ['.', '--no-sandbox'],
env: {
...process.env,
TEST_SETTINGS_FILE_KEY: projectDirName,
IS_PLAYWRIGHT: 'true',
},
...(process.env.ELECTRON_OVERRIDE_DIST_PATH
? { executablePath: process.env.ELECTRON_OVERRIDE_DIST_PATH + 'electron' }
: {}),
...(process.env.PLAYWRIGHT_RECORD_VIDEO
? {
recordVideo: {
dir: testInfo.snapshotPath(),
size: viewport,
},
}
: {}),
}
// Do this once and then reuse window on subsequent calls.
if (!electronApp) {
electronApp = await electron.launch(options)
}
if (!context || !page) {
context = electronApp.context()
page = await electronApp.firstWindow()
context.on('console', console.log)
page.on('console', console.log)
}
if (cleanProjectDir) {
const tempSettingsFilePath = path.join(projectDirName, SETTINGS_FILE_NAME)
const settingsOverrides = settingsToToml(
appSettings
? {
settings: {
...TEST_SETTINGS,
...appSettings,
app: {
...TEST_SETTINGS.app,
project_directory: projectDirName,
...appSettings.app,
},
},
}
: {
settings: {
...TEST_SETTINGS,
app: {
...TEST_SETTINGS.app,
project_directory: projectDirName,
},
},
}
)
await fsp.writeFile(tempSettingsFilePath, settingsOverrides)
}
return { electronApp, page, context, dir: projectDirName }
}
function failOnConsoleErrors(page: Page, testInfo?: TestInfo) {
// enabled for chrome for now
if (page.context().browser()?.browserType().name() === 'chromium') {
page.on('pageerror', (exception) => {
// No idea wtf exception is
page.on('pageerror', (exception: any) => {
if (isErrorWhitelisted(exception)) {
return
}

View File

@ -1,4 +1,5 @@
import { test, expect, Page } from './zoo-test'
import { Page } from '@playwright/test'
import { test, expect } from './zoo-test'
import { deg, getUtils, wiggleMove } from './test-utils'
import { LineInputsType } from 'lang/std/sketchcombos'

View File

@ -257,6 +257,7 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
page,
homePage,
scene,
cmdBar,
}) => {
test.setTimeout(90_000)
const u = await getUtils(page)
@ -352,28 +353,15 @@ profile003 = startProfileAt([40.16, -120.48], sketch006)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
await scene.settled(cmdBar)
await u.openAndClearDebugPanel()
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
vantage: { x: 1139.49, y: -7053, z: 8597.31 },
center: { x: -2206.68, y: -1298.36, z: 60 },
up: { x: 0, y: 0, z: 1 },
},
})
await page.waitForTimeout(100)
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
await page.waitForTimeout(100)
const camPosition1 = async () => {
await scene.moveCameraTo(
{ x: 1139.49, y: -7053, z: 8597.31 },
{ x: -2206.68, y: -1298.36, z: 60 }
)
}
await camPosition1()
const revolve = { x: 635, y: 253 }
const parentExtrude = { x: 915, y: 133 }
@ -386,7 +374,7 @@ profile003 = startProfileAt([40.16, -120.48], sketch006)
await expect(page.locator('.cm-activeLine')).toHaveText(
'|> line(end = [0, -pipeLength])'
)
await u.clearCommandLogs()
await u.openAndClearDebugPanel()
await page.keyboard.press('Delete')
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
await page.waitForTimeout(200)
@ -399,11 +387,12 @@ profile003 = startProfileAt([40.16, -120.48], sketch006)
// and replace the sketch on face with a hard coded custom plane, but since there was a sketch on that plane maybe it
// should have delete the sketch? it's broken atm, but not sure if worth fixing since desired behaviour is a little
// vague
// // DELETE PARENT EXTRUDE
// DELETE PARENT EXTRUDE
// await camPosition2()
// await page.mouse.click(parentExtrude.x, parentExtrude.y)
// await page.waitForTimeout(100)
// await expect(page.locator('.cm-activeLine')).toHaveText(
// '|> line(end = [170.36, -121.61], tag = $seg01)'
// '|> line(end = [112.54, 127.64], tag = $seg02)'
// )
// await u.clearCommandLogs()
// await page.keyboard.press('Backspace')
@ -463,71 +452,77 @@ profile003 = startProfileAt([40.16, -120.48], sketch006)
await page.waitForTimeout(200)
await expect(u.codeLocator).not.toContainText(codeToBeDeletedSnippet)
})
test.fixme(
"Deleting solid that the AST mod can't handle results in a toast message",
async ({ page, homePage }) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`sketch001 = startSketchOn('XZ')
|> startProfileAt([-79.26, 95.04], %)
|> line(end = [112.54, 127.64], tag = $seg02)
|> line(end = [170.36, -121.61], tag = $seg01)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(sketch001, length = 50)
launderExtrudeThroughVar = extrude001
sketch002 = startSketchOn(launderExtrudeThroughVar, seg02)
|> startProfileAt([-100.54, 16.99], %)
|> line(end = [0, 20.03])
|> line(end = [62.61, 0], tag = $seg03)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
`
)
}, KCL_DEFAULT_LENGTH)
await page.setBodyDimensions({ width: 1000, height: 500 })
test('parent Solid should be select and deletable and uses custom planes to position children', async ({
page,
homePage,
scene,
cmdBar,
editor,
}) => {
test.setTimeout(90_000)
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`part001 = startSketchOn('XY')
yo = startProfileAt([4.83, 12.56], part001)
|> line(end = [15.1, 2.48])
|> line(end = [3.15, -9.85], tag = $seg01)
|> line(end = [-15.17, -4.1])
|> angledLine([segAng(seg01), 12.35], %, $seg02)
|> line(end = [-13.02, 10.03])
|> close()
yoo = extrude(yo, length = 4)
sketch002 = startSketchOn(yoo, seg02)
sketch001 = startSketchOn(yoo, 'END')
profile002 = startProfileAt([-11.08, 2.39], sketch002)
|> line(end = [4.89, 0.9])
|> line(end = [-0.61, -2.41])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(profile002, length = 15)
profile001 = startProfileAt([7.49, 9.96], sketch001)
|> angledLine([0, 5.05], %, $rectangleSegmentA001)
|> angledLine([
segAng(rectangleSegmentA001) - 90,
4.81
], %)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
await homePage.goToModelingScene()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
await u.closeDebugPanel()
await u.openAndClearDebugPanel()
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
vantage: { x: 1139.49, y: -7053, z: 8597.31 },
center: { x: -2206.68, y: -1298.36, z: 60 },
up: { x: 0, y: 0, z: 1 },
},
})
await page.waitForTimeout(100)
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
await page.waitForTimeout(100)
// attempt delete
await page.mouse.click(930, 139)
await page.waitForTimeout(100)
await expect(page.locator('.cm-activeLine')).toHaveText(
'|> line(end = [170.36, -121.61], tag = $seg01)'
`
)
await u.clearCommandLogs()
await page.keyboard.press('Delete')
}, KCL_DEFAULT_LENGTH)
await page.setBodyDimensions({ width: 1000, height: 500 })
await expect(page.getByText('Unable to delete selection')).toBeVisible()
}
)
await homePage.goToModelingScene()
await scene.settled(cmdBar)
const extrudeWall = { x: 575, y: 238 }
// DELETE with selection on face of parent
await page.mouse.click(extrudeWall.x, extrudeWall.y)
await page.waitForTimeout(100)
await expect(page.locator('.cm-activeLine')).toHaveText(
'|> line(end = [-15.17, -4.1])'
)
await u.openAndClearDebugPanel()
await page.keyboard.press('Delete')
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
await page.waitForTimeout(200)
await editor.expectEditor.not.toContain(`yoo = extrude(yo, length = 4)`, {
shouldNormalise: true,
})
await editor.expectEditor.toContain(`startSketchOn({plane={origin`, {
shouldNormalise: true,
})
await editor.snapshot()
})
test('Hovering over 3d features highlights code, clicking puts the cursor in the right place and sends selection id to engine', async ({
page,
homePage,

View File

@ -0,0 +1 @@
part001 = startSketchOn('XY')yo = startProfileAt([4.83, 12.56], part001) |> line(end = [15.1, 2.48]) |> line(end = [3.15, -9.85], tag = $seg01) |> line(end = [-15.17, -4.1]) |> angledLine([segAng(seg01), 12.35], %, $seg02) |> line(end = [-13.02, 10.03]) |> close()sketch002 = startSketchOn({ plane = { origin = { x = 7.49, y = 2.4, z = 0 }, xAxis = { x = -0.3, y = 0.95, z = 0 }, yAxis = { x = 0, y = 0, z = 1 }, zAxis = { x = 0.95, y = 0.3, z = 0 } }})sketch001 = startSketchOn({ plane = { origin = { x = 0, y = 0, z = 4 }, xAxis = { x = 1, y = 0, z = 0 }, yAxis = { x = 0, y = 1, z = 0 }, zAxis = { x = 0, y = 0, z = 1 } }})profile002 = startProfileAt([-11.08, 2.39], sketch002) |> line(end = [4.89, 0.9]) |> line(end = [-0.61, -2.41]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> close()extrude001 = extrude(profile002, length = 15)profile001 = startProfileAt([7.49, 9.96], sketch001) |> angledLine([0, 5.05], %, $rectangleSegmentA001) |> angledLine([ segAng(rectangleSegmentA001) - 90, 4.81 ], %) |> angledLine([ segAng(rectangleSegmentA001), -segLen(rectangleSegmentA001) ], %) |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> close()

View File

@ -20,35 +20,40 @@ import { DeepPartial } from 'lib/types'
import { Settings } from '@rust/kcl-lib/bindings/Settings'
test.describe('Testing settings', () => {
test(
'Stored settings are validated and fall back to defaults',
test('Stored settings are validated and fall back to defaults', async ({
page,
homePage,
tronApp,
}) => {
if (!tronApp) {
fail()
}
// Override beforeEach test setup
// with corrupted settings
{
appSettings: TEST_SETTINGS_CORRUPTED as DeepPartial<Settings>,
},
async ({ page, homePage }) => {
await page.setBodyDimensions({ width: 1200, height: 500 })
await tronApp.cleanProjectDir(
TEST_SETTINGS_CORRUPTED as DeepPartial<Settings>
)
// Check the settings were reset
const storedSettings = tomlToSettings(
await page.evaluate(
({ settingsKey }) => localStorage.getItem(settingsKey) || '',
{ settingsKey: TEST_SETTINGS_KEY }
)
await page.setBodyDimensions({ width: 1200, height: 500 })
// Check the settings were reset
const storedSettings = tomlToSettings(
await page.evaluate(
({ settingsKey }) => localStorage.getItem(settingsKey) || '',
{ settingsKey: TEST_SETTINGS_KEY }
)
)
expect(storedSettings.settings?.app?.theme).toBe('dark')
expect(storedSettings.settings?.app?.theme).toBe('dark')
// Check that the invalid settings were changed to good defaults
expect(storedSettings.settings?.modeling?.base_unit).toBe('in')
expect(storedSettings.settings?.modeling?.mouse_controls).toBe('zoo')
expect(storedSettings.settings?.app?.project_directory).toBe('')
expect(storedSettings.settings?.project?.default_project_name).toBe(
'project-$nnn'
)
}
)
// Check that the invalid settings were changed to good defaults
expect(storedSettings.settings?.modeling?.base_unit).toBe('in')
expect(storedSettings.settings?.modeling?.mouse_controls).toBe('zoo')
expect(storedSettings.settings?.app?.project_directory).toBe('')
expect(storedSettings.settings?.project?.default_project_name).toBe(
'project-$nnn'
)
})
// The behavior is actually broken. Parent always takes precedence
test.fixme(
@ -357,8 +362,6 @@ test.describe('Testing settings', () => {
`Load desktop app with no settings file`,
{
tag: '@electron',
// This is what makes no settings file get created
cleanProjectDir: false,
},
async ({ page }, testInfo) => {
await page.setBodyDimensions({ width: 1200, height: 500 })
@ -379,13 +382,17 @@ test.describe('Testing settings', () => {
`Load desktop app with a settings file, but no project directory setting`,
{
tag: '@electron',
appSettings: {
},
async ({ context, page, tronApp }, testInfo) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: {
theme_color: '259',
},
},
},
async ({ context, page }, testInfo) => {
})
await page.setBodyDimensions({ width: 1200, height: 500 })
// Selectors and constants
@ -405,15 +412,20 @@ test.describe('Testing settings', () => {
'user settings reload on external change, on project and modeling view',
{
tag: '@electron',
appSettings: {
},
async ({ context, page, tronApp }, testInfo) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: {
// Doesn't matter what you set it to. It will
// default to 264.5
theme_color: '0',
},
},
},
async ({ context, page }, testInfo) => {
})
const { dir: projectDirName } = await context.folderSetupFn(
async () => {}
)
@ -783,128 +795,136 @@ test.describe('Testing settings', () => {
})
})
test(
`Changing system theme preferences (via media query) should update UI and stream`,
{
// Override the settings so that the theme is set to `system`
appSettings: TEST_SETTINGS_DEFAULT_THEME,
},
async ({ page, homePage }) => {
const u = await getUtils(page)
// Selectors and constants
const darkBackgroundCss = 'oklch(0.3012 0 264.5)'
const lightBackgroundCss = 'oklch(0.9911 0 264.5)'
const darkBackgroundColor: [number, number, number] = [27, 27, 27]
const lightBackgroundColor: [number, number, number] = [245, 245, 245]
const streamBackgroundPixelIsColor = async (
color: [number, number, number]
) => {
return u.getGreatestPixDiff({ x: 1000, y: 200 }, color)
}
const toolbar = page.locator('menu').filter({ hasText: 'Start Sketch' })
await test.step(`Test setup`, async () => {
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await u.waitForPageLoad()
await page.waitForTimeout(1000)
await expect(toolbar).toBeVisible()
})
await test.step(`Check the background color is light before`, async () => {
await expect(toolbar).toHaveCSS('background-color', lightBackgroundCss)
await expect
.poll(() => streamBackgroundPixelIsColor(lightBackgroundColor))
.toBeLessThan(15)
})
await test.step(`Change media query preference to dark, emulating dusk with system theme`, async () => {
await page.emulateMedia({ colorScheme: 'dark' })
})
await test.step(`Check the background color is dark after`, async () => {
await expect(toolbar).toHaveCSS('background-color', darkBackgroundCss)
await expect
.poll(() => streamBackgroundPixelIsColor(darkBackgroundColor))
.toBeLessThan(15)
})
test(`Changing system theme preferences (via media query) should update UI and stream`, async ({
page,
homePage,
tronApp,
}) => {
if (!tronApp) {
fail()
}
)
test(
`Turning off "Show debug panel" with debug panel open leaves no phantom panel`,
{
await tronApp.cleanProjectDir({
// Override the settings so that the theme is set to `system`
...TEST_SETTINGS_DEFAULT_THEME,
})
const u = await getUtils(page)
// Selectors and constants
const darkBackgroundCss = 'oklch(0.3012 0 264.5)'
const lightBackgroundCss = 'oklch(0.9911 0 264.5)'
const darkBackgroundColor: [number, number, number] = [27, 27, 27]
const lightBackgroundColor: [number, number, number] = [245, 245, 245]
const streamBackgroundPixelIsColor = async (
color: [number, number, number]
) => {
return u.getGreatestPixDiff({ x: 1000, y: 200 }, color)
}
const toolbar = page.locator('menu').filter({ hasText: 'Start Sketch' })
await test.step(`Test setup`, async () => {
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await u.waitForPageLoad()
await page.waitForTimeout(1000)
await expect(toolbar).toBeVisible()
})
await test.step(`Check the background color is light before`, async () => {
await expect(toolbar).toHaveCSS('background-color', lightBackgroundCss)
await expect
.poll(() => streamBackgroundPixelIsColor(lightBackgroundColor))
.toBeLessThan(15)
})
await test.step(`Change media query preference to dark, emulating dusk with system theme`, async () => {
await page.emulateMedia({ colorScheme: 'dark' })
})
await test.step(`Check the background color is dark after`, async () => {
await expect(toolbar).toHaveCSS('background-color', darkBackgroundCss)
await expect
.poll(() => streamBackgroundPixelIsColor(darkBackgroundColor))
.toBeLessThan(15)
})
})
test(`Turning off "Show debug panel" with debug panel open leaves no phantom panel`, async ({
context,
page,
homePage,
tronApp,
}) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
// Override beforeEach test setup
// with debug panel open
// but "show debug panel" set to false
appSettings: {
...TEST_SETTINGS,
app: { ...TEST_SETTINGS.app, show_debug_panel: false },
modeling: { ...TEST_SETTINGS.modeling },
},
},
async ({ context, page, homePage }) => {
const u = await getUtils(page)
...TEST_SETTINGS,
app: { ...TEST_SETTINGS.app, show_debug_panel: false },
modeling: { ...TEST_SETTINGS.modeling },
})
await context.addInitScript(async () => {
localStorage.setItem(
'persistModelingContext',
'{"openPanes":["debug"]}'
const u = await getUtils(page)
await context.addInitScript(async () => {
localStorage.setItem('persistModelingContext', '{"openPanes":["debug"]}')
})
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
// Constants and locators
const resizeHandle = page.locator('.sidebar-resize-handles > div.block')
const debugPaneButton = page.getByTestId('debug-pane-button')
const commandsButton = page.getByRole('button', { name: 'Commands' })
const debugPaneOption = page.getByRole('option', {
name: 'Settings · app · show debug panel',
})
async function setShowDebugPanelTo(value: 'On' | 'Off') {
await commandsButton.click()
await debugPaneOption.click()
await page.getByRole('option', { name: value }).click()
await expect(
page.getByText(
`Set show debug panel to "${value === 'On'}" for this project`
)
})
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
// Constants and locators
const resizeHandle = page.locator('.sidebar-resize-handles > div.block')
const debugPaneButton = page.getByTestId('debug-pane-button')
const commandsButton = page.getByRole('button', { name: 'Commands' })
const debugPaneOption = page.getByRole('option', {
name: 'Settings · app · show debug panel',
})
async function setShowDebugPanelTo(value: 'On' | 'Off') {
await commandsButton.click()
await debugPaneOption.click()
await page.getByRole('option', { name: value }).click()
await expect(
page.getByText(
`Set show debug panel to "${value === 'On'}" for this project`
)
).toBeVisible()
}
await test.step(`Initial load with corrupted settings`, async () => {
// Check that the debug panel is not visible
await expect(debugPaneButton).not.toBeVisible()
// Check the pane resize handle wrapper is not visible
await expect(resizeHandle).not.toBeVisible()
})
await test.step(`Open code pane to verify we see the resize handles`, async () => {
await u.openKclCodePanel()
await expect(resizeHandle).toBeVisible()
await u.closeKclCodePanel()
})
await test.step(`Turn on debug panel, open it`, async () => {
await setShowDebugPanelTo('On')
await expect(debugPaneButton).toBeVisible()
// We want the logic to clear the phantom panel, so we shouldn't see
// the real panel (and therefore the resize handle) yet
await expect(resizeHandle).not.toBeVisible()
await u.openDebugPanel()
await expect(resizeHandle).toBeVisible()
})
await test.step(`Turn off debug panel setting with it open`, async () => {
await setShowDebugPanelTo('Off')
await expect(debugPaneButton).not.toBeVisible()
await expect(resizeHandle).not.toBeVisible()
})
).toBeVisible()
}
)
await test.step(`Initial load with corrupted settings`, async () => {
// Check that the debug panel is not visible
await expect(debugPaneButton).not.toBeVisible()
// Check the pane resize handle wrapper is not visible
await expect(resizeHandle).not.toBeVisible()
})
await test.step(`Open code pane to verify we see the resize handles`, async () => {
await u.openKclCodePanel()
await expect(resizeHandle).toBeVisible()
await u.closeKclCodePanel()
})
await test.step(`Turn on debug panel, open it`, async () => {
await setShowDebugPanelTo('On')
await expect(debugPaneButton).toBeVisible()
// We want the logic to clear the phantom panel, so we shouldn't see
// the real panel (and therefore the resize handle) yet
await expect(resizeHandle).not.toBeVisible()
await u.openDebugPanel()
await expect(resizeHandle).toBeVisible()
})
await test.step(`Turn off debug panel setting with it open`, async () => {
await setShowDebugPanelTo('Off')
await expect(debugPaneButton).not.toBeVisible()
await expect(resizeHandle).not.toBeVisible()
})
})
test(`Change inline units setting`, async ({
page,

View File

@ -1,4 +1,5 @@
import { test, expect, Page } from './zoo-test'
import { Page } from '@playwright/test'
import { test, expect } from './zoo-test'
import { getUtils, createProject } from './test-utils'
import { join } from 'path'
import fs from 'fs'
@ -429,7 +430,8 @@ test.describe('Text-to-CAD tests', { tag: ['@skipWin'] }, () => {
await expect(page.getByText(promptWithNewline)).toBeVisible()
})
test(
// This will be fine once greg makes prompt at top of file deterministic
test.fixme(
'can do many at once and get many prompts back, and interact with many',
{ tag: ['@skipWin'] },
async ({ page, homePage }) => {
@ -490,8 +492,15 @@ test.describe('Text-to-CAD tests', { tag: ['@skipWin'] }, () => {
// Click the button.
await copyToClipboardButton.first().click()
// Expect the code to be pasted.
await expect(page.locator('.cm-content')).toContainText(`2x8`)
// Do NOT do AI tests like this: "Expect the code to be pasted."
// Reason: AI tests are NONDETERMINISTIC. Thus we need to be as most
// general as we can for the assertion.
// We can use Kolmogorov complexity as a measurement of the
// "probably most minimal version of this program" to have a lower
// bound to work with. It is completely by feel because there are
// no proofs that any program is its smallest self.
const code2x8 = await page.locator('.cm-content').innerText()
await expect(code2x8.length).toBeGreaterThan(249)
// Ensure the final toast remains.
await expect(page.getByText(`a 2x10 lego`)).not.toBeVisible()
@ -504,7 +513,8 @@ test.describe('Text-to-CAD tests', { tag: ['@skipWin'] }, () => {
await copyToClipboardButton.click()
// Expect the code to be pasted.
await expect(page.locator('.cm-content')).toContainText(`2x4`)
const code2x4 = await page.locator('.cm-content').innerText()
await expect(code2x4.length).toBeGreaterThan(249)
}
)

View File

@ -35,7 +35,7 @@ test.fixme('Units menu', async ({ page, homePage }) => {
test(
'Successful export shows a success toast',
{ tag: '@skipLocalEngine' },
async ({ page, homePage }) => {
async ({ page, homePage, tronApp }) => {
// FYI this test doesn't work with only engine running locally
// And you will need to have the KittyCAD CLI installed
const u = await getUtils(page)
@ -92,12 +92,17 @@ part001 = startSketchOn('-XZ')
await page.waitForTimeout(1000)
await u.clearAndCloseDebugPanel()
if (!tronApp?.projectDirName) {
fail()
}
await doExport(
{
type: 'gltf',
storage: 'embedded',
presentation: 'pretty',
},
tronApp?.projectDirName,
page
)
}
@ -465,7 +470,7 @@ test('Delete key does not navigate back', async ({ page, homePage }) => {
await expect.poll(() => page.url()).not.toContain('/settings')
})
test('Sketch on face', async ({ page, homePage, scene, cmdBar }) => {
test('Sketch on face', async ({ page, homePage, scene, cmdBar, toolbar }) => {
test.setTimeout(90_000)
const u = await getUtils(page)
await page.addInitScript(async () => {
@ -491,25 +496,22 @@ extrude001 = extrude(sketch001, length = 5 + 7)`
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(300)
await scene.connectionEstablished()
await scene.settled(cmdBar)
let previousCodeContent = await page.locator('.cm-content').innerText()
await u.openAndClearDebugPanel()
await u.doAndWaitForCmd(
() => page.mouse.click(625, 165),
'default_camera_get_settings',
true
)
await page.waitForTimeout(150)
await u.closeDebugPanel()
await toolbar.startSketchThenCallbackThenWaitUntilReady(async () => {
await u.openAndClearDebugPanel()
await u.doAndWaitForCmd(
() => page.mouse.click(625, 165),
'default_camera_get_settings',
true
)
await page.waitForTimeout(150)
await u.closeDebugPanel()
})
await page.waitForTimeout(300)
const firstClickPosition = [612, 238]
const secondClickPosition = [661, 242]

View File

@ -1,21 +1,11 @@
import {
test as playwrightTestFn,
TestInfo as TestInfoPlaywright,
BrowserContext as BrowserContextPlaywright,
Page as PagePlaywright,
TestDetails as TestDetailsPlaywright,
PlaywrightTestArgs,
PlaywrightTestOptions,
PlaywrightWorkerArgs,
PlaywrightWorkerOptions,
ElectronApplication,
} from '@playwright/test'
/* eslint-disable react-hooks/rules-of-hooks */
import { test as playwrightTestFn, ElectronApplication } from '@playwright/test'
import {
fixtures,
fixturesBasedOnProcessEnvPlatform,
Fixtures,
AuthenticatedTronApp,
AuthenticatedApp,
ElectronZoo,
} from './fixtures/fixtureSetup'
import { Settings } from '@rust/kcl-lib/bindings/Settings'
@ -23,9 +13,6 @@ import { DeepPartial } from 'lib/types'
export { expect } from '@playwright/test'
declare module '@playwright/test' {
interface TestInfo {
tronApp?: AuthenticatedTronApp
}
interface BrowserContext {
folderSetupFn: (
cb: (dir: string) => Promise<void>
@ -41,288 +28,31 @@ declare module '@playwright/test' {
}
}
export type TestInfo = TestInfoPlaywright
export type BrowserContext = BrowserContextPlaywright
export type Page = PagePlaywright
export type TestDetails = TestDetailsPlaywright & {
cleanProjectDir?: boolean
appSettings?: DeepPartial<Settings>
}
// Each worker spawns a new thread, which will spawn its own ElectronZoo.
// So in some sense there is an implicit pool.
// For example, the variable just beneath this text is reused many times
// *for one worker*.
const electronZooInstance = new ElectronZoo()
// Our custom decorated Zoo test object. Makes it easier to add fixtures, and
// switch between web and electron if needed.
const pwTestFnWithFixtures = playwrightTestFn.extend<Fixtures>(fixtures)
// In JavaScript you cannot replace a function's body only (despite functions
// are themselves objects, which you'd expect a body property or something...)
// So we must redefine the function and then re-attach properties.
type PWFunction = (
args: PlaywrightTestArgs &
Fixtures &
PlaywrightWorkerArgs &
PlaywrightTestOptions &
PlaywrightWorkerOptions & {
electronApp?: ElectronApplication
},
testInfo: TestInfo
) => void | Promise<void>
let firstUrl = ''
export const test = (
desc: string,
objOrFn: PWFunction | TestDetails,
fnMaybe?: PWFunction
) => {
const hasTestConf = typeof objOrFn === 'object'
const fn = hasTestConf ? fnMaybe : objOrFn
return pwTestFnWithFixtures(
desc,
hasTestConf ? objOrFn : {},
async (
{
page,
context,
cmdBar,
editor,
toolbar,
scene,
homePage,
request,
playwright,
browser,
acceptDownloads,
bypassCSP,
colorScheme,
clientCertificates,
deviceScaleFactor,
extraHTTPHeaders,
geolocation,
hasTouch,
httpCredentials,
ignoreHTTPSErrors,
isMobile,
javaScriptEnabled,
locale,
offline,
permissions,
proxy,
storageState,
timezoneId,
userAgent,
viewport,
baseURL,
contextOptions,
actionTimeout,
navigationTimeout,
serviceWorkers,
testIdAttribute,
browserName,
defaultBrowserType,
headless,
channel,
launchOptions,
connectOptions,
screenshot,
trace,
video,
},
testInfo
) => {
// To switch to web, use PLATFORM=web environment variable.
// Only use this for debugging, since the playwright tracer is busted
// for electron.
let tronApp
if (process.env.PLATFORM === 'web') {
tronApp = new AuthenticatedApp(context, page, testInfo)
} else {
tronApp = new AuthenticatedTronApp(context, page, testInfo)
}
const fixtures: Fixtures = { cmdBar, editor, toolbar, scene, homePage }
if (tronApp instanceof AuthenticatedTronApp) {
const options = {
fixtures,
}
if (hasTestConf) {
Object.assign(options, {
appSettings: objOrFn?.appSettings,
cleanProjectDir: objOrFn?.cleanProjectDir,
})
}
await tronApp.initialise(options)
} else {
await tronApp.initialise('')
}
// We need to patch this because addInitScript will bind too late in our
// electron tests, never running. We need to call reload() after each call
// to guarantee it runs.
const oldContextAddInitScript = tronApp.context.addInitScript
tronApp.context.addInitScript = async function (a, b) {
// @ts-ignore pretty sure way out of tsc's type checking capabilities.
// This code works perfectly fine.
await oldContextAddInitScript.apply(this, [a, b])
await tronApp.page.reload()
}
// No idea why we mix and match page and context's addInitScript but we do
const oldPageAddInitScript = tronApp.page.addInitScript
tronApp.page.addInitScript = async function (a: any, b: any) {
// @ts-ignore pretty sure way out of tsc's type checking capabilities.
// This code works perfectly fine.
await oldPageAddInitScript.apply(this, [a, b])
await tronApp.page.reload()
}
// Create a consistent way to resize the page across electron and web.
// (lee) I had to do everything in the book to make electron change its
// damn window size. I succeeded in making it consistently and reliably
// do it after a whole afternoon.
tronApp.page.setBodyDimensions = async function (dims: {
width: number
height: number
}) {
await tronApp.page.setViewportSize(dims)
if (!(tronApp instanceof AuthenticatedTronApp)) {
return
}
await tronApp.electronApp?.evaluateHandle(async ({ app }, dims) => {
// @ts-ignore sorry jon but see comment in main.ts why this is ignored
await app.resizeWindow(dims.width, dims.height)
}, dims)
return tronApp.page.evaluate(
async (dims: { width: number; height: number }) => {
await window.electron.resizeWindow(dims.width, dims.height)
window.document.body.style.width = dims.width + 'px'
window.document.body.style.height = dims.height + 'px'
window.document.documentElement.style.width = dims.width + 'px'
window.document.documentElement.style.height = dims.height + 'px'
},
dims
)
}
await tronApp.page.setBodyDimensions(tronApp.viewPortSize)
// We need to expose this in order for some tests that require folder
// creation. Before they used to do this by their own electronSetup({...})
// calls.
if (tronApp instanceof AuthenticatedTronApp) {
tronApp.context.folderSetupFn = async function (fn) {
return fn(tronApp.dir)
.then(() => tronApp.page.reload())
.then(() => ({
dir: tronApp.dir,
}))
}
}
if (!firstUrl) {
await tronApp.page.getByText('Your Projects').count()
firstUrl = tronApp.page.url()
}
// Due to the app controlling its own window context we need to inject new
// options and context here.
// NOTE TO LEE: Seems to destroy page context when calling an electron loadURL.
// await tronApp.electronApp.evaluate(({ app }) => {
// return app.reuseWindowForTest();
// });
await tronApp.electronApp?.evaluate(({ app }, projectDirName) => {
// @ts-ignore can't declaration merge see main.ts
app.testProperty['TEST_SETTINGS_FILE_KEY'] = projectDirName
}, tronApp.dir)
// Always start at the root view
await tronApp.page.goto(firstUrl)
// Force a hard reload, destroying the stream and other state
await tronApp.page.reload()
// tsc aint smart enough to know this'll never be undefined
// but I dont blame it, the logic to know is complex
if (fn) {
await fn(
{
context: tronApp.context,
page: tronApp.page,
electronApp:
tronApp instanceof AuthenticatedTronApp
? tronApp.electronApp
: undefined,
...fixtures,
request,
playwright,
browser,
acceptDownloads,
bypassCSP,
colorScheme,
clientCertificates,
deviceScaleFactor,
extraHTTPHeaders,
geolocation,
hasTouch,
httpCredentials,
ignoreHTTPSErrors,
isMobile,
javaScriptEnabled,
locale,
offline,
permissions,
proxy,
storageState,
timezoneId,
userAgent,
viewport,
baseURL,
contextOptions,
actionTimeout,
navigationTimeout,
serviceWorkers,
testIdAttribute,
browserName,
defaultBrowserType,
headless,
channel,
launchOptions,
connectOptions,
screenshot,
trace,
video,
},
testInfo
)
}
testInfo.tronApp =
tronApp instanceof AuthenticatedTronApp ? tronApp : undefined
const playwrightTestFnWithFixtures_ = playwrightTestFn.extend<{
tronApp?: ElectronZoo
}>({
tronApp: async ({}, use, testInfo) => {
if (process.env.PLATFORM === 'web') {
await use(undefined)
return
}
)
}
type ZooTest = typeof test
await electronZooInstance.createInstanceIfMissing(testInfo)
await use(electronZooInstance)
await electronZooInstance.makeAvailableAgain()
},
})
test.describe = pwTestFnWithFixtures.describe
test.beforeEach = pwTestFnWithFixtures.beforeEach
test.afterEach = pwTestFnWithFixtures.afterEach
test.step = pwTestFnWithFixtures.step
test.skip = pwTestFnWithFixtures.skip
test.setTimeout = pwTestFnWithFixtures.setTimeout
test.fixme = pwTestFnWithFixtures.fixme as unknown as ZooTest
test.only = pwTestFnWithFixtures.only
test.fail = pwTestFnWithFixtures.fail
test.slow = pwTestFnWithFixtures.slow
test.beforeAll = pwTestFnWithFixtures.beforeAll
test.afterAll = pwTestFnWithFixtures.afterAll
test.use = pwTestFnWithFixtures.use
test.expect = pwTestFnWithFixtures.expect
test.extend = pwTestFnWithFixtures.extend
test.info = pwTestFnWithFixtures.info
const test = playwrightTestFnWithFixtures_.extend<Fixtures>(
fixturesBasedOnProcessEnvPlatform
)
export { test }

View File

@ -93,7 +93,7 @@
"fetch:wasm:windows": "./scripts/get-latest-wasm-bundle.ps1",
"fetch:samples": "rm -rf public/kcl-samples* && curl -L -o public/kcl-samples.zip https://github.com/KittyCAD/kcl-samples/archive/refs/heads/achalmers/kw-args-xylineto.zip && unzip -o public/kcl-samples.zip -d public && mv public/kcl-samples-* public/kcl-samples",
"build:wasm-dev": "yarn wasm-prep && (cd rust && wasm-pack build kcl-wasm-lib --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && yarn isomorphic-copy-wasm && yarn fmt",
"build:wasm:nocopy": "yarn wasm-prep && cd rust && wasm-pack build kcl-wasm-lib --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings",
"build:wasm:nocopy": "yarn wasm-prep && cd rust && RUSTFLAGS='--cfg getrandom_backend=\"wasm_js\"' wasm-pack build kcl-wasm-lib --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings",
"build:wasm": "yarn build:wasm:nocopy && cp rust/kcl-wasm-lib/pkg/kcl_wasm_lib_bg.wasm public && yarn fmt",
"build:wasm:windows": "yarn install:wasm-pack:cargo && yarn build:wasm:nocopy && ./scripts/copy-wasm.ps1 && yarn fmt",
"remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./rust/kcl-wasm-lib/pkg/kcl_wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./rust/kcl-wasm-lib/pkg/kcl_wasm_lib.js\" || echo \"sed for both mac and linux\"",
@ -106,7 +106,7 @@
"files:flip-to-nightly:windows": "./scripts/flip-files-to-nightly.ps1",
"files:invalidate-bucket": "./scripts/invalidate-files-bucket.sh",
"files:invalidate-bucket:nightly": "./scripts/invalidate-files-bucket.sh --nightly",
"postinstall": "./node_modules/.bin/electron-rebuild",
"postinstall": "yarn --cwd ./rust/kcl-language-server --modules-folder node_modules install && ./node_modules/.bin/electron-rebuild",
"make:dev": "make dev",
"generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts",
"generate:samples-manifest": "cd public/kcl-samples && node generate-manifest.js",

View File

@ -23,14 +23,14 @@ KCL samples conform to a set of style guidelines to ensure consistency and reada
When you submit a PR to add or modify KCL samples, images and STEP files will be generated and added to the repository automatically.
---
#### [3d-boaty](3d-boaty/main.kcl) ([step](step/3d-boaty.step)) ([screenshot](screenshots/3d-boaty.png))
[![3d-boaty](screenshots/3d-boaty.png)](3d-boaty/main.kcl)
#### [80-20-rail](80-20-rail/main.kcl) ([step](step/80-20-rail.step)) ([screenshot](screenshots/80-20-rail.png))
[![80-20-rail](screenshots/80-20-rail.png)](80-20-rail/main.kcl)
#### [a-parametric-bearing-pillow-block](a-parametric-bearing-pillow-block/main.kcl) ([step](step/a-parametric-bearing-pillow-block.step)) ([screenshot](screenshots/a-parametric-bearing-pillow-block.png))
[![a-parametric-bearing-pillow-block](screenshots/a-parametric-bearing-pillow-block.png)](a-parametric-bearing-pillow-block/main.kcl)
#### [ball-bearing](ball-bearing/main.kcl) ([step](step/ball-bearing.step)) ([screenshot](screenshots/ball-bearing.png))
[![ball-bearing](screenshots/ball-bearing.png)](ball-bearing/main.kcl)
#### [bench](bench/main.kcl) ([step](step/bench.step)) ([screenshot](screenshots/bench.png))
[![bench](screenshots/bench.png)](bench/main.kcl)
#### [bracket](bracket/main.kcl) ([step](step/bracket.step)) ([screenshot](screenshots/bracket.png))
[![bracket](screenshots/bracket.png)](bracket/main.kcl)
#### [car-wheel-assembly](car-wheel-assembly/main.kcl) ([step](step/car-wheel-assembly.step)) ([screenshot](screenshots/car-wheel-assembly.png))
@ -45,10 +45,8 @@ When you submit a PR to add or modify KCL samples, images and STEP files will be
[![enclosure](screenshots/enclosure.png)](enclosure/main.kcl)
#### [exhaust-manifold](exhaust-manifold/main.kcl) ([step](step/exhaust-manifold.step)) ([screenshot](screenshots/exhaust-manifold.png))
[![exhaust-manifold](screenshots/exhaust-manifold.png)](exhaust-manifold/main.kcl)
#### [flange-with-patterns](flange-with-patterns/main.kcl) ([step](step/flange-with-patterns.step)) ([screenshot](screenshots/flange-with-patterns.png))
[![flange-with-patterns](screenshots/flange-with-patterns.png)](flange-with-patterns/main.kcl)
#### [flange-xy](flange-xy/main.kcl) ([step](step/flange-xy.step)) ([screenshot](screenshots/flange-xy.png))
[![flange-xy](screenshots/flange-xy.png)](flange-xy/main.kcl)
#### [flange](flange/main.kcl) ([step](step/flange.step)) ([screenshot](screenshots/flange.png))
[![flange](screenshots/flange.png)](flange/main.kcl)
#### [focusrite-scarlett-mounting-bracket](focusrite-scarlett-mounting-bracket/main.kcl) ([step](step/focusrite-scarlett-mounting-bracket.step)) ([screenshot](screenshots/focusrite-scarlett-mounting-bracket.png))
[![focusrite-scarlett-mounting-bracket](screenshots/focusrite-scarlett-mounting-bracket.png)](focusrite-scarlett-mounting-bracket/main.kcl)
#### [food-service-spatula](food-service-spatula/main.kcl) ([step](step/food-service-spatula.step)) ([screenshot](screenshots/food-service-spatula.png))

View File

@ -15,90 +15,57 @@ padding = 1.5
bearingDia = 3
// (Needs to be updated). Sketch the block and extrude up to where the counterbore diameter starts.
block = startSketchOn('XY')
extrude001 = startSketchOn('XY')
|> startProfileAt([-width / 2, -length / 2], %)
|> line(endAbsolute = [width / 2, -length / 2])
|> line(endAbsolute = [width / 2, length / 2])
|> line(endAbsolute = [-width / 2, length / 2])
|> close()
|> hole(circle(
center = [
|> extrude(length = height)
extrude002 = startSketchOn(extrude001, 'end')
|> circle(
center = [
-(width / 2 - (padding / 2)),
-(length / 2 - (padding / 2))
],
radius = holeDia / 2
), %)
|> hole(circle(
center = [
-(width / 2 - (padding / 2)),
length / 2 - (padding / 2)
],
radius = holeDia / 2
), %)
|> hole(circle(
center = [
width / 2 - (padding / 2),
length / 2 - (padding / 2)
],
radius = holeDia / 2
), %)
|> hole(circle(
center = [
width / 2 - (padding / 2),
-(length / 2 - (padding / 2))
],
radius = holeDia / 2
), %)
|> hole(circle(
center = [0, 0],
radius = bearingDia / 2
), %)
|> extrude(length = height - cbDepth)
],
radius = cbDia / 2,
)
|> patternLinear2d(
instances = 2,
distance = length - padding,
axis = [0, 1],
)
|> patternLinear2d(
instances = 2,
distance = width - padding,
axis = [1, 0],
)
|> extrude(%, length = -cbDepth)
// Create a second sketch that creates the counterbore diameters and extrude the rest of the way to get the total height. Note: You cannot use startSketchOn(block, 'end'). The extrude lives outside the bounds, and the engine will not execute. This is a known issue.
secondHalf = startSketchOn({
plane = {
origin = { x = 0, y = 0, z = height - cbDepth },
xAxis = { x = 1, y = 0, z = 0 },
yAxis = { x = 0, y = 1, z = 0 },
zAxis = { x = 0, y = 0, z = 1 }
}
})
|> startProfileAt([-width / 2, -length / 2], %)
|> line(endAbsolute = [width / 2, -length / 2])
|> line(endAbsolute = [width / 2, length / 2])
|> line(endAbsolute = [-width / 2, length / 2])
|> close()
|> hole(circle(
center = [
-(width / 2 - (padding / 2)),
-(length / 2 - (padding / 2))
],
radius = cbDia / 2
), %)
|> hole(circle(
center = [
-(width / 2 - (padding / 2)),
length / 2 - (padding / 2)
],
radius = cbDia / 2
), %)
|> hole(circle(
center = [
width / 2 - (padding / 2),
length / 2 - (padding / 2)
],
radius = cbDia / 2
), %)
|> hole(circle(
center = [
width / 2 - (padding / 2),
-(length / 2 - (padding / 2))
],
radius = cbDia / 2
), %)
|> hole(circle(
center = [0, 0],
radius = bearingDia / 2
), %)
|> extrude(length = cbDepth)
extrude003 = startSketchOn(extrude001, 'start')
|> circle(
center = [
-(width / 2 - (padding / 2)),
-(length / 2 - (padding / 2))
],
radius = holeDia / 2,
)
|> patternLinear2d(
instances = 2,
distance = length - padding,
axis = [0, 1],
)
|> patternLinear2d(
instances = 2,
distance = width - padding,
axis = [1, 0],
)
|> extrude(length = -height + cbDepth)
extrude004 = startSketchOn(extrude001, 'end')
|> circle(
center = [0, 0],
radius = bearingDia/2,
)
|> extrude(length = -height)

View File

@ -16,21 +16,8 @@ chainWidth = sphereDia / 2
chainThickness = sphereDia / 8
linkDiameter = sphereDia / 4
customPlane = {
plane = {
origin = {
x = 0,
y = 0,
z = -overallThickness / 2
},
xAxis = { x = 1, y = 0, z = 0 },
yAxis = { x = 0, y = 1, z = 0 },
zAxis = { x = 0, y = 0, z = 1 }
}
}
// Sketch the inside bearing piece
insideWallSketch = startSketchOn(customPlane)
insideWallSketch = startSketchOn(offsetPlane("XY", offset = -overallThickness / 2))
|> circle(
center = [0, 0],
radius = shaftDia / 2 + wallThickness
@ -109,7 +96,7 @@ linkRevolve = revolve({ axis = 'Y', angle = 360 / nBalls }, linkSketch)
)
// Create the sketch for the outside walls
outsideWallSketch = startSketchOn(customPlane)
outsideWallSketch = startSketchOn(offsetPlane("XY", offset = -overallThickness / 2))
|> circle(
center = [0, 0],
radius = outsideDiameter / 2

View File

@ -1,4 +1,4 @@
// 3D Boaty
// Bench
// This is a slight remix of Depep1's original 3D Boaty (https://www.printables.com/model/1141963-3d-boaty). This is a tool used for benchmarking 3D FDM printers for bed adhesion, overhangs, bridging and top surface quality. The name of this file is a bit of misnomer, the shape of the object is a typical park bench.
// Set units in millimeters (mm)
@ -8,12 +8,12 @@
benchLength = 56
// Import various constants and functions from our library
import dividerThickness from "boat-parts.kcl"
import divider from "boat-parts.kcl"
import connector from "boat-parts.kcl"
import seatSlats from "boat-parts.kcl"
import backSlats from "boat-parts.kcl"
import armRest from "boat-parts.kcl"
import dividerThickness from "bench-parts.kcl"
import divider from "bench-parts.kcl"
import connector from "bench-parts.kcl"
import seatSlats from "bench-parts.kcl"
import backSlats from "bench-parts.kcl"
import armRest from "bench-parts.kcl"
// Create the dividers, these hold the seat and back slats
divider("YZ")

View File

@ -1,113 +1,76 @@
// Shelf Bracket
// 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.
// Set units
@settings(defaultLengthUnit = in)
// Define constants
sigmaAllow = 35000 // psi (6061-T6 aluminum)
width = 6
width = 6 // inch
p = 300 // Force on shelf - lbs
factorOfSafety = 1.2 // FOS of 1.2
shelfMountL = 5
wallMountL = 2
shelfMountL = 5 // inches
wallMountL = 2 // inches
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)
filletRadius = .375
extFilletRadius = .25
mountingHoleDiameter = 0.5
// Calculate required thickness of bracket
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
bracketLeg1Sketch = startSketchOn('XY')
sketch001 = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> line(end = [shelfMountL - filletRadius, 0], tag = $fillet1)
|> line(end = [0, width], tag = $fillet2)
|> line(end = [-shelfMountL + filletRadius, 0])
|> xLine(length = shelfMountL - thickness, tag = $seg01)
|> yLine(length = thickness, tag = $seg02)
|> xLine(length = -shelfMountL, tag = $seg03)
|> yLine(length = -wallMountL, tag = $seg04)
|> xLine(length = thickness, tag = $seg05)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg06)
|> close()
|> hole(circle(
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)
|> extrude(%, length = width)
|> fillet(
radius = extFilletRadius,
tags = [
getNextAdjacentEdge(fillet1),
getNextAdjacentEdge(fillet2)
]
tags = [getNextAdjacentEdge(seg03)],
)
// 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(
radius = extFilletRadius,
tags = [
getNextAdjacentEdge(fillet3),
getNextAdjacentEdge(fillet4)
]
radius = filletRadius,
tags = [getNextAdjacentEdge(seg06)],
)
|> 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

@ -9,18 +9,8 @@
// Import Constants
import caliperTolerance, caliperPadLength, caliperThickness, caliperOuterEdgeRadius, caliperInnerEdgeRadius, rotorDiameter, rotorTotalThickness, yAxisOffset from "globals.kcl"
// Create the plane for the brake caliper. This is so it can match up with the rotor model.
brakeCaliperPlane = {
plane = {
origin = { x = 0, y = yAxisOffset, z = 0 },
xAxis = { x = 1, y = 0, z = 0 },
yAxis = { x = 0, y = 1, z = 0 },
zAxis = { x = 0, y = 0, z = 1 }
}
}
// Sketch the brake caliper profile
brakeCaliperSketch = startSketchOn(brakeCaliperPlane)
brakeCaliperSketch = startSketchOn('XY')
|> startProfileAt([
rotorDiameter / 2 + caliperTolerance,
0

View File

@ -9,64 +9,61 @@
// Import Constants
import rotorDiameter, rotorInnerDiameter, rotorSinglePlateThickness, rotorInnerDiameterThickness, lugHolePatternDia, lugSpacing, rotorTotalThickness, spacerPatternDiameter, spacerDiameter, spacerLength, spacerCount, wheelDiameter, lugCount, yAxisOffset, drillAndSlotCount from "globals.kcl"
rotorPlane = {
plane = {
origin = { x = 0, y = yAxisOffset, z = 0 },
xAxis = { x = -1, y = 0, z = 0 },
yAxis = { x = 0, y = 0, z = 1 },
zAxis = { x = 0, y = 1, z = 0 }
}
}
fn lugPattern(plane) {
lugHolePattern = circle(
plane,
center = [-lugSpacing / 2, 0],
radius = 0.315
)
|> patternCircular2d(
arcDegrees = 360,
center = [0, 0],
instances = lugCount,
rotateDuplicates = true
)
return lugHolePattern
}
rotorSketch = startSketchOn(rotorPlane)
rotorSketch = startSketchOn('XZ')
|> circle(
center = [0, 0],
radius = rotorDiameter / 2
)
|> hole(lugPattern(%), %)
rotor = extrude(rotorSketch, length = rotorSinglePlateThickness)
|> appearance(color = "#dbcd70", roughness = 90, metalness = 90)
rotorBumpSketch = startSketchOn(rotorPlane)
rotorBumpSketch = startSketchOn(rotor, 'end')
|> circle(
center = [0, 0],
radius = rotorInnerDiameter / 2
)
|> hole(lugPattern(%), %)
rotorBump = extrude(rotorBumpSketch, length = -rotorInnerDiameterThickness)
rotorBump = extrude(rotorBumpSketch, length = rotorInnerDiameterThickness)
lugHoles = startSketchOn(rotorBump, 'end')
|> circle(
center = [-lugSpacing / 2, 0],
radius = 0.315
)
|> patternCircular2d(
arcDegrees = 360,
center = [0, 0],
instances = lugCount,
rotateDuplicates = true
)
|> extrude(%, length = -(rotorInnerDiameterThickness + rotorSinglePlateThickness))
|> appearance(color = "#dbcd70", roughness = 90, metalness = 90)
rotorSecondaryPlatePlane = {
plane = {
origin = {
x = 0,
y = yAxisOffset + rotorTotalThickness * 0.75,
z = 0
},
xAxis = { x = -1, y = 0, z = 0 },
yAxis = { x = 0, y = 0, z = 1 },
zAxis = { x = 0, y = 1, z = 0 }
}
}
secondaryRotorSketch = startSketchOn(rotorSecondaryPlatePlane)
// (update when boolean is available)
centerSpacer = startSketchOn(rotor, 'start')
|> circle(%, center = [0, 0], radius = .25)
|> extrude(%, length = spacerLength)
secondaryRotorSketch = startSketchOn(centerSpacer, 'end')
|> circle(
center = [0, 0],
radius = rotorDiameter / 2
)
|> hole(lugPattern(%), %)
secondRotor = extrude(secondaryRotorSketch, length = rotorSinglePlateThickness)
spacerSketch = startSketchOn(rotorSecondaryPlatePlane)
lugHoles2 = startSketchOn(secondRotor, 'end')
|> circle(
center = [-lugSpacing / 2, 0],
radius = 0.315
)
|> patternCircular2d(
arcDegrees = 360,
center = [0, 0],
instances = lugCount,
rotateDuplicates = true
)
|> extrude(length = -rotorSinglePlateThickness)
spacerSketch = startSketchOn(rotor, 'start')
|> circle(
center = [spacerPatternDiameter / 2, 0],
radius = spacerDiameter
@ -77,8 +74,8 @@ spacerSketch = startSketchOn(rotorSecondaryPlatePlane)
instances = spacerCount,
rotateDuplicates = true
)
spacers = extrude(spacerSketch, length = -spacerLength)
|> appearance(color = "#dbcd70", roughness = 90, metalness = 90)
spacers = extrude(spacerSketch, length = spacerLength)
rotorSlottedSketch = startSketchOn(rotor, 'START')
|> startProfileAt([2.17, 2.56], %)
|> xLine(length = 0.12)
@ -107,5 +104,6 @@ secondRotorSlottedSketch = startSketchOn(secondRotor, 'END')
arcDegrees = 360,
rotateDuplicates = true
)
secondRotorSlotted = extrude(secondRotorSlottedSketch, length = -rotorSinglePlateThickness / 2)
extrude(secondRotorSlottedSketch, length = -rotorSinglePlateThickness / 2)
|> appearance(color = "#dbcd70", roughness = 90, metalness = 90)

View File

@ -12,6 +12,7 @@ import 'car-tire.kcl' as carTire
import lugCount from 'globals.kcl'
carRotor
|> translate(translate = [0, 0.5, 0])
carWheel
lugNut
|> patternCircular3d(
@ -22,4 +23,5 @@ lugNut
rotateDuplicates = false
)
brakeCaliper
|> translate(translate = [0, 0.5, 0])
carTire

View File

@ -1,87 +0,0 @@
// Flange with XY coordinates
// 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.
// Set units
@settings(defaultLengthUnit = in)
// Define constants
mountingHoleDia = .625
baseDia = 4.625
pipeDia = 1.25
thickness = .625
totalThickness = 0.813
topTotalDiameter = 2.313
bottomThickness = 0.06
bottomTotalDiameter = 2.5
mountingHolePlacementDiameter = 3.5
baseThickness = .625
topTotalThickness = totalThickness - (bottomThickness + baseThickness)
holeLocator = baseDia - 8
nHoles = 4
// Add assertion so nHoles are always greater than 1
assertGreaterThan(nHoles, 1, "nHoles must be greater than 1")
// Create the flange base and the six mounting holes
flangeBase = startSketchOn('XY')
|> circle(
center = [0, 0],
radius = baseDia / 2
)
|> hole(circle(
center = [mountingHolePlacementDiameter / 2, 0],
radius = mountingHoleDia / 2
), %)
|> hole(circle(
center = [0, mountingHolePlacementDiameter / 2],
radius = mountingHoleDia / 2
), %)
|> hole(circle(
center = [-mountingHolePlacementDiameter / 2, 0],
radius = mountingHoleDia / 2
), %)
|> hole(circle(
center = [0, -mountingHolePlacementDiameter / 2],
radius = mountingHoleDia / 2
), %)
|> hole(circle(
center = [0, 0],
radius = pipeDia / 2
), %)
|> extrude(length = baseThickness)
// Plane for top face
topFacePlane = {
plane = {
origin = { x = 0, y = 0, z = baseThickness },
xAxis = { x = 1, y = 0, z = 0 },
yAxis = { x = 0, y = 1, z = 0 },
zAxis = { x = 0, y = 0, z = 1 }
}
}
// Create the extrusion on the top of the flange base
topExtrusion = startSketchOn(topFacePlane, 'end')
|> circle(
center = [0, 0],
radius = topTotalDiameter / 2
)
|> hole(circle(
center = [0, 0],
radius = pipeDia / 2
), %)
|> extrude(length = topTotalThickness)
// Create the extrusion on the bottom of the flange base
bottomExtrusion = startSketchOn("XY")
|> circle(
center = [0, 0],
radius = bottomTotalDiameter / 2
)
|> hole(circle(
center = [0, 0],
radius = pipeDia / 2
), %)
|> extrude(length = -bottomThickness)
// https://www.mcmaster.com/44685K193/

View File

@ -8,7 +8,6 @@
mountingHoleDia = .625
baseDia = 4.625
pipeDia = 1.25
thickness = .625
totalThickness = 0.813
topTotalDiameter = 2.313
bottomThickness = 0.06
@ -16,7 +15,6 @@ bottomTotalDiameter = 2.5
mountingHolePlacementDiameter = 3.5
baseThickness = .625
topTotalThickness = totalThickness - (bottomThickness + baseThickness)
holeLocator = baseDia - 8
nHoles = 4
// Add assertion so nHoles are always greater than 1
@ -42,42 +40,25 @@ flangeBase = startSketchOn('XY')
radius = baseDia / 2
)
|> hole(circles, %)
|> hole(circle(
center = [0, 0],
radius = pipeDia / 2
), %)
|> extrude(length = baseThickness)
// Plane for top face
topFacePlane = {
plane = {
origin = { x = 0, y = 0, z = baseThickness },
xAxis = { x = 1, y = 0, z = 0 },
yAxis = { x = 0, y = 1, z = 0 },
zAxis = { x = 0, y = 0, z = 1 }
}
}
// Create the extrusion on the top of the flange base
topExtrusion = startSketchOn(topFacePlane)
topExtrusion = startSketchOn(flangeBase, 'end')
|> circle(
center = [0, 0],
radius = topTotalDiameter / 2
)
|> hole(circle(
center = [0, 0],
radius = pipeDia / 2
), %)
|> extrude(length = topTotalThickness)
// Create the extrusion on the bottom of the flange base
bottomExtrusion = startSketchOn("XY")
bottomExtrusion = startSketchOn(flangeBase, 'start')
|> circle(
center = [0, 0],
radius = bottomTotalDiameter / 2
)
|> hole(circle(
center = [0, 0],
radius = pipeDia / 2
), %)
|> extrude(length = -bottomThickness)
|> extrude(length = bottomThickness)
// Cut a hole through the entire body
pipeHole = startSketchOn(topExtrusion, 'end')
|> circle(center = [0, 0], radius = pipeDia/2)
|> extrude(%, length = -(topTotalThickness + baseThickness + bottomThickness))

View File

@ -5,8 +5,8 @@
@settings(defaultLengthUnit = in)
// Define constants
lbumps = 5 // number of bumps long
wbumps = 3 // number of bumps wide
lbumps = 10 // number of bumps long
wbumps = 5 // number of bumps wide
pitch = 8.0
clearance = 0.1
bumpDiam = 4.8
@ -25,28 +25,8 @@ wSegments = totalWidth / wbumps
assertGreaterThan(lbumps, 1, "lbumps must be greater than 1")
assertGreaterThan(wbumps, 1, "wbumps must be greater than 1")
// Create the plane for the pegs. This is a hack so that the pegs can be patterned along the face of the lego base.
pegFace = {
plane = {
origin = { x = 0, y = 0, z = height },
xAxis = { x = 1, y = 0, z = 0 },
yAxis = { x = 0, y = 1, z = 0 },
zAxis = { x = 0, y = 0, z = 1 }
}
}
// Create the plane for the tubes underneath the lego. This is a hack so that the tubes can be patterned underneath the lego.
tubeFace = {
plane = {
origin = { x = 0, y = 0, z = height - t },
xAxis = { x = 1, y = 0, z = 0 },
yAxis = { x = 0, y = 1, z = 0 },
zAxis = { x = 0, y = 0, z = 1 }
}
}
// Make the base
s = startSketchOn('XY')
base = startSketchOn('XY')
|> startProfileAt([-totalWidth / 2, -totalLength / 2], %)
|> line(end = [totalWidth, 0])
|> line(end = [0, totalLength])
@ -54,8 +34,8 @@ s = startSketchOn('XY')
|> close()
|> extrude(length = height)
// Sketch and extrude a rectangular shape to create the shell underneath the lego. This is a hack until we have a shell function.
shellExtrude = startSketchOn(s, "start")
// Sketch and extrude a rectangular shape to create the shell underneath the lego. Will replace with shell function when able to call a face created from shell.
shellExtrude = startSketchOn(base, "start")
|> startProfileAt([
-(totalWidth / 2 - t),
-(totalLength / 2 - t)
@ -67,7 +47,7 @@ shellExtrude = startSketchOn(s, "start")
|> extrude(length = -(height - t))
// Create the pegs on the top of the base
peg = startSketchOn(s, 'end')
peg = startSketchOn(base, 'end')
|> circle(
center = [
-(pitch * (wbumps - 1) / 2),
@ -88,7 +68,7 @@ peg = startSketchOn(s, 'end')
|> extrude(length = bumpHeight)
// Create the pegs on the bottom of the base
tubePattern = startSketchOn(tubeFace)
tubePattern = startSketchOn(shellExtrude, 'start')
|> circle(
center = [
-(pitch * (wbumps - 1) / 2 - (pitch / 2)),
@ -106,4 +86,4 @@ tubePattern = startSketchOn(tubeFace)
instances = lbumps - 1,
distance = pitch
)
|> extrude(length = -bumpHeight)
|> extrude(length = bumpHeight)

View File

@ -1,11 +1,4 @@
[
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "3d-boaty/main.kcl",
"multipleFiles": true,
"title": "3D Boaty",
"description": "This is a slight remix of Depep1's original 3D Boaty (https://www.printables.com/model/1141963-3d-boaty). This is a tool used for benchmarking 3D FDM printers for bed adhesion, overhangs, bridging and top surface quality. The name of this file is a bit of misnomer, the shape of the object is a typical park bench."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "80-20-rail/main.kcl",
@ -27,6 +20,13 @@
"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": "bench/main.kcl",
"multipleFiles": true,
"title": "Bench",
"description": "This is a slight remix of Depep1's original 3D Boaty (https://www.printables.com/model/1141963-3d-boaty). This is a tool used for benchmarking 3D FDM printers for bed adhesion, overhangs, bridging and top surface quality. The name of this file is a bit of misnomer, the shape of the object is a typical park bench."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "bracket/main.kcl",
@ -78,18 +78,11 @@
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "flange-with-patterns/main.kcl",
"pathFromProjectDirectoryToFirstFile": "flange/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",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 69 KiB

View File

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 KiB

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 96 KiB

View File

@ -1,7 +1,7 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION((('zoo.dev export')), '2;1');
FILE_NAME('test.step', '2021-01-01T00:00:00Z', ('Test'), ('Zoo'), 'zoo.dev beta', 'zoo.dev', 'Test');
FILE_NAME('dump.step', '2021-01-01T00:00:00+00:00', ('Author unknown'), ('Organization unknown'), 'zoo.dev beta', 'zoo.dev', 'Authorization unknown');
FILE_SCHEMA(('AP203_CONFIGURATION_CONTROLLED_3D_DESIGN_OF_MECHANICAL_PARTS_AND_ASSEMBLIES_MIM_LF'));
ENDSEC;
DATA;

View File

@ -1,7 +1,7 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION((('zoo.dev export')), '2;1');
FILE_NAME('test.step', '2021-01-01T00:00:00Z', ('Test'), ('Zoo'), 'zoo.dev beta', 'zoo.dev', 'Test');
FILE_NAME('dump.step', '2021-01-01T00:00:00+00:00', ('Author unknown'), ('Organization unknown'), 'zoo.dev beta', 'zoo.dev', 'Authorization unknown');
FILE_SCHEMA(('AP203_CONFIGURATION_CONTROLLED_3D_DESIGN_OF_MECHANICAL_PARTS_AND_ASSEMBLIES_MIM_LF'));
ENDSEC;
DATA;

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,7 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION((('zoo.dev export')), '2;1');
FILE_NAME('test.step', '2021-01-01T00:00:00Z', ('Test'), ('Zoo'), 'zoo.dev beta', 'zoo.dev', 'Test');
FILE_NAME('dump.step', '2021-01-01T00:00:00+00:00', ('Author unknown'), ('Organization unknown'), 'zoo.dev beta', 'zoo.dev', 'Authorization unknown');
FILE_SCHEMA(('AP203_CONFIGURATION_CONTROLLED_3D_DESIGN_OF_MECHANICAL_PARTS_AND_ASSEMBLIES_MIM_LF'));
ENDSEC;
DATA;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,7 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION((('zoo.dev export')), '2;1');
FILE_NAME('test.step', '2021-01-01T00:00:00Z', ('Test'), ('Zoo'), 'zoo.dev beta', 'zoo.dev', 'Test');
FILE_NAME('dump.step', '2021-01-01T00:00:00+00:00', ('Author unknown'), ('Organization unknown'), 'zoo.dev beta', 'zoo.dev', 'Authorization unknown');
FILE_SCHEMA(('AP203_CONFIGURATION_CONTROLLED_3D_DESIGN_OF_MECHANICAL_PARTS_AND_ASSEMBLIES_MIM_LF'));
ENDSEC;
DATA;

View File

@ -1,7 +1,7 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION((('zoo.dev export')), '2;1');
FILE_NAME('test.step', '2021-01-01T00:00:00Z', ('Test'), ('Zoo'), 'zoo.dev beta', 'zoo.dev', 'Test');
FILE_NAME('dump.step', '2021-01-01T00:00:00+00:00', ('Author unknown'), ('Organization unknown'), 'zoo.dev beta', 'zoo.dev', 'Authorization unknown');
FILE_SCHEMA(('AP203_CONFIGURATION_CONTROLLED_3D_DESIGN_OF_MECHANICAL_PARTS_AND_ASSEMBLIES_MIM_LF'));
ENDSEC;
DATA;

View File

@ -1,7 +1,7 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION((('zoo.dev export')), '2;1');
FILE_NAME('test.step', '2021-01-01T00:00:00Z', ('Test'), ('Zoo'), 'zoo.dev beta', 'zoo.dev', 'Test');
FILE_NAME('dump.step', '2021-01-01T00:00:00+00:00', ('Author unknown'), ('Organization unknown'), 'zoo.dev beta', 'zoo.dev', 'Authorization unknown');
FILE_SCHEMA(('AP203_CONFIGURATION_CONTROLLED_3D_DESIGN_OF_MECHANICAL_PARTS_AND_ASSEMBLIES_MIM_LF'));
ENDSEC;
DATA;

View File

@ -1,7 +1,7 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION((('zoo.dev export')), '2;1');
FILE_NAME('test.step', '2021-01-01T00:00:00Z', ('Test'), ('Zoo'), 'zoo.dev beta', 'zoo.dev', 'Test');
FILE_NAME('dump.step', '2021-01-01T00:00:00+00:00', ('Author unknown'), ('Organization unknown'), 'zoo.dev beta', 'zoo.dev', 'Authorization unknown');
FILE_SCHEMA(('AP203_CONFIGURATION_CONTROLLED_3D_DESIGN_OF_MECHANICAL_PARTS_AND_ASSEMBLIES_MIM_LF'));
ENDSEC;
DATA;

View File

@ -1,7 +1,7 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION((('zoo.dev export')), '2;1');
FILE_NAME('test.step', '2021-01-01T00:00:00Z', ('Test'), ('Zoo'), 'zoo.dev beta', 'zoo.dev', 'Test');
FILE_NAME('dump.step', '2021-01-01T00:00:00+00:00', ('Author unknown'), ('Organization unknown'), 'zoo.dev beta', 'zoo.dev', 'Authorization unknown');
FILE_SCHEMA(('AP203_CONFIGURATION_CONTROLLED_3D_DESIGN_OF_MECHANICAL_PARTS_AND_ASSEMBLIES_MIM_LF'));
ENDSEC;
DATA;

View File

@ -1,7 +1,7 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION((('zoo.dev export')), '2;1');
FILE_NAME('test.step', '2021-01-01T00:00:00Z', ('Test'), ('Zoo'), 'zoo.dev beta', 'zoo.dev', 'Test');
FILE_NAME('dump.step', '2021-01-01T00:00:00+00:00', ('Author unknown'), ('Organization unknown'), 'zoo.dev beta', 'zoo.dev', 'Authorization unknown');
FILE_SCHEMA(('AP203_CONFIGURATION_CONTROLLED_3D_DESIGN_OF_MECHANICAL_PARTS_AND_ASSEMBLIES_MIM_LF'));
ENDSEC;
DATA;

View File

@ -1,7 +1,7 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION((('zoo.dev export')), '2;1');
FILE_NAME('test.step', '2021-01-01T00:00:00Z', ('Test'), ('Zoo'), 'zoo.dev beta', 'zoo.dev', 'Test');
FILE_NAME('dump.step', '2021-01-01T00:00:00+00:00', ('Author unknown'), ('Organization unknown'), 'zoo.dev beta', 'zoo.dev', 'Authorization unknown');
FILE_SCHEMA(('AP203_CONFIGURATION_CONTROLLED_3D_DESIGN_OF_MECHANICAL_PARTS_AND_ASSEMBLIES_MIM_LF'));
ENDSEC;
DATA;

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,7 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION((('zoo.dev export')), '2;1');
FILE_NAME('test.step', '2021-01-01T00:00:00Z', ('Test'), ('Zoo'), 'zoo.dev beta', 'zoo.dev', 'Test');
FILE_NAME('dump.step', '2021-01-01T00:00:00+00:00', ('Author unknown'), ('Organization unknown'), 'zoo.dev beta', 'zoo.dev', 'Authorization unknown');
FILE_SCHEMA(('AP203_CONFIGURATION_CONTROLLED_3D_DESIGN_OF_MECHANICAL_PARTS_AND_ASSEMBLIES_MIM_LF'));
ENDSEC;
DATA;

View File

@ -1,7 +1,7 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION((('zoo.dev export')), '2;1');
FILE_NAME('test.step', '2021-01-01T00:00:00Z', ('Test'), ('Zoo'), 'zoo.dev beta', 'zoo.dev', 'Test');
FILE_NAME('dump.step', '2021-01-01T00:00:00+00:00', ('Author unknown'), ('Organization unknown'), 'zoo.dev beta', 'zoo.dev', 'Authorization unknown');
FILE_SCHEMA(('AP203_CONFIGURATION_CONTROLLED_3D_DESIGN_OF_MECHANICAL_PARTS_AND_ASSEMBLIES_MIM_LF'));
ENDSEC;
DATA;

View File

@ -1,7 +1,7 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION((('zoo.dev export')), '2;1');
FILE_NAME('test.step', '2021-01-01T00:00:00Z', ('Test'), ('Zoo'), 'zoo.dev beta', 'zoo.dev', 'Test');
FILE_NAME('dump.step', '2021-01-01T00:00:00+00:00', ('Author unknown'), ('Organization unknown'), 'zoo.dev beta', 'zoo.dev', 'Authorization unknown');
FILE_SCHEMA(('AP203_CONFIGURATION_CONTROLLED_3D_DESIGN_OF_MECHANICAL_PARTS_AND_ASSEMBLIES_MIM_LF'));
ENDSEC;
DATA;

View File

@ -1,7 +1,7 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION((('zoo.dev export')), '2;1');
FILE_NAME('test.step', '2021-01-01T00:00:00Z', ('Test'), ('Zoo'), 'zoo.dev beta', 'zoo.dev', 'Test');
FILE_NAME('dump.step', '2021-01-01T00:00:00+00:00', ('Author unknown'), ('Organization unknown'), 'zoo.dev beta', 'zoo.dev', 'Authorization unknown');
FILE_SCHEMA(('AP203_CONFIGURATION_CONTROLLED_3D_DESIGN_OF_MECHANICAL_PARTS_AND_ASSEMBLIES_MIM_LF'));
ENDSEC;
DATA;

View File

@ -1,7 +1,7 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION((('zoo.dev export')), '2;1');
FILE_NAME('test.step', '2021-01-01T00:00:00Z', ('Test'), ('Zoo'), 'zoo.dev beta', 'zoo.dev', 'Test');
FILE_NAME('dump.step', '2021-01-01T00:00:00+00:00', ('Author unknown'), ('Organization unknown'), 'zoo.dev beta', 'zoo.dev', 'Authorization unknown');
FILE_SCHEMA(('AP203_CONFIGURATION_CONTROLLED_3D_DESIGN_OF_MECHANICAL_PARTS_AND_ASSEMBLIES_MIM_LF'));
ENDSEC;
DATA;

View File

@ -1,7 +1,7 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION((('zoo.dev export')), '2;1');
FILE_NAME('test.step', '2021-01-01T00:00:00Z', ('Test'), ('Zoo'), 'zoo.dev beta', 'zoo.dev', 'Test');
FILE_NAME('dump.step', '2021-01-01T00:00:00+00:00', ('Author unknown'), ('Organization unknown'), 'zoo.dev beta', 'zoo.dev', 'Authorization unknown');
FILE_SCHEMA(('AP203_CONFIGURATION_CONTROLLED_3D_DESIGN_OF_MECHANICAL_PARTS_AND_ASSEMBLIES_MIM_LF'));
ENDSEC;
DATA;

View File

@ -1,7 +1,7 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION((('zoo.dev export')), '2;1');
FILE_NAME('test.step', '2021-01-01T00:00:00Z', ('Test'), ('Zoo'), 'zoo.dev beta', 'zoo.dev', 'Test');
FILE_NAME('dump.step', '2021-01-01T00:00:00+00:00', ('Author unknown'), ('Organization unknown'), 'zoo.dev beta', 'zoo.dev', 'Authorization unknown');
FILE_SCHEMA(('AP203_CONFIGURATION_CONTROLLED_3D_DESIGN_OF_MECHANICAL_PARTS_AND_ASSEMBLIES_MIM_LF'));
ENDSEC;
DATA;

View File

@ -1,7 +1,7 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION((('zoo.dev export')), '2;1');
FILE_NAME('test.step', '2021-01-01T00:00:00Z', ('Test'), ('Zoo'), 'zoo.dev beta', 'zoo.dev', 'Test');
FILE_NAME('dump.step', '2021-01-01T00:00:00+00:00', ('Author unknown'), ('Organization unknown'), 'zoo.dev beta', 'zoo.dev', 'Authorization unknown');
FILE_SCHEMA(('AP203_CONFIGURATION_CONTROLLED_3D_DESIGN_OF_MECHANICAL_PARTS_AND_ASSEMBLIES_MIM_LF'));
ENDSEC;
DATA;

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