Compare commits

...

24 Commits

Author SHA1 Message Date
4a5ef17c92 Remove extra console error that shouldn't be needed anymore 2025-04-18 17:23:54 -04:00
83220a7255 Add text to the error message when JSON parse fails 2025-04-18 17:19:50 -04:00
a0f3f0f50e Fix KCLError to have a real message (#6396)
* Fix KCLError to have a real message

* Remove unneeded error exception
2025-04-18 17:13:09 -04:00
dad0d5488f Support any installation path for rust toolchain (#6380) 2025-04-18 20:28:38 +00:00
77773fb052 ci: Add error to console exception (#6392)
* Add error to console exception

* Ignore in codespell

* Increase retry on snapshots

* Fixme's all around

---------

Co-authored-by: Jace Browning <jacebrowning@gmail.com>
Co-authored-by: Frank Noirot <frankjohnson1993@gmail.com>
2025-04-18 15:51:13 -04:00
1240ca6f21 Revert "Update onboarding interactive numbers test to use editor fixture" (#6393)
Revert "Update onboarding interactive numbers test to use editor fixture (#6356)"

This reverts commit c77bb92da9.
2025-04-18 14:57:40 -04:00
ce3a6ad286 Revert "Remove all use of bold mono font" (#6394)
Revert "Remove all use of bold mono font (#6373)"

This reverts commit 03c4121ab7.
2025-04-18 14:57:10 -04:00
a20f420a55 Add fixmes to failing e2e tests (#6388)
* Add fixmes to bad snapshots tests

* max attempts to 5 on snapshots

* Skip revolve on windows

* Another one

* Add fixme to edit with ML snapshot test

* fmt

* Use absolute import, fix lint

* Disable another test

---------

Co-authored-by: Frank Noirot <frankjohnson1993@gmail.com>
2025-04-18 14:14:23 -04:00
03c4121ab7 Remove all use of bold mono font (#6373)
* Remove all use of bold mono font
A few mono font points are left, mostly for numbers which look better in
monospace.

* make React happy with `clipRule`
2025-04-18 14:11:20 -04:00
8c2c877817 Bump crossbeam-channel from 0.5.14 to 0.5.15 in /rust in the security group (#6354)
Bump crossbeam-channel in /rust in the security group

Bumps the security group in /rust with 1 update: [crossbeam-channel](https://github.com/crossbeam-rs/crossbeam).


Updates `crossbeam-channel` from 0.5.14 to 0.5.15
- [Release notes](https://github.com/crossbeam-rs/crossbeam/releases)
- [Changelog](https://github.com/crossbeam-rs/crossbeam/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crossbeam-rs/crossbeam/compare/crossbeam-channel-0.5.14...crossbeam-channel-0.5.15)

---
updated-dependencies:
- dependency-name: crossbeam-channel
  dependency-version: 0.5.15
  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-04-18 14:10:54 -04:00
c77bb92da9 Update onboarding interactive numbers test to use editor fixture (#6356)
Trying to bring it down from the leaderboard of failed tests.

Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
2025-04-18 14:06:23 -04:00
bd4bad0020 allow sending async commands to engine (#6342)
* start of async

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

check at end if the async commands completed

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

run at the end of inner_run

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

set import as async

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

updates

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

updates

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

add to the wasm side

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

updates

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

fmt

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

* fire

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

* flake

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

* fixup for awaiting on import

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 mock

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

* fix mock

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>

* add a test where we import then do a bunch of other stuff

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

* updates

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

* fixup to see

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

* fixups

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

* fix tests

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

* updates

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

* cross platform time

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>

* another appearance tests

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

* new docs and tests

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

* updates

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

* dont loop so tight

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

* fixes

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-04-17 17:22:19 -07:00
0b9889e313 Double the timeout in sketch editing test (#6376) 2025-04-17 14:32:11 -04:00
f9fbaa2298 Add files via upload (#6227)
* Add files via upload

Adding parametric pc fan and bottle

* Update and rename globals.kcl to parameters.kcl

* Update fan-housing.kcl

* Update fan-housing.kcl

* Update fan.kcl

* Update motor.kcl

* Update parameters.kcl

* Update kcl-samples simulation test output

* Update main.kcl

avoiding fn imports

* Update kcl-samples simulation test output

* remove functions

* angledLine kwargs

* tangentalArc kwargs

* Update kcl-samples simulation test output

* Update housing middle

more tweaks because I just can't help myself

* Update kcl-samples simulation test output

* Update kcl-samples simulation test output

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: jgomez720 <114548659+jgomez720@users.noreply.github.com>
2025-04-17 17:46:56 +00:00
6f2d127c4f Assemblies: Set translate and rotate via point-and-click (#6167)
* WIP: Add point-and-click Import for geometry
Will eventually fix #6120
Right now the whole loop is there but the codemod doesn't work yet

* Better pathToNOde, log on non-working cm dispatch call

* Add workaround to updateModelingState not working

* Back to updateModelingState with a skip flag

* Better todo

* Change working from Import to Insert, cleanups

* Sister command in kclCommands to populate file options

* Improve path selector

* Unsure: move importAstMod to kclCommands onSubmit 😶

* Add e2e test

* Clean up for review

* Add native file menu entry and test

* No await yo lint said so

* WIP: UX improvements around foreign file imports
Fixes #6152

* WIP: Set translate and rotate via point-and-click on imports. Boilerplate code
Will eventually close #6020

* Full working loop of rotate and translate pipe mutation, including edits, only on module imports. VERY VERBOSE

* Add first e2e test for set transform. Bunch of caveats listed as TODOs

* @lrev-Dev's suggestion to remove a comment

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>

* Update to scene.settled(cmdBar)

* Add partNNN default name for alias

* Lint

* Lint

* Fix unit tests

* Add sad path insert test
Thanks @Irev-Dev for the suggestion

* Add step insert test

* Lint

* Add test for second foreign import thru file tree click

* WIP: Add point-and-click Load to copy files from outside the project into the project
Towards #6210

* Move Insert button to modeling toolbar, update menus and toolbars

* Add default value for local name alias

* Aligning tests

* Fix tests

* Add padding for filenames starting with a digit

* Lint

* Lint

* Update snapshots

* Merge branch 'main' into pierremtb/issue6210-Add-point-and-click-Load-to-copy-files-from-outside-the-project-into-the-project

* Add disabled transform subbutton

* Allow start of Transform flow from toolbar with selection

* Merge kcl-samples and local disk load into one 'Load external model' command

* Fix em tests

* Fix test

* Add test for file pick import, better input

* Fix non .kcl loading

* Lint

* Update snapshots

* Fix issue leading to test failure

* Fix clone test

* Add note

* Fix nested clone issue

* Clean up for review

* Add valueSummary for path

* Fix test after path change

* Clean up for review

* Support much wider range for transform

* Set display names

* Bug fixed itself moment...

* Add test for extrude tranform

* Oops missed a thing

* Clean up selection arg

* More tests incl for variable stuff

* Fix imports

* Add supportsTransform: true on all solids returning operations

* Fix edit flow on variables, add test

* Split transform command into translate and rotate

* Clean up and comment

* Clean up operations.ts

* Add comment

* Improve assemblies test

* Support more things

* Typo

* Fix test after unit change on import

* Last clean up for review

* Fix remaining test

---------

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2025-04-17 15:44:31 +00:00
056a4d4a22 [FIX]: "Connecting to engine" overlay shows too often (#6350)
* Use the engineStreamState that's available right there silly

* fix lint

* Just look at playing/paused

---------

Co-authored-by: Jace Browning <jacebrowning@gmail.com>
2025-04-17 12:49:31 +00:00
bab79331cb #6367 Tangent snapping improvements (#6369)
* fix snapping line being culled

* make snap line more grayed out

* make snapping tolerance smaller
2025-04-17 08:26:05 -04:00
16bbc970ae fix sketch flash part 2 (#6366) 2025-04-17 08:15:54 -04:00
0f1cff316c add toast for selections we don't recognise (#6370)
* ad toast for selections we don't recognise

* remove log
2025-04-17 21:32:13 +10:00
938a2bae13 Fix to not duplicate arg labels on constrained sketches (#6337) 2025-04-17 07:16:55 +00:00
3324835b72 ci: Fix so that just commands regenerate ast output (#6338)
* Fix so that just commands regenerate ast output

* Update just command to include manifest

* Fix overwrite-sim-test kcl_samples to generate manifest

This is the command used in CI.
2025-04-17 06:03:58 +00:00
1628ea86b2 Remove pixel color checks (#6365) 2025-04-17 14:25:45 +10:00
be119248a6 put execution benchmarks behind flag (#6364)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-04-16 21:23:30 -07:00
ac75181f7f fix bug and remove flash in sketch mode (#6346)
* fix bug and remove flash

* add test

* fix tests

* fix tests
2025-04-17 10:10:27 +10:00
103 changed files with 5695084 additions and 354 deletions

View File

@ -1,3 +1,3 @@
[codespell]
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,atleast,ue,afterall,ser,fromM,FromM
skip: **/target,node_modules,build,dist,./out,**/Cargo.lock,./docs/kcl/*.md,.package-lock.json,**/package-lock.json,./openapi/*.json,./packages/codemirror-lang-kcl/test/all.test.ts,./public/kcl-samples,./rust/kcl-lib/tests/kcl_samples,tsconfig.tsbuildinfo,./src/lib/machine-api.d.ts
skip: **/target,node_modules,build,dist,./out,**/Cargo.lock,./docs/kcl/*.md,./e2e/playwright/lib/console-error-whitelist.ts,.package-lock.json,**/package-lock.json,./openapi/*.json,./packages/codemirror-lang-kcl/test/all.test.ts,./public/kcl-samples,./rust/kcl-lib/tests/kcl_samples,tsconfig.tsbuildinfo,./src/lib/machine-api.d.ts

View File

@ -50,13 +50,12 @@ jobs:
- name: Build the benchmark target(s)
run: |
cd rust
cargo codspeed build --measurement-mode walltime
cargo codspeed build
- 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 }}

View File

@ -226,8 +226,8 @@ jobs:
with:
shell: bash
command: npm run test:snapshots
timeout_minutes: 30
max_attempts: 3
timeout_minutes: 5
max_attempts: 5
env:
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
snapshottoken: ${{ secrets.KITTYCAD_API_TOKEN }}

View File

@ -15,8 +15,8 @@ ifdef WINDOWS
CARGO ?= $(USERPROFILE)/.cargo/bin/cargo.exe
WASM_PACK ?= $(USERPROFILE)/.cargo/bin/wasm-pack.exe
else
CARGO ?= ~/.cargo/bin/cargo
WASM_PACK ?= ~/.cargo/bin/wasm-pack
CARGO ?= $(shell which cargo || echo ~/.cargo/bin/cargo)
WASM_PACK ?= $(shell which wasm-pack || echo ~/.cargo/bin/wasm-pack)
endif
.PHONY: install

File diff suppressed because one or more lines are too long

View File

@ -109,3 +109,98 @@ Coordinate systems:
- `zoo` (the default), forward: -Y, up: +Z, handedness: right
- `opengl`, forward: +Z, up: +Y, handedness: right
- `vulkan`, forward: +Z, up: -Y, handedness: left
### Performance
Parallelized foreign-file imports now let you overlap file reads, initialization,
and rendering. To maximize throughput, you need to understand the three distinct
stages—reading, initializing (background render start), and invocation (blocking)
—and structure your code to defer blocking operations until the end.
#### Foreign Import Execution Stages
1. **Import (Read) Stage**
```norun
import "tests/inputs/cube.step" as cube
```
- Reads the file from disk and makes its API available.
- **Does _not_** start Engine rendering or block your script.
2. **Initialization (Background Render) Stage**
```norun
import "tests/inputs/cube.step" as cube
myCube = cube // <- This line starts background rendering
```
- Invoking the imported symbol (assignment or plain call) triggers Engine rendering _in the background_.
- This kickstarts the render pipeline but doesnt block—you can continue other work while the Engine processes the model.
3. **Invocation (Blocking) Stage**
```norun
import "tests/inputs/cube.step" as cube
myCube = cube
myCube
|> translate(z=10) // <- This line blocks
```
- Any method call (e.g., `translate`, `scale`, `rotate`) waits for the background render to finish before applying transformations.
- This is the only point where your script will block.
> **Nuance:** Foreign imports differ from pure KCL modules—calling the same import symbol multiple times (e.g., `screw` twice) starts background rendering twice.
#### Best Practices
##### 1. Defer Blocking Calls
Initialize early but delay all transformations until after your heavy computation:
```norun
import "tests/inputs/cube.step" as cube // 1) Read
myCube = cube // 2) Background render starts
// --- perform other operations and calculations or setup here ---
myCube
|> translate(z=10) // 3) Blocks only here
```
##### 2. Encapsulate Imports in Modules
Keep `main.kcl` free of reads and initialization; wrap them:
```norun
// imports.kcl
import "tests/inputs/cube.step" as cube // Read only
export myCube = cube // Kick off rendering
```
```norun
// main.kcl
import myCube from "imports.kcl" // Import the initialized object
// ... computations ...
myCube
|> translate(z=10) // Blocking call at the end
```
##### 3. Avoid Immediate Method Calls
```norun
import "tests/inputs/cube.step" as cube
cube
|> translate(z=10) // Blocks immediately, negating parallelism
```
Both calling methods right on `cube` immediately or leaving an implicit import without assignment introduce blocking.
#### Future Improvements
Upcoming releases will autoanalyze dependencies and only block when truly necessary. Until then, explicit deferral and modular wrapping give you the best performance.

View File

@ -53,7 +53,7 @@ rotate(
### Returns
[`SolidOrSketchOrImportedGeometry`](/docs/kcl/types/SolidOrSketchOrImportedGeometry) - Data for a solid or an imported geometry.
[`SolidOrSketchOrImportedGeometry`](/docs/kcl/types/SolidOrSketchOrImportedGeometry) - Data for a solid, sketch, or an imported geometry.
### Examples

View File

@ -37,7 +37,7 @@ scale(
### Returns
[`SolidOrSketchOrImportedGeometry`](/docs/kcl/types/SolidOrSketchOrImportedGeometry) - Data for a solid or an imported geometry.
[`SolidOrSketchOrImportedGeometry`](/docs/kcl/types/SolidOrSketchOrImportedGeometry) - Data for a solid, sketch, or an imported geometry.
### Examples

View File

@ -28101,14 +28101,62 @@
"args": [
{
"name": "solids",
"type": "[Solid]",
"type": "SolidOrImportedGeometry",
"schema": {
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
"title": "Array_of_Solid",
"type": "array",
"items": {
"$ref": "#/components/schemas/Solid"
},
"title": "SolidOrImportedGeometry",
"description": "Data for a solid or an imported geometry.",
"oneOf": [
{
"description": "Data for an imported geometry.",
"type": "object",
"required": [
"id",
"type",
"value"
],
"properties": {
"type": {
"type": "string",
"enum": [
"importedGeometry"
]
},
"id": {
"description": "The ID of the imported geometry.",
"type": "string",
"format": "uuid"
},
"value": {
"description": "The original file paths.",
"type": "array",
"items": {
"type": "string"
}
}
}
},
{
"type": [
"object",
"array"
],
"items": {
"$ref": "#/components/schemas/Solid"
},
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"solidSet"
]
}
}
}
],
"definitions": {
"Solid": {
"type": "object",
@ -34571,14 +34619,62 @@
],
"returnValue": {
"name": "",
"type": "[Solid]",
"type": "SolidOrImportedGeometry",
"schema": {
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
"title": "Array_of_Solid",
"type": "array",
"items": {
"$ref": "#/components/schemas/Solid"
},
"title": "SolidOrImportedGeometry",
"description": "Data for a solid or an imported geometry.",
"oneOf": [
{
"description": "Data for an imported geometry.",
"type": "object",
"required": [
"id",
"type",
"value"
],
"properties": {
"type": {
"type": "string",
"enum": [
"importedGeometry"
]
},
"id": {
"description": "The ID of the imported geometry.",
"type": "string",
"format": "uuid"
},
"value": {
"description": "The original file paths.",
"type": "array",
"items": {
"type": "string"
}
}
}
},
{
"type": [
"object",
"array"
],
"items": {
"$ref": "#/components/schemas/Solid"
},
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"solidSet"
]
}
}
}
],
"definitions": {
"Solid": {
"type": "object",
@ -36198,7 +36294,8 @@
"// Setting the appearance of a 3D pattern can be done _before_ or _after_ the pattern.\n// This example shows _before_ the pattern.\nexampleSketch = startSketchOn(XZ)\n |> startProfileAt([0, 0], %)\n |> line(end = [0, 2])\n |> line(end = [3, 1])\n |> line(end = [0, -4])\n |> close()\n\nexample = extrude(exampleSketch, length = 1)\n |> appearance(color = '#ff0000', metalness = 90, roughness = 90)\n |> patternLinear3d(axis = [1, 0, 1], instances = 7, distance = 6)",
"// Setting the appearance of a 3D pattern can be done _before_ or _after_ the pattern.\n// This example shows _after_ the pattern.\nexampleSketch = startSketchOn(XZ)\n |> startProfileAt([0, 0], %)\n |> line(end = [0, 2])\n |> line(end = [3, 1])\n |> line(end = [0, -4])\n |> close()\n\nexample = extrude(exampleSketch, length = 1)\n |> patternLinear3d(axis = [1, 0, 1], instances = 7, distance = 6)\n |> appearance(color = '#ff0000', metalness = 90, roughness = 90)",
"// Color the result of a 2D pattern that was extruded.\nexampleSketch = startSketchOn(XZ)\n |> startProfileAt([.5, 25], %)\n |> line(end = [0, 5])\n |> line(end = [-1, 0])\n |> line(end = [0, -5])\n |> close()\n |> patternCircular2d(\n center = [0, 0],\n instances = 13,\n arcDegrees = 360,\n rotateDuplicates = true,\n )\n\nexample = extrude(exampleSketch, length = 1)\n |> appearance(color = '#ff0000', metalness = 90, roughness = 90)",
"// Color the result of a sweep.\n\n// Create a path for the sweep.\nsweepPath = startSketchOn(XZ)\n |> startProfileAt([0.05, 0.05], %)\n |> line(end = [0, 7])\n |> tangentialArc(angle = 90, radius = 5)\n |> line(end = [-3, 0])\n |> tangentialArc(angle = -90, radius = 5)\n |> line(end = [0, 7])\n\npipeHole = startSketchOn(XY)\n |> circle(center = [0, 0], radius = 1.5)\n\nsweepSketch = startSketchOn(XY)\n |> circle(center = [0, 0], radius = 2)\n |> hole(pipeHole, %)\n |> sweep(path = sweepPath)\n |> appearance(color = \"#ff0000\", metalness = 50, roughness = 50)"
"// Color the result of a sweep.\n\n// Create a path for the sweep.\nsweepPath = startSketchOn(XZ)\n |> startProfileAt([0.05, 0.05], %)\n |> line(end = [0, 7])\n |> tangentialArc(angle = 90, radius = 5)\n |> line(end = [-3, 0])\n |> tangentialArc(angle = -90, radius = 5)\n |> line(end = [0, 7])\n\npipeHole = startSketchOn(XY)\n |> circle(center = [0, 0], radius = 1.5)\n\nsweepSketch = startSketchOn(XY)\n |> circle(center = [0, 0], radius = 2)\n |> hole(pipeHole, %)\n |> sweep(path = sweepPath)\n |> appearance(color = \"#ff0000\", metalness = 50, roughness = 50)",
"// Change the appearance of an imported model.\n\n\nimport \"tests/inputs/cube.sldprt\" as cube\n\ncube\n// |> appearance(\n// color = \"#ff0000\",\n// metalness = 50,\n// roughness = 50\n// )"
]
},
{
@ -249578,7 +249675,7 @@
"schema": {
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
"title": "SolidOrSketchOrImportedGeometry",
"description": "Data for a solid or an imported geometry.",
"description": "Data for a solid, sketch, or an imported geometry.",
"oneOf": [
{
"description": "Data for an imported geometry.",
@ -260975,7 +261072,7 @@
"schema": {
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
"title": "SolidOrSketchOrImportedGeometry",
"description": "Data for a solid or an imported geometry.",
"description": "Data for a solid, sketch, or an imported geometry.",
"oneOf": [
{
"description": "Data for an imported geometry.",
@ -262721,7 +262818,7 @@
"schema": {
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
"title": "SolidOrSketchOrImportedGeometry",
"description": "Data for a solid or an imported geometry.",
"description": "Data for a solid, sketch, or an imported geometry.",
"oneOf": [
{
"description": "Data for an imported geometry.",
@ -270879,7 +270976,7 @@
"schema": {
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
"title": "SolidOrSketchOrImportedGeometry",
"description": "Data for a solid or an imported geometry.",
"description": "Data for a solid, sketch, or an imported geometry.",
"oneOf": [
{
"description": "Data for an imported geometry.",
@ -319774,7 +319871,7 @@
"schema": {
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
"title": "SolidOrSketchOrImportedGeometry",
"description": "Data for a solid or an imported geometry.",
"description": "Data for a solid, sketch, or an imported geometry.",
"oneOf": [
{
"description": "Data for an imported geometry.",
@ -327932,7 +328029,7 @@
"schema": {
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
"title": "SolidOrSketchOrImportedGeometry",
"description": "Data for a solid or an imported geometry.",
"description": "Data for a solid, sketch, or an imported geometry.",
"oneOf": [
{
"description": "Data for an imported geometry.",

View File

@ -33,7 +33,7 @@ translate(
### Returns
[`SolidOrSketchOrImportedGeometry`](/docs/kcl/types/SolidOrSketchOrImportedGeometry) - Data for a solid or an imported geometry.
[`SolidOrSketchOrImportedGeometry`](/docs/kcl/types/SolidOrSketchOrImportedGeometry) - Data for a solid, sketch, or an imported geometry.
### Examples

View File

@ -1,10 +1,10 @@
---
title: "SolidOrSketchOrImportedGeometry"
excerpt: "Data for a solid or an imported geometry."
excerpt: "Data for a solid, sketch, or an imported geometry."
layout: manual
---
Data for a solid or an imported geometry.
Data for a solid, sketch, or an imported geometry.

View File

@ -178,6 +178,13 @@ export class CmdBarFixture {
return this.page.getByRole('option', options)
}
/**
* Clicks the Create new variable button for kcl input
*/
createNewVariable = async () => {
await this.page.getByRole('button', { name: 'Create new variable' }).click()
}
/**
* Captures a snapshot of the request sent to the text-to-cad API endpoint
* and saves it to a file named after the current test.

View File

@ -257,6 +257,14 @@ export class SceneFixture {
await expectPixelColor(this.page, colour, coords, diff)
}
expectPixelColorNotToBe = async (
colour: [number, number, number] | [number, number, number][],
coords: { x: number; y: number },
diff: number
) => {
await expectPixelColorNotToBe(this.page, colour, coords, diff)
}
get gizmo() {
return this.page.locator('[aria-label*=gizmo]')
}
@ -278,37 +286,69 @@ function isColourArray(
return isArray(colour[0])
}
export async function expectPixelColor(
type PixelColorMatchMode = 'matches' | 'differs'
export async function checkPixelColor(
page: Page,
colour: [number, number, number] | [number, number, number][],
coords: { x: number; y: number },
diff: number
diff: number,
mode: PixelColorMatchMode
) {
let finalValue = colour
const isMatchMode = mode === 'matches'
const actionText = isMatchMode ? 'expecting' : 'not expecting'
const functionName = isMatchMode
? 'ExpectPixelColor'
: 'ExpectPixelColourNotToBe'
await expect
.poll(
async () => {
const pixel = (await getPixelRGBs(page)(coords, 1))[0]
if (!pixel) return null
finalValue = pixel
let matches
if (!isColourArray(colour)) {
return pixel.every(
matches = pixel.every(
(channel, index) => Math.abs(channel - colour[index]) < diff
)
} else {
matches = colour.some((c) =>
c.every((channel, index) => Math.abs(pixel[index] - channel) < diff)
)
}
return colour.some((c) =>
c.every((channel, index) => Math.abs(pixel[index] - channel) < diff)
)
return isMatchMode ? matches : !matches
},
{ timeout: 10_000 }
)
.toBeTruthy()
.catch((cause) => {
throw new Error(
`ExpectPixelColor: point ${JSON.stringify(
`${functionName}: point ${JSON.stringify(
coords
)} was expecting ${colour} but got ${finalValue}`,
)} was ${actionText} ${colour} but got ${finalValue}`,
{ cause }
)
})
}
export async function expectPixelColor(
page: Page,
colour: [number, number, number] | [number, number, number][],
coords: { x: number; y: number },
diff: number
) {
await checkPixelColor(page, colour, coords, diff, 'matches')
}
export async function expectPixelColorNotToBe(
page: Page,
colour: [number, number, number] | [number, number, number][],
coords: { x: number; y: number },
diff: number
) {
await checkPixelColor(page, colour, coords, diff, 'differs')
}

View File

@ -169,6 +169,180 @@ test.describe('Point-and-click assemblies tests', () => {
}
)
test(
`Insert the bracket part into an assembly and transform it`,
{ tag: ['@electron'] },
async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
tronApp,
}) => {
if (!tronApp) {
fail()
}
const midPoint = { x: 500, y: 250 }
const moreToTheRightPoint = { x: 900, y: 250 }
const bgColor: [number, number, number] = [30, 30, 30]
const partColor: [number, number, number] = [100, 100, 100]
const tolerance = 30
const u = await getUtils(page)
const gizmo = page.locator('[aria-label*=gizmo]')
const resetCameraButton = page.getByRole('button', { name: 'Reset view' })
await test.step('Setup parts and expect empty assembly scene', async () => {
const projectName = 'assembly'
await context.folderSetupFn(async (dir) => {
const bracketDir = path.join(dir, projectName)
await fsp.mkdir(bracketDir, { recursive: true })
await Promise.all([
fsp.copyFile(
path.join('public', 'kcl-samples', 'bracket', 'main.kcl'),
path.join(bracketDir, 'bracket.kcl')
),
fsp.writeFile(path.join(bracketDir, 'main.kcl'), ''),
])
})
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.openProject(projectName)
await scene.settled(cmdBar)
await toolbar.closePane('code')
})
await test.step('Insert kcl as module', async () => {
await insertPartIntoAssembly(
'bracket.kcl',
'bracket',
toolbar,
cmdBar,
page
)
await toolbar.openPane('code')
await editor.expectEditor.toContain(
`
import "bracket.kcl" as bracket
bracket
`,
{ shouldNormalise: true }
)
await scene.settled(cmdBar)
// Check scene for changes
await toolbar.closePane('code')
await u.doAndWaitForCmd(async () => {
await gizmo.click({ button: 'right' })
await resetCameraButton.click()
}, 'zoom_to_fit')
await toolbar.closePane('debug')
await scene.expectPixelColor(partColor, midPoint, tolerance)
await scene.expectPixelColor(bgColor, moreToTheRightPoint, tolerance)
})
await test.step('Set translate on module', async () => {
await toolbar.openPane('feature-tree')
const op = await toolbar.getFeatureTreeOperation('bracket', 0)
await op.click({ button: 'right' })
await page.getByTestId('context-menu-set-translate').click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'x',
currentArgValue: '0',
headerArguments: {
X: '',
Y: '',
Z: '',
},
highlightedHeaderArg: 'x',
commandName: 'Translate',
})
await page.keyboard.insertText('5')
await cmdBar.progressCmdBar()
await page.keyboard.insertText('0.1')
await cmdBar.progressCmdBar()
await page.keyboard.insertText('0.2')
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: {
X: '5',
Y: '0.1',
Z: '0.2',
},
commandName: 'Translate',
})
await cmdBar.progressCmdBar()
await toolbar.closePane('feature-tree')
await toolbar.openPane('code')
await editor.expectEditor.toContain(
`
bracket
|> translate(x = 5, y = 0.1, z = 0.2)
`,
{ shouldNormalise: true }
)
// Expect translated part in the scene
await scene.expectPixelColor(bgColor, midPoint, tolerance)
await scene.expectPixelColor(partColor, moreToTheRightPoint, tolerance)
})
await test.step('Set rotate on module', async () => {
await toolbar.closePane('code')
await toolbar.openPane('feature-tree')
const op = await toolbar.getFeatureTreeOperation('bracket', 0)
await op.click({ button: 'right' })
await page.getByTestId('context-menu-set-rotate').click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'roll',
currentArgValue: '0',
headerArguments: {
Roll: '',
Pitch: '',
Yaw: '',
},
highlightedHeaderArg: 'roll',
commandName: 'Rotate',
})
await page.keyboard.insertText('0.1')
await cmdBar.progressCmdBar()
await page.keyboard.insertText('0.2')
await cmdBar.progressCmdBar()
await page.keyboard.insertText('0.3')
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Roll: '0.1',
Pitch: '0.2',
Yaw: '0.3',
},
commandName: 'Rotate',
})
await cmdBar.progressCmdBar()
await toolbar.closePane('feature-tree')
await toolbar.openPane('code')
await editor.expectEditor.toContain(
`
bracket
|> translate(x = 5, y = 0.1, z = 0.2)
|> rotate(roll = 0.1, pitch = 0.2, yaw = 0.3)
`,
{ shouldNormalise: true }
)
// Expect no change in the scene as the rotations are tiny
await scene.expectPixelColor(bgColor, midPoint, tolerance)
await scene.expectPixelColor(partColor, moreToTheRightPoint, tolerance)
})
}
)
test(
`Insert foreign parts into assembly as whole module import`,
{ tag: ['@electron'] },
@ -231,10 +405,6 @@ test.describe('Point-and-click assemblies tests', () => {
)
await scene.settled(cmdBar)
// TODO: remove this once #5780 is fixed
await page.reload()
await scene.settled(cmdBar)
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
await toolbar.closePane('code')
await scene.expectPixelColor(partColor, partPoint, tolerance)
@ -279,10 +449,6 @@ test.describe('Point-and-click assemblies tests', () => {
)
await scene.settled(cmdBar)
// TODO: remove this once #5780 is fixed
await page.reload()
await scene.settled(cmdBar)
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
await toolbar.closePane('code')
await scene.expectPixelColor(partColor, partPoint, tolerance)

View File

@ -5,7 +5,10 @@ import type { Locator, Page } from '@playwright/test'
import type { EditorFixture } from '@e2e/playwright/fixtures/editorFixture'
import type { SceneFixture } from '@e2e/playwright/fixtures/sceneFixture'
import type { ToolbarFixture } from '@e2e/playwright/fixtures/toolbarFixture'
import { orRunWhenFullSuiteEnabled } from '@e2e/playwright/test-utils'
import {
orRunWhenFullSuiteEnabled,
runningOnWindows,
} from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test'
// test file is for testing point an click code gen functionality that's not sketch mode related
@ -3544,6 +3547,9 @@ tag=$rectangleSegmentC002,
toolbar,
cmdBar,
}) => {
if (runningOnWindows()) {
test.fixme(orRunWhenFullSuiteEnabled())
}
const initialCode = `sketch001 = startSketchOn(XZ)
|> startProfileAt([-102.57, 101.72], %)
|> angledLine(angle = 0, length = 202.6, tag = $rectangleSegmentA001)
@ -3835,4 +3841,469 @@ extrude001 = extrude(profile001, length = 100)
)
})
})
const translateExtrudeCases: { variables: boolean }[] = [
{
variables: false,
},
{
variables: true,
},
]
translateExtrudeCases.map(({ variables }) => {
test(`Set translate on extrude through right-click menu (variables: ${variables})`, async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
const initialCode = `sketch001 = startSketchOn(XZ)
profile001 = circle(sketch001, center = [0, 0], radius = 1)
extrude001 = extrude(profile001, length = 1)
`
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.settled(cmdBar)
// One dumb hardcoded screen pixel value
const midPoint = { x: 500, y: 250 }
const moreToTheRightPoint = { x: 800, y: 250 }
const bgColor: [number, number, number] = [50, 50, 50]
const partColor: [number, number, number] = [150, 150, 150]
const tolerance = 50
await test.step('Confirm extrude exists with default appearance', async () => {
await toolbar.closePane('code')
await scene.expectPixelColor(partColor, midPoint, tolerance)
await scene.expectPixelColor(bgColor, moreToTheRightPoint, tolerance)
})
await test.step('Set translate through command bar flow', async () => {
await toolbar.openPane('feature-tree')
const op = await toolbar.getFeatureTreeOperation('Extrude', 0)
await op.click({ button: 'right' })
await page.getByTestId('context-menu-set-translate').click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'x',
currentArgValue: '0',
headerArguments: {
X: '',
Y: '',
Z: '',
},
highlightedHeaderArg: 'x',
commandName: 'Translate',
})
await page.keyboard.insertText('3')
if (variables) {
await cmdBar.createNewVariable()
}
await cmdBar.progressCmdBar()
await page.keyboard.insertText('0.1')
if (variables) {
await cmdBar.createNewVariable()
}
await cmdBar.progressCmdBar()
await page.keyboard.insertText('0.2')
if (variables) {
await cmdBar.createNewVariable()
}
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: {
X: '3',
Y: '0.1',
Z: '0.2',
},
commandName: 'Translate',
})
await cmdBar.progressCmdBar()
await toolbar.closePane('feature-tree')
})
await test.step('Confirm code and scene have changed', async () => {
await toolbar.openPane('code')
if (variables) {
await editor.expectEditor.toContain(
`
z001 = 0.2
y001 = 0.1
x001 = 3
sketch001 = startSketchOn(XZ)
profile001 = circle(sketch001, center = [0, 0], radius = 1)
extrude001 = extrude(profile001, length = 1)
|> translate(x = x001, y = y001, z = z001)
`,
{ shouldNormalise: true }
)
} else {
await editor.expectEditor.toContain(
`
sketch001 = startSketchOn(XZ)
profile001 = circle(sketch001, center = [0, 0], radius = 1)
extrude001 = extrude(profile001, length = 1)
|> translate(x = 3, y = 0.1, z = 0.2)
`,
{ shouldNormalise: true }
)
}
await scene.expectPixelColor(bgColor, midPoint, tolerance)
await scene.expectPixelColor(partColor, moreToTheRightPoint, tolerance)
})
await test.step('Edit translate', async () => {
await toolbar.openPane('feature-tree')
const op = await toolbar.getFeatureTreeOperation('Extrude', 0)
await op.click({ button: 'right' })
await page.getByTestId('context-menu-set-translate').click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'z',
currentArgValue: variables ? 'z001' : '0.2',
headerArguments: {
X: '3',
Y: '0.1',
Z: '0.2',
},
highlightedHeaderArg: 'z',
commandName: 'Translate',
})
await page.keyboard.insertText('0.3')
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: {
X: '3',
Y: '0.1',
Z: '0.3',
},
commandName: 'Translate',
})
await cmdBar.progressCmdBar()
await toolbar.closePane('feature-tree')
await toolbar.openPane('code')
await editor.expectEditor.toContain(`z = 0.3`)
// Expect almost no change in scene
await scene.expectPixelColor(bgColor, midPoint, tolerance)
await scene.expectPixelColor(partColor, moreToTheRightPoint, tolerance)
})
})
})
const rotateExtrudeCases: { variables: boolean }[] = [
{
variables: false,
},
{
variables: true,
},
]
rotateExtrudeCases.map(({ variables }) => {
test(`Set rotate on extrude through right-click menu (variables: ${variables})`, async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
const initialCode = `sketch001 = startSketchOn(XZ)
profile001 = circle(sketch001, center = [0, 0], radius = 1)
extrude001 = extrude(profile001, length = 1)
`
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.settled(cmdBar)
await test.step('Set rotate through command bar flow', async () => {
await toolbar.openPane('feature-tree')
const op = await toolbar.getFeatureTreeOperation('Extrude', 0)
await op.click({ button: 'right' })
await page.getByTestId('context-menu-set-rotate').click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'roll',
currentArgValue: '0',
headerArguments: {
Roll: '',
Pitch: '',
Yaw: '',
},
highlightedHeaderArg: 'roll',
commandName: 'Rotate',
})
await page.keyboard.insertText('1.1')
if (variables) {
await cmdBar.createNewVariable()
}
await cmdBar.progressCmdBar()
await page.keyboard.insertText('1.2')
if (variables) {
await cmdBar.createNewVariable()
}
await cmdBar.progressCmdBar()
await page.keyboard.insertText('1.3')
if (variables) {
await cmdBar.createNewVariable()
}
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Roll: '1.1',
Pitch: '1.2',
Yaw: '1.3',
},
commandName: 'Rotate',
})
await cmdBar.progressCmdBar()
await toolbar.closePane('feature-tree')
})
await test.step('Confirm code and scene have changed', async () => {
await toolbar.openPane('code')
if (variables) {
await editor.expectEditor.toContain(
`
yaw001 = 1.3
pitch001 = 1.2
roll001 = 1.1
sketch001 = startSketchOn(XZ)
profile001 = circle(sketch001, center = [0, 0], radius = 1)
extrude001 = extrude(profile001, length = 1)
|> rotate(roll = roll001, pitch = pitch001, yaw = yaw001)
`,
{ shouldNormalise: true }
)
} else {
await editor.expectEditor.toContain(
`
sketch001 = startSketchOn(XZ)
profile001 = circle(sketch001, center = [0, 0], radius = 1)
extrude001 = extrude(profile001, length = 1)
|> rotate(roll = 1.1, pitch = 1.2, yaw = 1.3)
`,
{ shouldNormalise: true }
)
}
})
await test.step('Edit rotate', async () => {
await toolbar.openPane('feature-tree')
const op = await toolbar.getFeatureTreeOperation('Extrude', 0)
await op.click({ button: 'right' })
await page.getByTestId('context-menu-set-rotate').click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'yaw',
currentArgValue: variables ? 'yaw001' : '1.3',
headerArguments: {
Roll: '1.1',
Pitch: '1.2',
Yaw: '1.3',
},
highlightedHeaderArg: 'yaw',
commandName: 'Rotate',
})
await page.keyboard.insertText('13')
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Roll: '1.1',
Pitch: '1.2',
Yaw: '13',
},
commandName: 'Rotate',
})
await cmdBar.progressCmdBar()
await toolbar.closePane('feature-tree')
await toolbar.openPane('code')
await editor.expectEditor.toContain(`yaw = 13`)
})
})
})
test(`Set translate and rotate on extrude through selection`, async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
const initialCode = `sketch001 = startSketchOn(XZ)
profile001 = circle(sketch001, center = [0, 0], radius = 1)
extrude001 = extrude(profile001, length = 1)
`
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.settled(cmdBar)
// One dumb hardcoded screen pixel value
const midPoint = { x: 500, y: 250 }
const moreToTheRightPoint = { x: 800, y: 250 }
const bgColor: [number, number, number] = [50, 50, 50]
const partColor: [number, number, number] = [150, 150, 150]
const tolerance = 50
const [clickMidPoint] = scene.makeMouseHelpers(midPoint.x, midPoint.y)
const [clickMoreToTheRightPoint] = scene.makeMouseHelpers(
moreToTheRightPoint.x,
moreToTheRightPoint.y
)
await test.step('Confirm extrude exists with default appearance', async () => {
await toolbar.closePane('code')
await scene.expectPixelColor(partColor, midPoint, tolerance)
await scene.expectPixelColor(bgColor, moreToTheRightPoint, tolerance)
})
await test.step('Set translate through command bar flow', async () => {
await cmdBar.openCmdBar()
await cmdBar.chooseCommand('Translate')
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'selection',
currentArgValue: '',
headerArguments: {
Selection: '',
X: '',
Y: '',
Z: '',
},
highlightedHeaderArg: 'selection',
commandName: 'Translate',
})
await clickMidPoint()
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'x',
currentArgValue: '0',
headerArguments: {
Selection: '1 path',
X: '',
Y: '',
Z: '',
},
highlightedHeaderArg: 'x',
commandName: 'Translate',
})
await page.keyboard.insertText('2')
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Selection: '1 path',
X: '2',
Y: '0',
Z: '0',
},
commandName: 'Translate',
})
await cmdBar.progressCmdBar()
})
await test.step('Confirm code and scene have changed', async () => {
await toolbar.openPane('code')
await editor.expectEditor.toContain(
`
sketch001 = startSketchOn(XZ)
profile001 = circle(sketch001, center = [0, 0], radius = 1)
extrude001 = extrude(profile001, length = 1)
|> translate(x = 2, y = 0, z = 0)
`,
{ shouldNormalise: true }
)
await scene.expectPixelColor(bgColor, midPoint, tolerance)
await scene.expectPixelColor(partColor, moreToTheRightPoint, tolerance)
})
await test.step('Set rotate through command bar flow', async () => {
// clear selection
await clickMidPoint()
await cmdBar.openCmdBar()
await cmdBar.chooseCommand('Rotate')
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'selection',
currentArgValue: '',
headerArguments: {
Selection: '',
Roll: '',
Pitch: '',
Yaw: '',
},
highlightedHeaderArg: 'selection',
commandName: 'Rotate',
})
await clickMoreToTheRightPoint()
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'roll',
currentArgValue: '0',
headerArguments: {
Selection: '1 path',
Roll: '',
Pitch: '',
Yaw: '',
},
highlightedHeaderArg: 'roll',
commandName: 'Rotate',
})
await page.keyboard.insertText('0.1')
await cmdBar.progressCmdBar()
await page.keyboard.insertText('0.2')
await cmdBar.progressCmdBar()
await page.keyboard.insertText('0.3')
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Selection: '1 path',
Roll: '0.1',
Pitch: '0.2',
Yaw: '0.3',
},
commandName: 'Rotate',
})
await cmdBar.progressCmdBar()
})
await test.step('Confirm code has changed', async () => {
await toolbar.openPane('code')
await editor.expectEditor.toContain(
`
sketch001 = startSketchOn(XZ)
profile001 = circle(sketch001, center = [0, 0], radius = 1)
extrude001 = extrude(profile001, length = 1)
|> translate(x = 2, y = 0, z = 0)
|> rotate(roll = 0.1, pitch = 0.2, yaw = 0.3)
`,
{ shouldNormalise: true }
)
// No change here since the angles are super small
await scene.expectPixelColor(bgColor, midPoint, tolerance)
await scene.expectPixelColor(partColor, moreToTheRightPoint, tolerance)
})
})
})

View File

@ -1,4 +1,5 @@
import { expect, test } from '@e2e/playwright/zoo-test'
import { orRunWhenFullSuiteEnabled } from '@e2e/playwright/test-utils'
/* eslint-disable jest/no-conditional-expect */
@ -57,6 +58,7 @@ test.describe('edit with AI example snapshots', () => {
`change colour`,
{ tag: '@snapshot' },
async ({ context, homePage, cmdBar, editor, page, scene }) => {
test.fixme(orRunWhenFullSuiteEnabled())
await context.addInitScript((file) => {
localStorage.setItem('persistCode', file)
}, file)

View File

@ -3058,7 +3058,7 @@ test.describe('manual edits during sketch mode', () => {
}) => {
const initialCode = `myVar1 = 5
myVar2 = 6
sketch001 = startSketchOn(XZ)
profile001 = startProfileAt([106.68, 89.77], sketch001)
|> line(end = [132.34, 157.8])
@ -3069,10 +3069,8 @@ test.describe('manual edits during sketch mode', () => {
sketch002 = startSketchOn(extrude001, face = seg01)
profile002 = startProfileAt([83.39, 329.15], sketch002)
|> angledLine(angle = 0, length = 119.61, tag = $rectangleSegmentA001)
|> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 156.54, angle = -28)
|> angledLine(length = 156.54, angle = -28)
|> angledLine(
angle = segAng(rectangleSegmentA001),
length = -segLen(rectangleSegmentA001),
angle = -151,
length = 116.27,
)
@ -3089,38 +3087,43 @@ test.describe('manual edits during sketch mode', () => {
await homePage.goToModelingScene()
await scene.connectionEstablished()
await scene.settled(cmdBar)
const expectSketchOriginToBeDrawn = async () => {
await scene.expectPixelColor(TEST_COLORS.WHITE, { x: 672, y: 193 }, 15)
}
await test.step('Open feature tree and edit second sketch', async () => {
await toolbar.openFeatureTreePane()
const sketchButton = await toolbar.getFeatureTreeOperation('Sketch', 1)
await sketchButton.dblclick()
await page.waitForTimeout(700) // Wait for engine animation
await expectSketchOriginToBeDrawn()
})
await test.step('Add new variable and wait for re-execution', async () => {
await page.waitForTimeout(500) // wait for deferred execution
await editor.replaceCode('myVar2 = 6', 'myVar2 = 6\nmyVar3 = 7')
await page.waitForTimeout(2000) // wait for deferred execution
await expectSketchOriginToBeDrawn()
})
const handle1Location = { x: 843, y: 235 }
await test.step('Edit sketch by dragging handle', async () => {
await page.waitForTimeout(500)
await editor.expectEditor.toContain('length = 156.54, angle = -28')
await page.mouse.move(handle1Location.x, handle1Location.y)
await page.mouse.down()
await page.mouse.move(handle1Location.x + 50, handle1Location.y + 50, {
steps: 5,
})
await page.mouse.up()
await editor.expectEditor.toContain('length = 231.59, angle = -34')
// await page.waitForTimeout(1000) // Wait for update
await expect
.poll(
async () => {
await editor.expectEditor.toContain('length = 156.54, angle = -28')
await page.mouse.move(handle1Location.x, handle1Location.y)
await page.mouse.down()
await page.mouse.move(
handle1Location.x + 50,
handle1Location.y + 50,
{
steps: 5,
}
)
await page.mouse.up()
await editor.expectEditor.toContain('length = 231.59, angle = -34')
return true
},
{ timeout: 10_000 }
)
.toBeTruthy()
})
await test.step('Delete variables and wait for re-execution', async () => {
@ -3129,20 +3132,27 @@ test.describe('manual edits during sketch mode', () => {
await page.waitForTimeout(50)
await editor.replaceCode('myVar2 = 6', '')
await page.waitForTimeout(2000) // Wait for deferred execution
await expectSketchOriginToBeDrawn()
})
const handle2Location = { x: 872, y: 273 }
await test.step('Edit sketch again', async () => {
await editor.expectEditor.toContain('length = 231.59, angle = -34')
await page.waitForTimeout(500)
await page.mouse.move(handle2Location.x, handle2Location.y)
await page.mouse.down()
await page.mouse.move(handle2Location.x, handle2Location.y - 50, {
steps: 5,
})
await page.mouse.up()
await editor.expectEditor.toContain('length = 167.36, angle = -14')
await expect
.poll(
async () => {
await page.mouse.move(handle2Location.x, handle2Location.y)
await page.mouse.down()
await page.mouse.move(handle2Location.x, handle2Location.y - 50, {
steps: 5,
})
await page.mouse.up()
await editor.expectEditor.toContain('length = 167.36, angle = -14')
return true
},
{ timeout: 10_000 }
)
.toBeTruthy()
})
await test.step('add whole other sketch before current sketch', async () => {
@ -3154,19 +3164,27 @@ test.describe('manual edits during sketch mode', () => {
profile004 = circle(sketch003, center = [143.91, 136.89], radius = 71.63)`
)
await page.waitForTimeout(2000) // Wait for deferred execution
await expectSketchOriginToBeDrawn()
})
const handle3Location = { x: 844, y: 212 }
await test.step('edit sketch again', async () => {
await editor.expectEditor.toContain('length = 167.36, angle = -14')
await page.mouse.move(handle3Location.x, handle3Location.y)
await page.mouse.down()
await page.mouse.move(handle3Location.x, handle3Location.y + 110, {
steps: 5,
})
await page.mouse.up()
await editor.expectEditor.toContain('length = 219.2, angle = -56')
await page.waitForTimeout(500) // Wait for deferred execution
await expect
.poll(
async () => {
await editor.expectEditor.toContain('length = 167.36, angle = -14')
await page.mouse.move(handle3Location.x, handle3Location.y)
await page.mouse.down()
await page.mouse.move(handle3Location.x, handle3Location.y + 110, {
steps: 5,
})
await page.mouse.up()
await editor.expectEditor.toContain('length = 219.2, angle = -56')
return true
},
{ timeout: 10_000 }
)
.toBeTruthy()
})
// exit sketch and assert whole code
@ -3174,32 +3192,27 @@ test.describe('manual edits during sketch mode', () => {
await toolbar.exitSketch()
await editor.expectEditor.toContain(
`myVar1 = 5
sketch003 = startSketchOn(XY)
profile004 = circle(sketch003, center = [143.91, 136.89], radius = 71.63)
sketch001 = startSketchOn(XZ)
profile001 = startProfileAt([106.68, 89.77], sketch001)
|> line(end = [132.34, 157.8])
|> line(end = [67.65, -460.55], tag = $seg01)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(profile001, length = 500)
sketch002 = startSketchOn(extrude001, face = seg01)
profile002 = startProfileAt([83.39, 329.15], sketch002)
|> angledLine(angle = 0, length = 119.61, tag = $rectangleSegmentA001)
|> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 219.2, angle = -56)
|> angledLine(
angle = segAng(rectangleSegmentA001),
length = -segLen(rectangleSegmentA001),
angle = -151,
length = 116.27,
)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
profile003 = startProfileAt([-201.08, 254.17], sketch002)
|> line(end = [103.55, 33.32])
|> line(end = [48.8, -153.54])
`,
sketch003 = startSketchOn(XY)
profile004 = circle(sketch003, center = [143.91, 136.89], radius = 71.63)
sketch001 = startSketchOn(XZ)
profile001 = startProfileAt([106.68, 89.77], sketch001)
|> line(end = [132.34, 157.8])
|> line(end = [67.65, -460.55], tag = $seg01)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(profile001, length = 500)
sketch002 = startSketchOn(extrude001, face = seg01)
profile002 = startProfileAt([83.39, 329.15], sketch002)
|> angledLine(angle = 0, length = 119.61, tag = $rectangleSegmentA001)
|> angledLine(length = 219.2, angle = -56)
|> angledLine(angle = -151, length = 116.27)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
profile003 = startProfileAt([-201.08, 254.17], sketch002)
|> line(end = [103.55, 33.32])
|> line(end = [48.8, -153.54])
`,
{ shouldNormalise: true }
)
await editor.expectState({
@ -3220,7 +3233,7 @@ test.describe('manual edits during sketch mode', () => {
}) => {
const initialCode = `myVar1 = 5
myVar2 = 6
sketch001 = startSketchOn(XZ)
profile001 = startProfileAt([106.68, 89.77], sketch001)
|> line(end = [132.34, 157.8])
@ -3231,10 +3244,8 @@ test.describe('manual edits during sketch mode', () => {
sketch002 = startSketchOn(extrude001, face = seg01)
profile002 = startProfileAt([83.39, 329.15], sketch002)
|> angledLine(angle = 0, length = 119.61, tag = $rectangleSegmentA001)
|> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 156.54, angle = -28)
|> angledLine(length = 156.54, angle = -28)
|> angledLine(
angle = segAng(rectangleSegmentA001),
length = -segLen(rectangleSegmentA001),
angle = -151,
length = 116.27,
)
@ -3350,7 +3361,21 @@ test.describe('manual edits during sketch mode', () => {
// this checks sketch segments have been drawn
await verifyArrowHeadColor(arrowHeadWhite)
})
await page.waitForTimeout(100)
await test.step('make a change to the code and expect pixel color to change', async () => {
// defends against a regression where sketch would duplicate in the scene
// https://github.com/KittyCAD/modeling-app/issues/6345
await editor.replaceCode(
'startProfileAt([75.8, 317.2',
'startProfileAt([75.8, 217.2'
)
// expect not white anymore
await scene.expectPixelColorNotToBe(
TEST_COLORS.WHITE,
arrowHeadLocation,
15
)
})
}
)
})

View File

@ -377,8 +377,7 @@ test.describe(
'extrude on default planes should be stable',
{ tag: '@snapshot' },
() => {
// FIXME: Skip on macos its being weird.
test.skip(process.platform === 'darwin', 'Skip on macos')
test.fixme(orRunWhenFullSuiteEnabled())
test('XY', async ({ page, context, cmdBar, scene }) => {
await extrudeDefaultPlane(context, page, cmdBar, scene, 'XY')
@ -410,6 +409,7 @@ test(
'Draft segments should look right',
{ tag: '@snapshot' },
async ({ page, scene, toolbar }) => {
test.fixme(orRunWhenFullSuiteEnabled())
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio
@ -534,8 +534,7 @@ test(
'Draft rectangles should look right',
{ tag: '@snapshot' },
async ({ page, context, cmdBar, scene }) => {
// FIXME: Skip on macos its being weird.
test.skip(process.platform === 'darwin', 'Skip on macos')
test.fixme(orRunWhenFullSuiteEnabled())
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
@ -629,8 +628,7 @@ test.describe(
'Client side scene scale should match engine scale',
{ tag: '@snapshot' },
() => {
// FIXME: Skip on macos its being weird.
test.skip(process.platform === 'darwin', 'Skip on macos')
test.fixme(orRunWhenFullSuiteEnabled())
test('Inch scale', async ({ page, cmdBar, scene }) => {
const u = await getUtils(page)
@ -807,8 +805,7 @@ test(
'Sketch on face with none z-up',
{ tag: '@snapshot' },
async ({ page, context, cmdBar, scene }) => {
// FIXME: Skip on macos its being weird.
test.skip(process.platform === 'darwin', 'Skip on macos')
test.fixme(orRunWhenFullSuiteEnabled())
const u = await getUtils(page)
await context.addInitScript(async (KCL_DEFAULT_LENGTH) => {
@ -868,8 +865,7 @@ test(
'Zoom to fit on load - solid 2d',
{ tag: '@snapshot' },
async ({ page, context, cmdBar, scene }) => {
// FIXME: Skip on macos its being weird.
test.skip(process.platform === 'darwin', 'Skip on macos')
test.fixme(orRunWhenFullSuiteEnabled())
const u = await getUtils(page)
await context.addInitScript(async () => {
@ -907,8 +903,7 @@ test(
'Zoom to fit on load - solid 3d',
{ tag: '@snapshot' },
async ({ page, context, cmdBar, scene }) => {
// FIXME: Skip on macos its being weird.
test.skip(process.platform === 'darwin', 'Skip on macos')
test.fixme(orRunWhenFullSuiteEnabled())
const u = await getUtils(page)
await context.addInitScript(async () => {
@ -949,6 +944,7 @@ test.describe('Grid visibility', { tag: '@snapshot' }, () => {
cmdBar,
scene,
}) => {
test.fixme(orRunWhenFullSuiteEnabled())
const u = await getUtils(page)
const stream = page.getByTestId('stream')
@ -1008,6 +1004,7 @@ test.describe('Grid visibility', { tag: '@snapshot' }, () => {
})
test('Grid turned off', async ({ page, cmdBar, scene }) => {
test.fixme(orRunWhenFullSuiteEnabled())
const u = await getUtils(page)
const stream = page.getByTestId('stream')
@ -1029,6 +1026,7 @@ test.describe('Grid visibility', { tag: '@snapshot' }, () => {
})
test('Grid turned on', async ({ page, context, cmdBar, scene }) => {
test.fixme(orRunWhenFullSuiteEnabled())
await context.addInitScript(
async ({ settingsKey, settings }) => {
localStorage.setItem(settingsKey, settings)
@ -1138,6 +1136,7 @@ test('theme persists', async ({ page, context }) => {
test.describe('code color goober', { tag: '@snapshot' }, () => {
test('code color goober', async ({ page, context, scene, cmdBar }) => {
test.fixme(orRunWhenFullSuiteEnabled())
const u = await getUtils(page)
await context.addInitScript(async () => {
localStorage.setItem(

View File

@ -44,7 +44,9 @@
packages =
(with pkgs; [
rustToolchain
cargo-criterion
cargo-nextest
cargo-sort
just
postgresql.lib
openssl

View File

@ -25,10 +25,14 @@ When you submit a PR to add or modify KCL samples, images and STEP files will be
---
#### [80-20-rail](80-20-rail/main.kcl) ([screenshot](screenshots/80-20-rail.png))
[![80-20-rail](screenshots/80-20-rail.png)](80-20-rail/main.kcl)
#### [axial-fan](axial-fan/main.kcl) ([screenshot](screenshots/axial-fan.png))
[![axial-fan](screenshots/axial-fan.png)](axial-fan/main.kcl)
#### [ball-bearing](ball-bearing/main.kcl) ([screenshot](screenshots/ball-bearing.png))
[![ball-bearing](screenshots/ball-bearing.png)](ball-bearing/main.kcl)
#### [bench](bench/main.kcl) ([screenshot](screenshots/bench.png))
[![bench](screenshots/bench.png)](bench/main.kcl)
#### [bottle](bottle/main.kcl) ([screenshot](screenshots/bottle.png))
[![bottle](screenshots/bottle.png)](bottle/main.kcl)
#### [bracket](bracket/main.kcl) ([screenshot](screenshots/bracket.png))
[![bracket](screenshots/bracket.png)](bracket/main.kcl)
#### [car-wheel-assembly](car-wheel-assembly/main.kcl) ([screenshot](screenshots/car-wheel-assembly.png))

View File

@ -0,0 +1,153 @@
// Fan Housing
// The plastic housing that contains the fan and the motor
// Set units
@settings(defaultLengthUnit = mm)
// Import parameters
import * from "parameters.kcl"
// Model the housing which holds the motor, the fan, and the mounting provisions
// Bottom mounting face
bottomFaceSketch = startSketchOn(XY)
|> startProfileAt([-fanSize / 2, -fanSize / 2], %)
|> angledLine(angle = 0, length = fanSize, tag = $rectangleSegmentA001)
|> angledLine(angle = segAng(rectangleSegmentA001) + 90, length = fanSize, tag = $rectangleSegmentB001)
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $rectangleSegmentC001)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $rectangleSegmentD001)
|> close()
|> hole(circle(center = [0, 0], radius = 4), %)
|> hole(circle(
center = [
mountingHoleSpacing / 2,
mountingHoleSpacing / 2
],
radius = mountingHoleSize / 2,
), %)
|> hole(circle(
center = [
-mountingHoleSpacing / 2,
mountingHoleSpacing / 2
],
radius = mountingHoleSize / 2,
), %)
|> hole(circle(
center = [
mountingHoleSpacing / 2,
-mountingHoleSpacing / 2
],
radius = mountingHoleSize / 2,
), %)
|> hole(circle(
center = [
-mountingHoleSpacing / 2,
-mountingHoleSpacing / 2
],
radius = mountingHoleSize / 2,
), %)
|> extrude(length = 4)
// Add large openings to the bottom face to allow airflow through the fan
airflowPattern = startSketchOn(bottomFaceSketch, face = END)
|> startProfileAt([fanSize * 7 / 25, -fanSize * 9 / 25], %)
|> angledLine(angle = 140, length = fanSize * 12 / 25, tag = $seg01)
|> tangentialArc(radius = fanSize * 1 / 50, angle = 90)
|> angledLine(angle = -130, length = fanSize * 8 / 25)
|> tangentialArc(radius = fanSize * 1 / 50, angle = 90)
|> angledLine(angle = segAng(seg01) + 180, length = fanSize * 2 / 25)
|> tangentialArc(radius = fanSize * 8 / 25, angle = 40)
|> xLine(length = fanSize * 3 / 25)
|> tangentialArc(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
|> patternCircular2d(
instances = 4,
center = [0, 0],
arcDegrees = 360,
rotateDuplicates = true,
)
|> extrude(length = -4)
// Create the middle segment of the fan housing body
housingMiddleLength = fanSize / 3
housingMiddleRadius = fanSize / 3 - 1
bodyMiddle = startSketchOn(bottomFaceSketch, face = END)
|> startProfileAt([
housingMiddleLength / 2,
-housingMiddleLength / 2 - housingMiddleRadius
], %)
|> tangentialArc(radius = housingMiddleRadius, angle = 90)
|> yLine(length = housingMiddleLength)
|> tangentialArc(radius = housingMiddleRadius, angle = 90)
|> xLine(length = -housingMiddleLength)
|> tangentialArc(radius = housingMiddleRadius, angle = 90)
|> yLine(length = -housingMiddleLength)
|> tangentialArc(radius = housingMiddleRadius, angle = 90)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> extrude(length = fanHeight - 4 - 4)
// Cut a hole in the body to accommodate the fan
bodyFanHole = startSketchOn(bodyMiddle, face = END)
|> circle(center = [0, 0], radius = fanSize * 23 / 50)
|> extrude(length = -(fanHeight - 4 - 4))
// Top mounting face. Cut a hole in the face to accommodate the fan
topFaceSketch = startSketchOn(bodyMiddle, face = END)
topHoles = startProfileAt([-fanSize / 2, -fanSize / 2], topFaceSketch)
|> angledLine(angle = 0, length = fanSize, tag = $rectangleSegmentA002)
|> angledLine(angle = segAng(rectangleSegmentA002) + 90, length = fanSize, tag = $rectangleSegmentB002)
|> angledLine(angle = segAng(rectangleSegmentA002), length = -segLen(rectangleSegmentA002), tag = $rectangleSegmentC002)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $rectangleSegmentD002)
|> close()
|> hole(circle(center = [0, 0], radius = fanSize * 23 / 50), %)
|> hole(circle(
center = [
mountingHoleSpacing / 2,
mountingHoleSpacing / 2
],
radius = mountingHoleSize / 2,
), %)
|> hole(circle(
center = [
-mountingHoleSpacing / 2,
mountingHoleSpacing / 2
],
radius = mountingHoleSize / 2,
), %)
|> hole(circle(
center = [
mountingHoleSpacing / 2,
-mountingHoleSpacing / 2
],
radius = mountingHoleSize / 2,
), %)
|> hole(circle(
center = [
-mountingHoleSpacing / 2,
-mountingHoleSpacing / 2
],
radius = mountingHoleSize / 2,
), %)
|> extrude(length = 4)
// Create a housing for the electric motor to sit
motorHousing = startSketchOn(bottomFaceSketch, face = END)
|> circle(center = [0, 0], radius = 11.2)
|> extrude(length = 16)
startSketchOn(motorHousing, face = END)
|> circle(center = [0, 0], radius = 10)
|> extrude(length = -16)
|> appearance(color = "#a55e2c")
|> fillet(
radius = abs(fanSize - mountingHoleSpacing) / 2,
tags = [
getNextAdjacentEdge(rectangleSegmentA001),
getNextAdjacentEdge(rectangleSegmentB001),
getNextAdjacentEdge(rectangleSegmentC001),
getNextAdjacentEdge(rectangleSegmentD001),
getNextAdjacentEdge(rectangleSegmentA002),
getNextAdjacentEdge(rectangleSegmentB002),
getNextAdjacentEdge(rectangleSegmentC002),
getNextAdjacentEdge(rectangleSegmentD002)
],
)

View File

@ -0,0 +1,92 @@
// Fan
// Spinning axial fan that moves airflow
// Set units
@settings(defaultLengthUnit = mm)
// Import parameters
import * from "parameters.kcl"
// Model the center of the fan
fanCenter = startSketchOn(XZ)
|> startProfileAt([-0.0001, fanHeight], %)
|> xLine(endAbsolute = -15 + 1.5)
|> tangentialArc(radius = 1.5, angle = 90)
|> yLine(endAbsolute = 4.5)
|> xLine(endAbsolute = -13)
|> yLine(endAbsolute = profileStartY(%) - 5)
|> tangentialArc(radius = 1, angle = -90)
|> xLine(endAbsolute = -1)
|> yLine(length = 2)
|> xLine(length = -0.15)
|> line(endAbsolute = [
profileStartX(%) - 1,
profileStartY(%) - 1.4
])
|> xLine(endAbsolute = profileStartX(%))
|> yLine(endAbsolute = profileStartY(%))
|> close()
|> revolve(axis = {
direction = [0.0, 1.0],
origin = [0.0, 0.0]
})
|> appearance(color = "#f3e2d8")
// Create a function for a lofted fan blade cross section that rotates about the center hub of the fan
fn fanBlade(offsetHeight, startAngle) {
fanBlade = startSketchOn(offsetPlane(XY, offset = offsetHeight))
|> startProfileAt([
15 * cos(toRadians(startAngle)),
15 * sin(toRadians(startAngle))
], %)
|> arc({
angleStart = startAngle,
angleEnd = startAngle + 14,
radius = 15
}, %)
|> arcTo({
end = [
fanSize * 22 / 50 * cos(toRadians(startAngle - 20)),
fanSize * 22 / 50 * sin(toRadians(startAngle - 20))
],
interior = [
fanSize * 11 / 50 * cos(toRadians(startAngle + 3)),
fanSize * 11 / 50 * sin(toRadians(startAngle + 3))
]
}, %)
|> arcTo({
end = [
fanSize * 22 / 50 * cos(toRadians(startAngle - 24)),
fanSize * 22 / 50 * sin(toRadians(startAngle - 24))
],
interior = [
fanSize * 22 / 50 * cos(toRadians(startAngle - 22)),
fanSize * 22 / 50 * sin(toRadians(startAngle - 22))
]
}, %)
|> arcTo({
end = [profileStartX(%), profileStartY(%)],
interior = [
fanSize * 11 / 50 * cos(toRadians(startAngle - 5)),
fanSize * 11 / 50 * sin(toRadians(startAngle - 5))
]
}, %)
|> close()
return fanBlade
}
// Loft the fan blade cross sections into a single blade, then pattern them about the fan center
loft([
fanBlade(4.5, 50),
fanBlade((fanHeight - 2 - 4) / 2, 30),
fanBlade(fanHeight - 2, 0)
])
|> appearance(color = "#f3e2d8")
|> patternCircular3d(
%,
instances = 9,
axis = [0, 0, 1],
center = [0, 0, 0],
arcDegrees = 360,
rotateDuplicates = true,
)

View File

@ -0,0 +1,15 @@
// PC Fan
// A small axial fan, used to push or draw airflow over components to remove excess heat
// Set units
@settings(defaultLengthUnit = mm)
// Import all parts into assembly file
import "fan-housing.kcl" as fanHousing
import "motor.kcl" as motor
import "fan.kcl" as fan
// Produce the model for each imported part
fanHousing
motor
fan

View File

@ -0,0 +1,22 @@
// Motor
// A small electric motor to power the fan
// Set Units
@settings(defaultLengthUnit = mm)
// Import Parameters
import * from "parameters.kcl"
// Model the motor body and stem
topFacePlane = offsetPlane(XY, offset = 4)
motorBody = startSketchOn(topFacePlane)
|> circle(center = [0, 0], radius = 10, tag = $seg04)
|> extrude(length = 17)
|> appearance(color = "#021b55")
|> fillet(radius = 2, tags = [getOppositeEdge(seg04), seg04])
startSketchOn(offsetPlane(XY, offset = 21))
|> circle(center = [0, 0], radius = 1)
|> extrude(length = 3.8)
|> appearance(color = "#dbc89e")

View File

@ -0,0 +1,10 @@
// Global parameters for the axial fan
// Set units
@settings(defaultLengthUnit = mm)
// Define Parameters
export fanSize = 120
export fanHeight = 25
export mountingHoleSpacing = 105
export mountingHoleSize = 4.5

View File

@ -0,0 +1,35 @@
// Bottle
// A simple bottle with a hollow, watertight interior
// Set Units
@settings(defaultLengthUnit = mm)
// Input dimensions to define the bottle
bottleWidth = 80
bottleLength = 125
bottleHeight = 220
neckDepth = 18
neckDiameter = 45
wallThickness = 4
// Create a rounded body for the bottle
bottleBody = startSketchOn(XY)
|> startProfileAt([-bottleLength / 2, 0], %)
|> yLine(length = bottleWidth / 3)
|> arcTo({
end = [bottleLength / 2, bottleWidth / 3],
interior = [0, bottleWidth / 2]
}, %)
|> yLine(endAbsolute = 0)
|> mirror2d(axis = X)
|> close()
|> extrude(length = bottleHeight - neckDepth)
// Create a neck centered at the top of the bottle
bottleNeck = startSketchOn(bottleBody, face = END)
|> circle(center = [0, 0], radius = neckDiameter / 2)
|> extrude(length = neckDepth)
// Define a shell operation so that the entire body and neck are hollow, with only the top face opened
bottleShell = shell(bottleNeck, faces = [END], thickness = wallThickness)
|> appearance(%, color = "#0078c2")

View File

@ -6,6 +6,13 @@
"title": "80/20 Rail",
"description": "An 80/20 extruded aluminum linear rail. T-slot profile adjustable by profile height, rail length, and origin position"
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "axial-fan/main.kcl",
"multipleFiles": true,
"title": "PC Fan",
"description": "A small axial fan, used to push or draw airflow over components to remove excess heat"
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "ball-bearing/main.kcl",
@ -20,6 +27,13 @@
"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": "bottle/main.kcl",
"multipleFiles": false,
"title": "Bottle",
"description": "A simple bottle with a hollow, watertight interior"
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "bracket/main.kcl",

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

93
rust/Cargo.lock generated
View File

@ -535,7 +535,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -665,9 +665,9 @@ dependencies = [
[[package]]
name = "crossbeam-channel"
version = "0.5.14"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471"
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
dependencies = [
"crossbeam-utils",
]
@ -758,7 +758,7 @@ dependencies = [
"hashbrown 0.14.5",
"lock_api",
"once_cell",
"parking_lot_core",
"parking_lot_core 0.9.10",
]
[[package]]
@ -772,7 +772,7 @@ dependencies = [
"hashbrown 0.14.5",
"lock_api",
"once_cell",
"parking_lot_core",
"parking_lot_core 0.9.10",
]
[[package]]
@ -847,7 +847,7 @@ dependencies = [
"backtrace",
"lazy_static",
"mintex",
"parking_lot",
"parking_lot 0.12.3",
"rustc-hash 1.1.0",
"serde",
"serde_json",
@ -954,7 +954,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -1697,6 +1697,18 @@ dependencies = [
"similar",
]
[[package]]
name = "instant"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
dependencies = [
"cfg-if",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "ipnet"
version = "2.11.0"
@ -1711,7 +1723,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
dependencies = [
"hermit-abi",
"libc",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -1885,6 +1897,7 @@ dependencies = [
"image",
"indexmap 2.8.0",
"insta",
"instant",
"itertools 0.13.0",
"js-sys",
"kcl-derive-docs",
@ -1921,6 +1934,7 @@ dependencies = [
"validator",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-timer",
"web-sys",
"web-time",
"winnow 0.6.24",
@ -2460,6 +2474,17 @@ dependencies = [
"unicode-width 0.2.0",
]
[[package]]
name = "parking_lot"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
dependencies = [
"instant",
"lock_api",
"parking_lot_core 0.8.6",
]
[[package]]
name = "parking_lot"
version = "0.12.3"
@ -2467,7 +2492,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
dependencies = [
"lock_api",
"parking_lot_core",
"parking_lot_core 0.9.10",
]
[[package]]
name = "parking_lot_core"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc"
dependencies = [
"cfg-if",
"instant",
"libc",
"redox_syscall 0.2.16",
"smallvec",
"winapi",
]
[[package]]
@ -2478,7 +2517,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"redox_syscall 0.5.10",
"smallvec",
"windows-targets 0.52.6",
]
@ -2883,7 +2922,7 @@ dependencies = [
"once_cell",
"socket2",
"tracing",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -3018,6 +3057,15 @@ dependencies = [
"rand_core 0.3.1",
]
[[package]]
name = "redox_syscall"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "redox_syscall"
version = "0.5.10"
@ -3211,7 +3259,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -3809,7 +3857,7 @@ dependencies = [
"getrandom 0.3.1",
"once_cell",
"rustix",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -3984,7 +4032,7 @@ dependencies = [
"bytes",
"libc",
"mio",
"parking_lot",
"parking_lot 0.12.3",
"pin-project-lite",
"signal-hook-registry",
"socket2",
@ -4581,6 +4629,21 @@ dependencies = [
"web-sys",
]
[[package]]
name = "wasm-timer"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f"
dependencies = [
"futures",
"js-sys",
"parking_lot 0.11.2",
"pin-utils",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "web-sys"
version = "0.3.77"
@ -4632,7 +4695,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]

View File

@ -33,10 +33,16 @@ new-sim-test test_name render_to_png="true":
# Run a KCL deterministic simulation test case and accept output.
overwrite-sim-test-sample test_name:
EXPECTORATE=overwrite TWENTY_TWENTY=overwrite {{cita}} -p kcl-lib --no-quiet -- simulation_tests::kcl_samples::parse_{{test_name}}
EXPECTORATE=overwrite TWENTY_TWENTY=overwrite {{cita}} -p kcl-lib --no-quiet -- simulation_tests::kcl_samples::unparse_{{test_name}}
EXPECTORATE=overwrite TWENTY_TWENTY=overwrite {{cita}} -p kcl-lib --no-quiet -- simulation_tests::kcl_samples::kcl_test_execute_{{test_name}}
EXPECTORATE=overwrite TWENTY_TWENTY=overwrite {{cita}} -p kcl-lib --no-quiet -- simulation_tests::kcl_samples::test_after_engine_generate_manifest
overwrite-sim-test test_name:
EXPECTORATE=overwrite TWENTY_TWENTY=overwrite {{cita}} -p kcl-lib --no-quiet -- simulation_tests::{{test_name}}::parse
EXPECTORATE=overwrite TWENTY_TWENTY=overwrite {{cita}} -p kcl-lib --no-quiet -- simulation_tests::{{test_name}}::unparse
EXPECTORATE=overwrite TWENTY_TWENTY=overwrite {{cita}} -p kcl-lib --no-quiet -- simulation_tests::{{test_name}}::kcl_test_execute
[ {{test_name}} != "kcl_samples" ] || EXPECTORATE=overwrite TWENTY_TWENTY=overwrite {{cita}} -p kcl-lib --no-quiet -- simulation_tests::{{test_name}}::test_after_engine_generate_manifest
# Regenerate all the simulation test output.
redo-sim-tests:

View File

@ -89,14 +89,17 @@ winnow = "=0.6.24"
zip = { workspace = true }
[target.'cfg(target_arch = "wasm32")'.dependencies]
instant = { version = "0.1.13", features = ["wasm-bindgen", "inaccurate"] }
js-sys = { version = "0.3.72" }
tokio = { workspace = true, features = ["sync", "time"] }
tower-lsp = { workspace = true, features = ["runtime-agnostic"] }
wasm-bindgen = "0.2.99"
wasm-bindgen-futures = "0.4.49"
wasm-timer = "0.2.5"
web-sys = { version = "0.3.76", features = ["console"] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
instant = "0.1.13"
tokio = { workspace = true, features = ["full"] }
tokio-tungstenite = { version = "0.24.0", features = [
"rustls-tls-native-roots",
@ -105,6 +108,7 @@ tower-lsp = { workspace = true, features = ["proposed", "default"] }
[features]
default = ["cli", "engine"]
benchmark-execution = []
cli = ["dep:clap", "kittycad/clap"]
dhat-heap = ["dep:dhat"]
# For the lsp server, when run with stdout for rpc we want to disable println.

View File

@ -45,6 +45,7 @@ fn run_benchmarks(c: &mut Criterion) {
let benchmark_dirs = discover_benchmark_dirs(&base_dir);
#[cfg(feature = "benchmark-execution")]
let rt = tokio::runtime::Runtime::new().unwrap();
for dir in benchmark_dirs {
@ -67,12 +68,14 @@ fn run_benchmarks(c: &mut Criterion) {
.sample_size(10)
.measurement_time(std::time::Duration::from_secs(1)); // Short measurement time to keep it from running in parallel
#[cfg(feature = "benchmark-execution")]
let program = kcl_lib::Program::parse_no_errs(&input_content).unwrap();
group.bench_function(format!("parse_{}", dir_name), |b| {
b.iter(|| kcl_lib::Program::parse_no_errs(black_box(&input_content)).unwrap())
});
#[cfg(feature = "benchmark-execution")]
group.bench_function(format!("execute_{}", dir_name), |b| {
b.iter(|| {
if let Err(err) = rt.block_on(async {

View File

@ -2109,7 +2109,7 @@ async fn kcl_test_better_type_names() {
},
None => todo!(),
};
assert_eq!(err, "This function expected the input argument to be one or more Solids but it's actually of type Sketch. You can convert a sketch (2D) into a Solid (3D) by calling a function like `extrude` or `revolve`");
assert_eq!(err, "This function expected the input argument to be one or more Solids or imported geometry but it's actually of type Sketch. You can convert a sketch (2D) into a Solid (3D) by calling a function like `extrude` or `revolve`");
}
#[tokio::test(flavor = "multi_thread")]

View File

@ -133,6 +133,7 @@ impl StdLibFnArg {
|| self.type_ == "[Solid]"
|| self.type_ == "SketchSurface"
|| self.type_ == "SketchOrSurface"
|| self.type_ == "SolidOrImportedGeometry"
|| self.type_ == "SolidOrSketchOrImportedGeometry")
&& (self.required || self.include_in_snippet)
{

View File

@ -45,6 +45,7 @@ pub struct EngineConnection {
batch: Arc<RwLock<Vec<(WebSocketRequest, SourceRange)>>>,
batch_end: Arc<RwLock<IndexMap<uuid::Uuid, (WebSocketRequest, SourceRange)>>>,
artifact_commands: Arc<RwLock<Vec<ArtifactCommand>>>,
ids_of_async_commands: Arc<RwLock<IndexMap<Uuid, SourceRange>>>,
/// The default planes for the scene.
default_planes: Arc<RwLock<Option<DefaultPlanes>>>,
@ -115,6 +116,17 @@ impl Drop for TcpReadHandle {
}
}
struct ResponsesInformation {
/// The responses from the engine.
responses: Arc<RwLock<IndexMap<uuid::Uuid, WebSocketResponse>>>,
}
impl ResponsesInformation {
pub async fn add(&self, id: Uuid, response: WebSocketResponse) {
self.responses.write().await.insert(id, response);
}
}
/// Requests to send to the engine, and a way to await a response.
struct ToEngineReq {
/// The request to send
@ -227,10 +239,13 @@ impl EngineConnection {
let session_data: Arc<RwLock<Option<ModelingSessionData>>> = Arc::new(RwLock::new(None));
let session_data2 = session_data.clone();
let responses: Arc<RwLock<IndexMap<uuid::Uuid, WebSocketResponse>>> = Arc::new(RwLock::new(IndexMap::new()));
let responses_clone = responses.clone();
let ids_of_async_commands: Arc<RwLock<IndexMap<Uuid, SourceRange>>> = Arc::new(RwLock::new(IndexMap::new()));
let socket_health = Arc::new(RwLock::new(SocketHealth::Active));
let pending_errors = Arc::new(RwLock::new(Vec::new()));
let pending_errors_clone = pending_errors.clone();
let responses_information = ResponsesInformation {
responses: responses.clone(),
};
let socket_health_tcp_read = socket_health.clone();
let tcp_read_handle = tokio::spawn(async move {
@ -244,8 +259,7 @@ impl EngineConnection {
WebSocketResponse::Success(SuccessWebSocketResponse {
resp: OkWebSocketResponseData::ModelingBatch { responses },
..
}) =>
{
}) => {
#[expect(
clippy::iter_over_hash_type,
reason = "modeling command uses a HashMap and keys are random, so we don't really have a choice"
@ -254,26 +268,32 @@ impl EngineConnection {
let id: uuid::Uuid = (*resp_id).into();
match batch_response {
BatchResponse::Success { response } => {
responses_clone.write().await.insert(
id,
WebSocketResponse::Success(SuccessWebSocketResponse {
success: true,
request_id: Some(id),
resp: OkWebSocketResponseData::Modeling {
modeling_response: response.clone(),
},
}),
);
// If the id is in our ids of async commands, remove
// it.
responses_information
.add(
id,
WebSocketResponse::Success(SuccessWebSocketResponse {
success: true,
request_id: Some(id),
resp: OkWebSocketResponseData::Modeling {
modeling_response: response.clone(),
},
}),
)
.await;
}
BatchResponse::Failure { errors } => {
responses_clone.write().await.insert(
id,
WebSocketResponse::Failure(FailureWebSocketResponse {
success: false,
request_id: Some(id),
errors: errors.clone(),
}),
);
responses_information
.add(
id,
WebSocketResponse::Failure(FailureWebSocketResponse {
success: false,
request_id: Some(id),
errors: errors.clone(),
}),
)
.await;
}
}
}
@ -291,14 +311,16 @@ impl EngineConnection {
errors,
}) => {
if let Some(id) = request_id {
responses_clone.write().await.insert(
*id,
WebSocketResponse::Failure(FailureWebSocketResponse {
success: false,
request_id: *request_id,
errors: errors.clone(),
}),
);
responses_information
.add(
*id,
WebSocketResponse::Failure(FailureWebSocketResponse {
success: false,
request_id: *request_id,
errors: errors.clone(),
}),
)
.await;
} else {
// Add it to our pending errors.
let mut pe = pending_errors_clone.write().await;
@ -314,7 +336,7 @@ impl EngineConnection {
}
if let Some(id) = id {
responses_clone.write().await.insert(id, ws_resp.clone());
responses_information.add(id, ws_resp.clone()).await;
}
}
Err(e) => {
@ -341,6 +363,7 @@ impl EngineConnection {
batch: Arc::new(RwLock::new(Vec::new())),
batch_end: Arc::new(RwLock::new(IndexMap::new())),
artifact_commands: Arc::new(RwLock::new(Vec::new())),
ids_of_async_commands,
default_planes: Default::default(),
session_data,
stats: Default::default(),
@ -366,6 +389,10 @@ impl EngineManager for EngineConnection {
self.artifact_commands.clone()
}
fn ids_of_async_commands(&self) -> Arc<RwLock<IndexMap<Uuid, SourceRange>>> {
self.ids_of_async_commands.clone()
}
fn stats(&self) -> &EngineStats {
&self.stats
}
@ -386,13 +413,13 @@ impl EngineManager for EngineConnection {
Ok(())
}
async fn inner_send_modeling_cmd(
async fn inner_fire_modeling_cmd(
&self,
id: uuid::Uuid,
_id: uuid::Uuid,
source_range: SourceRange,
cmd: WebSocketRequest,
_id_to_source_range: HashMap<Uuid, SourceRange>,
) -> Result<WebSocketResponse, KclError> {
) -> Result<(), KclError> {
let (tx, rx) = oneshot::channel();
// Send the request to the engine, via the actor.
@ -424,6 +451,19 @@ impl EngineManager for EngineConnection {
})
})?;
Ok(())
}
async fn inner_send_modeling_cmd(
&self,
id: uuid::Uuid,
source_range: SourceRange,
cmd: WebSocketRequest,
id_to_source_range: HashMap<Uuid, SourceRange>,
) -> Result<WebSocketResponse, KclError> {
self.inner_fire_modeling_cmd(id, source_range, cmd, id_to_source_range)
.await?;
// Wait for the response.
let current_time = std::time::Instant::now();
while current_time.elapsed().as_secs() < 60 {

View File

@ -12,7 +12,7 @@ use kcmc::{
WebSocketResponse,
},
};
use kittycad_modeling_cmds::{self as kcmc};
use kittycad_modeling_cmds::{self as kcmc, websocket::ModelingCmdReq, ImportFiles, ModelingCmd};
use tokio::sync::RwLock;
use uuid::Uuid;
@ -29,6 +29,8 @@ pub struct EngineConnection {
batch: Arc<RwLock<Vec<(WebSocketRequest, SourceRange)>>>,
batch_end: Arc<RwLock<IndexMap<uuid::Uuid, (WebSocketRequest, SourceRange)>>>,
artifact_commands: Arc<RwLock<Vec<ArtifactCommand>>>,
ids_of_async_commands: Arc<RwLock<IndexMap<Uuid, SourceRange>>>,
responses: Arc<RwLock<IndexMap<Uuid, WebSocketResponse>>>,
/// The default planes for the scene.
default_planes: Arc<RwLock<Option<DefaultPlanes>>>,
stats: EngineStats,
@ -40,6 +42,8 @@ impl EngineConnection {
batch: Arc::new(RwLock::new(Vec::new())),
batch_end: Arc::new(RwLock::new(IndexMap::new())),
artifact_commands: Arc::new(RwLock::new(Vec::new())),
ids_of_async_commands: Arc::new(RwLock::new(IndexMap::new())),
responses: Arc::new(RwLock::new(IndexMap::new())),
default_planes: Default::default(),
stats: Default::default(),
})
@ -57,7 +61,7 @@ impl crate::engine::EngineManager for EngineConnection {
}
fn responses(&self) -> Arc<RwLock<IndexMap<Uuid, WebSocketResponse>>> {
Arc::new(RwLock::new(IndexMap::new()))
self.responses.clone()
}
fn stats(&self) -> &EngineStats {
@ -68,6 +72,10 @@ impl crate::engine::EngineManager for EngineConnection {
self.artifact_commands.clone()
}
fn ids_of_async_commands(&self) -> Arc<RwLock<IndexMap<Uuid, SourceRange>>> {
self.ids_of_async_commands.clone()
}
fn get_default_planes(&self) -> Arc<RwLock<Option<DefaultPlanes>>> {
self.default_planes.clone()
}
@ -80,6 +88,25 @@ impl crate::engine::EngineManager for EngineConnection {
Ok(())
}
async fn inner_fire_modeling_cmd(
&self,
id: uuid::Uuid,
source_range: SourceRange,
cmd: WebSocketRequest,
id_to_source_range: HashMap<Uuid, SourceRange>,
) -> Result<(), KclError> {
// Pop off the id we care about.
self.ids_of_async_commands.write().await.swap_remove(&id);
// Add the response to our responses.
let response = self
.inner_send_modeling_cmd(id, source_range, cmd, id_to_source_range)
.await?;
self.responses().write().await.insert(id, response);
Ok(())
}
async fn inner_send_modeling_cmd(
&self,
id: uuid::Uuid,
@ -109,6 +136,20 @@ impl crate::engine::EngineManager for EngineConnection {
success: true,
}))
}
WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
cmd: ModelingCmd::ImportFiles(ImportFiles { .. }),
cmd_id,
}) => Ok(WebSocketResponse::Success(SuccessWebSocketResponse {
request_id: Some(id),
resp: OkWebSocketResponseData::Modeling {
modeling_response: OkModelingCmdResponse::ImportFiles(
kittycad_modeling_cmds::output::ImportFiles {
object_id: cmd_id.into(),
},
),
},
success: true,
})),
WebSocketRequest::ModelingCmdReq(_) => Ok(WebSocketResponse::Success(SuccessWebSocketResponse {
request_id: Some(id),
resp: OkWebSocketResponseData::Modeling {

View File

@ -22,6 +22,15 @@ extern "C" {
#[derive(Debug, Clone)]
pub type EngineCommandManager;
#[wasm_bindgen(method, js_name = fireModelingCommandFromWasm, catch)]
fn fire_modeling_cmd_from_wasm(
this: &EngineCommandManager,
id: String,
rangeStr: String,
cmdStr: String,
idToRangeStr: String,
) -> Result<(), js_sys::Error>;
#[wasm_bindgen(method, js_name = sendModelingCommandFromWasm, catch)]
fn send_modeling_cmd_from_wasm(
this: &EngineCommandManager,
@ -38,33 +47,128 @@ extern "C" {
#[derive(Debug, Clone)]
pub struct EngineConnection {
manager: Arc<EngineCommandManager>,
response_context: Arc<ResponseContext>,
batch: Arc<RwLock<Vec<(WebSocketRequest, SourceRange)>>>,
batch_end: Arc<RwLock<IndexMap<uuid::Uuid, (WebSocketRequest, SourceRange)>>>,
responses: Arc<RwLock<IndexMap<Uuid, WebSocketResponse>>>,
artifact_commands: Arc<RwLock<Vec<ArtifactCommand>>>,
ids_of_async_commands: Arc<RwLock<IndexMap<Uuid, SourceRange>>>,
/// The default planes for the scene.
default_planes: Arc<RwLock<Option<DefaultPlanes>>>,
stats: EngineStats,
}
#[wasm_bindgen]
#[derive(Debug, Clone)]
pub struct ResponseContext {
responses: Arc<RwLock<IndexMap<Uuid, WebSocketResponse>>>,
}
#[wasm_bindgen]
impl ResponseContext {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self {
responses: Arc::new(RwLock::new(IndexMap::new())),
}
}
// Add a response to the context.
pub async fn send_response(&self, data: js_sys::Uint8Array) -> Result<(), JsValue> {
let ws_result: WebSocketResponse = match bson::from_slice(&data.to_vec()) {
Ok(res) => res,
Err(_) => {
// We don't care about the error if we can't parse it.
return Ok(());
}
};
let id = match &ws_result {
WebSocketResponse::Success(res) => res.request_id,
WebSocketResponse::Failure(res) => res.request_id,
};
let Some(id) = id else {
// We only care if we have an id.
return Ok(());
};
// Add this response to our responses.
self.add(id, ws_result.clone()).await;
Ok(())
}
}
impl ResponseContext {
pub async fn add(&self, id: Uuid, response: WebSocketResponse) {
self.responses.write().await.insert(id, response);
}
pub fn responses(&self) -> Arc<RwLock<IndexMap<Uuid, WebSocketResponse>>> {
self.responses.clone()
}
}
// Safety: WebAssembly will only ever run in a single-threaded context.
unsafe impl Send for EngineConnection {}
unsafe impl Sync for EngineConnection {}
impl EngineConnection {
pub async fn new(manager: EngineCommandManager) -> Result<EngineConnection, JsValue> {
pub async fn new(
manager: EngineCommandManager,
response_context: Arc<ResponseContext>,
) -> Result<EngineConnection, JsValue> {
#[allow(clippy::arc_with_non_send_sync)]
Ok(EngineConnection {
manager: Arc::new(manager),
batch: Arc::new(RwLock::new(Vec::new())),
batch_end: Arc::new(RwLock::new(IndexMap::new())),
responses: Arc::new(RwLock::new(IndexMap::new())),
response_context,
artifact_commands: Arc::new(RwLock::new(Vec::new())),
ids_of_async_commands: Arc::new(RwLock::new(IndexMap::new())),
default_planes: Default::default(),
stats: Default::default(),
})
}
async fn do_fire_modeling_cmd(
&self,
id: uuid::Uuid,
source_range: SourceRange,
cmd: WebSocketRequest,
id_to_source_range: HashMap<uuid::Uuid, SourceRange>,
) -> Result<(), KclError> {
let source_range_str = serde_json::to_string(&source_range).map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to serialize source range: {:?}", e),
source_ranges: vec![source_range],
})
})?;
let cmd_str = serde_json::to_string(&cmd).map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to serialize modeling command: {:?}", e),
source_ranges: vec![source_range],
})
})?;
let id_to_source_range_str = serde_json::to_string(&id_to_source_range).map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to serialize id to source range: {:?}", e),
source_ranges: vec![source_range],
})
})?;
self.manager
.fire_modeling_cmd_from_wasm(id.to_string(), source_range_str, cmd_str, id_to_source_range_str)
.map_err(|e| {
KclError::Engine(KclErrorDetails {
message: e.to_string().into(),
source_ranges: vec![source_range],
})
})?;
Ok(())
}
async fn do_send_modeling_cmd(
&self,
id: uuid::Uuid,
@ -151,7 +255,7 @@ impl crate::engine::EngineManager for EngineConnection {
}
fn responses(&self) -> Arc<RwLock<IndexMap<Uuid, WebSocketResponse>>> {
self.responses.clone()
self.response_context.responses.clone()
}
fn stats(&self) -> &EngineStats {
@ -162,6 +266,10 @@ impl crate::engine::EngineManager for EngineConnection {
self.artifact_commands.clone()
}
fn ids_of_async_commands(&self) -> Arc<RwLock<IndexMap<Uuid, SourceRange>>> {
self.ids_of_async_commands.clone()
}
fn get_default_planes(&self) -> Arc<RwLock<Option<DefaultPlanes>>> {
self.default_planes.clone()
}
@ -193,6 +301,19 @@ impl crate::engine::EngineManager for EngineConnection {
Ok(())
}
async fn inner_fire_modeling_cmd(
&self,
id: uuid::Uuid,
source_range: SourceRange,
cmd: WebSocketRequest,
id_to_source_range: HashMap<Uuid, SourceRange>,
) -> Result<(), KclError> {
self.do_fire_modeling_cmd(id, source_range, cmd, id_to_source_range)
.await?;
Ok(())
}
async fn inner_send_modeling_cmd(
&self,
id: uuid::Uuid,
@ -204,9 +325,7 @@ impl crate::engine::EngineManager for EngineConnection {
.do_send_modeling_cmd(id, source_range, cmd, id_to_source_range)
.await?;
let mut responses = self.responses.write().await;
responses.insert(id, ws_result.clone());
drop(responses);
self.response_context.add(id, ws_result.clone()).await;
Ok(ws_result)
}

View File

@ -76,6 +76,9 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
/// Get the artifact commands that have accumulated so far.
fn artifact_commands(&self) -> Arc<RwLock<Vec<ArtifactCommand>>>;
/// Get the ids of the async commands we are waiting for.
fn ids_of_async_commands(&self) -> Arc<RwLock<IndexMap<Uuid, SourceRange>>>;
/// Take the batch of commands that have accumulated so far and clear them.
async fn take_batch(&self) -> Vec<(WebSocketRequest, SourceRange)> {
std::mem::take(&mut *self.batch().write().await)
@ -96,6 +99,11 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
std::mem::take(&mut *self.artifact_commands().write().await)
}
/// Take the ids of async commands that have accumulated so far and clear them.
async fn take_ids_of_async_commands(&self) -> IndexMap<Uuid, SourceRange> {
std::mem::take(&mut *self.ids_of_async_commands().write().await)
}
/// Take the responses that have accumulated so far and clear them.
async fn take_responses(&self) -> IndexMap<Uuid, WebSocketResponse> {
std::mem::take(&mut *self.responses().write().await)
@ -136,8 +144,18 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
async fn clear_queues(&self) {
self.batch().write().await.clear();
self.batch_end().write().await.clear();
self.ids_of_async_commands().write().await.clear();
}
/// Send a modeling command and do not wait for the response message.
async fn inner_fire_modeling_cmd(
&self,
id: uuid::Uuid,
source_range: SourceRange,
cmd: WebSocketRequest,
id_to_source_range: HashMap<Uuid, SourceRange>,
) -> Result<(), crate::errors::KclError>;
/// Send a modeling command and wait for the response message.
async fn inner_send_modeling_cmd(
&self,
@ -180,6 +198,68 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
Ok(())
}
/// Ensure a specific async command has been completed.
async fn ensure_async_command_completed(
&self,
id: uuid::Uuid,
source_range: Option<SourceRange>,
) -> Result<OkWebSocketResponseData, KclError> {
let source_range = if let Some(source_range) = source_range {
source_range
} else {
// Look it up if we don't have it.
self.ids_of_async_commands()
.read()
.await
.get(&id)
.cloned()
.unwrap_or_default()
};
let current_time = instant::Instant::now();
while current_time.elapsed().as_secs() < 60 {
let responses = self.responses().read().await.clone();
let Some(resp) = responses.get(&id) else {
// Sleep for a little so we don't hog the CPU.
// No seriously WE DO NOT WANT TO PAUSE THE WHOLE APP ON THE JS SIDE.
let duration = instant::Duration::from_millis(100);
#[cfg(target_arch = "wasm32")]
wasm_timer::Delay::new(duration).await.map_err(|err| {
KclError::Internal(KclErrorDetails {
message: format!("Failed to sleep: {:?}", err),
source_ranges: vec![source_range],
})
})?;
#[cfg(not(target_arch = "wasm32"))]
tokio::time::sleep(duration).await;
continue;
};
// If the response is an error, return it.
// Parsing will do that and we can ignore the result, we don't care.
let response = self.parse_websocket_response(resp.clone(), source_range)?;
return Ok(response);
}
Err(KclError::Engine(KclErrorDetails {
message: "async command timed out".to_string(),
source_ranges: vec![source_range],
}))
}
/// Ensure ALL async commands have been completed.
async fn ensure_async_commands_completed(&self) -> Result<(), KclError> {
// Check if all async commands have been completed.
let ids = self.take_ids_of_async_commands().await;
// Try to get them from the responses.
for (id, source_range) in ids {
self.ensure_async_command_completed(id, Some(source_range)).await?;
}
Ok(())
}
/// Set the visibility of edges.
async fn set_edge_visibility(
&self,
@ -342,6 +422,36 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
self.run_batch(requests, source_range).await
}
/// Send the modeling cmd async and don't wait for the response.
/// Add it to our list of async commands.
async fn async_modeling_cmd(
&self,
id: uuid::Uuid,
source_range: SourceRange,
cmd: &ModelingCmd,
) -> Result<(), crate::errors::KclError> {
// Add the command ID to the list of async commands.
self.ids_of_async_commands().write().await.insert(id, source_range);
// Add to artifact commands.
self.handle_artifact_command(cmd, id.into(), &HashMap::from([(id, source_range)]))
.await?;
// Fire off the command now, but don't wait for the response, we don't care about it.
self.inner_fire_modeling_cmd(
id,
source_range,
WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
cmd: cmd.clone(),
cmd_id: id.into(),
}),
HashMap::from([(id, source_range)]),
)
.await?;
Ok(())
}
/// Run the batch for the specific commands.
async fn run_batch(
&self,

View File

@ -15,6 +15,8 @@ use crate::{
std::{args::TyF64, sketch::PlaneData},
};
use super::ExecutorContext;
type Point2D = kcmc::shared::Point2d<f64>;
type Point3D = kcmc::shared::Point3d<f64>;
@ -76,9 +78,45 @@ pub struct ImportedGeometry {
pub value: Vec<String>,
#[serde(skip)]
pub meta: Vec<Metadata>,
/// If the imported geometry has completed.
#[serde(skip)]
completed: bool,
}
/// Data for a solid or an imported geometry.
impl ImportedGeometry {
pub fn new(id: uuid::Uuid, value: Vec<String>, meta: Vec<Metadata>) -> Self {
Self {
id,
value,
meta,
completed: false,
}
}
async fn wait_for_finish(&mut self, ctx: &ExecutorContext) -> Result<(), KclError> {
if self.completed {
return Ok(());
}
ctx.engine
.ensure_async_command_completed(self.id, self.meta.first().map(|m| m.source_range))
.await?;
self.completed = true;
Ok(())
}
pub async fn id(&mut self, ctx: &ExecutorContext) -> Result<uuid::Uuid, KclError> {
if !self.completed {
self.wait_for_finish(ctx).await?;
}
Ok(self.id)
}
}
/// Data for a solid, sketch, or an imported geometry.
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
@ -128,11 +166,61 @@ impl From<SolidOrSketchOrImportedGeometry> for crate::execution::KclValue {
}
impl SolidOrSketchOrImportedGeometry {
pub(crate) fn ids(&self) -> Vec<uuid::Uuid> {
pub(crate) async fn ids(&mut self, ctx: &ExecutorContext) -> Result<Vec<uuid::Uuid>, KclError> {
match self {
SolidOrSketchOrImportedGeometry::ImportedGeometry(s) => vec![s.id],
SolidOrSketchOrImportedGeometry::SolidSet(s) => s.iter().map(|s| s.id).collect(),
SolidOrSketchOrImportedGeometry::SketchSet(s) => s.iter().map(|s| s.id).collect(),
SolidOrSketchOrImportedGeometry::ImportedGeometry(s) => {
let id = s.id(ctx).await?;
Ok(vec![id])
}
SolidOrSketchOrImportedGeometry::SolidSet(s) => Ok(s.iter().map(|s| s.id).collect()),
SolidOrSketchOrImportedGeometry::SketchSet(s) => Ok(s.iter().map(|s| s.id).collect()),
}
}
}
/// Data for a solid or an imported geometry.
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
#[allow(clippy::vec_box)]
pub enum SolidOrImportedGeometry {
ImportedGeometry(Box<ImportedGeometry>),
SolidSet(Vec<Solid>),
}
impl From<SolidOrImportedGeometry> for crate::execution::KclValue {
fn from(value: SolidOrImportedGeometry) -> Self {
match value {
SolidOrImportedGeometry::ImportedGeometry(s) => crate::execution::KclValue::ImportedGeometry(*s),
SolidOrImportedGeometry::SolidSet(mut s) => {
if s.len() == 1 {
crate::execution::KclValue::Solid {
value: Box::new(s.pop().unwrap()),
}
} else {
crate::execution::KclValue::HomArray {
value: s
.into_iter()
.map(|s| crate::execution::KclValue::Solid { value: Box::new(s) })
.collect(),
ty: crate::execution::types::RuntimeType::solid(),
}
}
}
}
}
}
impl SolidOrImportedGeometry {
pub(crate) async fn ids(&mut self, ctx: &ExecutorContext) -> Result<Vec<uuid::Uuid>, KclError> {
match self {
SolidOrImportedGeometry::ImportedGeometry(s) => {
let id = s.id(ctx).await?;
Ok(vec![id])
}
SolidOrImportedGeometry::SolidSet(s) => Ok(s.iter().map(|s| s.id).collect()),
}
}
}

View File

@ -5,10 +5,8 @@ use kcmc::{
coord::{System, KITTYCAD},
each_cmd as mcmd,
format::InputFormat3d,
ok_response::OkModelingCmdResponse,
shared::FileImportFormat,
units::UnitLength,
websocket::OkWebSocketResponseData,
ImportFile, ModelingCmd,
};
use kittycad_modeling_cmds as kcmc;
@ -289,34 +287,17 @@ pub struct PreImportedGeometry {
}
pub async fn send_to_engine(pre: PreImportedGeometry, ctxt: &ExecutorContext) -> Result<ImportedGeometry, KclError> {
if ctxt.no_engine_commands().await {
return Ok(ImportedGeometry {
id: pre.id,
value: pre.command.files.iter().map(|f| f.path.to_string()).collect(),
meta: vec![pre.source_range.into()],
});
}
let imported_geometry = ImportedGeometry::new(
pre.id,
pre.command.files.iter().map(|f| f.path.to_string()).collect(),
vec![pre.source_range.into()],
);
let resp = ctxt
.engine
.send_modeling_cmd(pre.id, pre.source_range, &ModelingCmd::from(pre.command.clone()))
ctxt.engine
.async_modeling_cmd(pre.id, pre.source_range, &ModelingCmd::from(pre.command.clone()))
.await?;
let OkWebSocketResponseData::Modeling {
modeling_response: OkModelingCmdResponse::ImportFiles(imported_files),
} = &resp
else {
return Err(KclError::Engine(KclErrorDetails {
message: format!("ImportFiles response was not as expected: {:?}", resp),
source_ranges: vec![pre.source_range],
}));
};
Ok(ImportedGeometry {
id: imported_files.object_id,
value: pre.command.files.iter().map(|f| f.path.to_string()).collect(),
meta: vec![pre.source_range.into()],
})
Ok(imported_geometry)
}
/// Get the source format from the extension.

View File

@ -947,7 +947,7 @@ impl ExecutorContext {
exec_state.global.artifact_graph.clone(),
module_id_to_module_path,
exec_state.global.id_to_source.clone(),
default_planes,
default_planes.clone(),
)
})?;
@ -957,6 +957,7 @@ impl ExecutorContext {
cache::write_old_memory((mem, exec_state.global.module_infos.clone())).await;
}
let session_data = self.engine.get_session_data().await;
Ok((env_ref, session_data))
}
@ -984,6 +985,9 @@ impl ExecutorContext {
)
.await;
// Ensure all the async commands completed.
self.engine.ensure_async_commands_completed().await?;
// If we errored out and early-returned, there might be commands which haven't been executed
// and should be dropped.
self.engine.clear_queues().await;

View File

@ -1338,11 +1338,11 @@ mod test {
value: Box::new(Plane::from_plane_data(crate::std::sketch::PlaneData::XY, exec_state)),
},
// No easy way to make a Face, Sketch, Solid, or Helix
KclValue::ImportedGeometry(crate::execution::ImportedGeometry {
id: uuid::Uuid::nil(),
value: Vec::new(),
meta: Vec::new(),
}),
KclValue::ImportedGeometry(crate::execution::ImportedGeometry::new(
uuid::Uuid::nil(),
Vec::new(),
Vec::new(),
)),
// Other values don't have types
]
}

View File

@ -109,7 +109,7 @@ pub mod exec {
pub mod wasm_engine {
pub use crate::{
coredump::wasm::{CoreDumpManager, CoreDumper},
engine::conn_wasm::{EngineCommandManager, EngineConnection},
engine::conn_wasm::{EngineCommandManager, EngineConnection, ResponseContext},
fs::wasm::{FileManager, FileSystemManager},
};
}

View File

@ -2579,3 +2579,24 @@ mod tangent_to_3_point_arc {
super::execute(TEST_NAME, true).await
}
}
mod import_async {
const TEST_NAME: &str = "import_async";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[tokio::test(flavor = "multi_thread")]
async fn unparse() {
super::unparse(TEST_NAME).await
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, true).await
}
}

View File

@ -13,7 +13,7 @@ use crate::{
errors::{KclError, KclErrorDetails},
execution::{
types::{NumericType, PrimitiveType, RuntimeType},
ExecState, KclValue, Solid,
ExecState, KclValue, SolidOrImportedGeometry,
},
std::Args,
};
@ -43,7 +43,11 @@ struct AppearanceData {
/// Set the appearance of a solid. This only works on solids, not sketches or individual paths.
pub async fn appearance(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let solids = args.get_unlabeled_kw_arg_typed("solids", &RuntimeType::solids(), exec_state)?;
let solids = args.get_unlabeled_kw_arg_typed(
"solids",
&RuntimeType::Union(vec![RuntimeType::solids(), RuntimeType::imported()]),
exec_state,
)?;
let color: String = args.get_kw_arg("color")?;
let count_ty = RuntimeType::Primitive(PrimitiveType::Number(NumericType::count()));
@ -270,6 +274,19 @@ pub async fn appearance(exec_state: &mut ExecState, args: Args) -> Result<KclVal
/// roughness = 50
/// )
/// ```
///
/// ```no_run
/// // Change the appearance of an imported model.
///
/// import "tests/inputs/cube.sldprt" as cube
///
/// cube
/// // |> appearance(
/// // color = "#ff0000",
/// // metalness = 50,
/// // roughness = 50
/// // )
/// ```
#[stdlib {
name = "appearance",
keywords = true,
@ -282,14 +299,16 @@ pub async fn appearance(exec_state: &mut ExecState, args: Args) -> Result<KclVal
}
}]
async fn inner_appearance(
solids: Vec<Solid>,
solids: SolidOrImportedGeometry,
color: String,
metalness: Option<f64>,
roughness: Option<f64>,
exec_state: &mut ExecState,
args: Args,
) -> Result<Vec<Solid>, KclError> {
for solid in &solids {
) -> Result<SolidOrImportedGeometry, KclError> {
let mut solids = solids.clone();
for solid_id in solids.ids(&args.ctx).await? {
// Set the material properties.
let rgb = rgba_simple::RGB::<f32>::from_hex(&color).map_err(|err| {
KclError::Semantic(KclErrorDetails {
@ -308,7 +327,7 @@ async fn inner_appearance(
args.batch_modeling_cmd(
exec_state.next_uuid(),
ModelingCmd::from(mcmd::ObjectSetMaterialParamsPbr {
object_id: solid.id,
object_id: solid_id,
color,
metalness: metalness.unwrap_or_default() as f32 / 100.0,
roughness: roughness.unwrap_or_default() as f32 / 100.0,

View File

@ -28,6 +28,9 @@ use crate::{
ModuleId,
};
const ERROR_STRING_SKETCH_TO_SOLID_HELPER: &str =
"You can convert a sketch (2D) into a Solid (3D) by calling a function like `extrude` or `revolve`";
#[derive(Debug, Clone)]
pub struct Arg {
/// The evaluated argument.
@ -220,18 +223,19 @@ impl Args {
ty.human_friendly_type(),
);
let suggestion = match (ty, actual_type_name) {
(RuntimeType::Primitive(PrimitiveType::Solid), "Sketch") => Some(
"You can convert a sketch (2D) into a Solid (3D) by calling a function like `extrude` or `revolve`",
),
(RuntimeType::Array(t, _), "Sketch") if **t == RuntimeType::Primitive(PrimitiveType::Solid) => Some(
"You can convert a sketch (2D) into a Solid (3D) by calling a function like `extrude` or `revolve`",
),
(RuntimeType::Primitive(PrimitiveType::Solid), "Sketch") => Some(ERROR_STRING_SKETCH_TO_SOLID_HELPER),
(RuntimeType::Array(t, _), "Sketch") if **t == RuntimeType::Primitive(PrimitiveType::Solid) => {
Some(ERROR_STRING_SKETCH_TO_SOLID_HELPER)
}
_ => None,
};
let message = match suggestion {
let mut message = match suggestion {
None => msg_base,
Some(sugg) => format!("{msg_base}. {sugg}"),
};
if message.contains("one or more Solids or imported geometry but it's actually of type Sketch") {
message = format!("{message}. {ERROR_STRING_SKETCH_TO_SOLID_HELPER}");
}
KclError::Semantic(KclErrorDetails {
source_ranges: arg.source_ranges(),
message,
@ -343,18 +347,20 @@ impl Args {
ty.human_friendly_type(),
);
let suggestion = match (ty, actual_type_name) {
(RuntimeType::Primitive(PrimitiveType::Solid), "Sketch") => Some(
"You can convert a sketch (2D) into a Solid (3D) by calling a function like `extrude` or `revolve`",
),
(RuntimeType::Array(ty, _), "Sketch") if **ty == RuntimeType::Primitive(PrimitiveType::Solid) => Some(
"You can convert a sketch (2D) into a Solid (3D) by calling a function like `extrude` or `revolve`",
),
(RuntimeType::Primitive(PrimitiveType::Solid), "Sketch") => Some(ERROR_STRING_SKETCH_TO_SOLID_HELPER),
(RuntimeType::Array(ty, _), "Sketch") if **ty == RuntimeType::Primitive(PrimitiveType::Solid) => {
Some(ERROR_STRING_SKETCH_TO_SOLID_HELPER)
}
_ => None,
};
let message = match suggestion {
let mut message = match suggestion {
None => msg_base,
Some(sugg) => format!("{msg_base}. {sugg}"),
};
if message.contains("one or more Solids or imported geometry but it's actually of type Sketch") {
message = format!("{message}. {ERROR_STRING_SKETCH_TO_SOLID_HELPER}");
}
KclError::Semantic(KclErrorDetails {
source_ranges: arg.source_ranges(),
message,
@ -1396,6 +1402,26 @@ impl<'a> FromKclValue<'a> for crate::execution::SolidOrSketchOrImportedGeometry
}
}
impl<'a> FromKclValue<'a> for crate::execution::SolidOrImportedGeometry {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
match arg {
KclValue::Solid { value } => Some(Self::SolidSet(vec![(**value).clone()])),
KclValue::HomArray { value, .. } => {
let mut solids = vec![];
for item in value {
match item {
KclValue::Solid { value } => solids.push((**value).clone()),
_ => return None,
}
}
Some(Self::SolidSet(solids))
}
KclValue::ImportedGeometry(value) => Some(Self::ImportedGeometry(Box::new(value.clone()))),
_ => None,
}
}
}
impl<'a> FromKclValue<'a> for super::sketch::SketchData {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
// Order is critical since PlaneData is a subset of Plane.

View File

@ -171,7 +171,8 @@ async fn inner_scale(
args.flush_batch_for_solids(exec_state, solids).await?;
}
for object_id in objects.ids() {
let mut objects = objects.clone();
for object_id in objects.ids(&args.ctx).await? {
let id = exec_state.next_uuid();
args.batch_modeling_cmd(
@ -409,7 +410,8 @@ async fn inner_translate(
args.flush_batch_for_solids(exec_state, solids).await?;
}
for object_id in objects.ids() {
let mut objects = objects.clone();
for object_id in objects.ids(&args.ctx).await? {
let id = exec_state.next_uuid();
args.batch_modeling_cmd(
@ -774,7 +776,8 @@ async fn inner_rotate(
args.flush_batch_for_solids(exec_state, solids).await?;
}
for object_id in objects.ids() {
let mut objects = objects.clone();
for object_id in objects.ids(&args.ctx).await? {
let id = exec_state.next_uuid();
if let (Some(axis), Some(angle)) = (axis, angle) {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,6 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Artifact graph flowchart import_async.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,503 @@
```mermaid
flowchart LR
subgraph path3 [Path]
3["Path<br>[1035, 1085, 0]"]
4["Segment<br>[1035, 1085, 0]"]
5[Solid2d]
end
subgraph path13 [Path]
13["Path<br>[1562, 1599, 0]"]
14["Segment<br>[1250, 1288, 0]"]
15["Segment<br>[1250, 1288, 0]"]
16["Segment<br>[1250, 1288, 0]"]
17["Segment<br>[1250, 1288, 0]"]
18["Segment<br>[1250, 1288, 0]"]
19["Segment<br>[1250, 1288, 0]"]
20["Segment<br>[1250, 1288, 0]"]
21["Segment<br>[1250, 1288, 0]"]
22["Segment<br>[1250, 1288, 0]"]
23["Segment<br>[1250, 1288, 0]"]
24["Segment<br>[1250, 1288, 0]"]
25["Segment<br>[1250, 1288, 0]"]
26["Segment<br>[1250, 1288, 0]"]
27["Segment<br>[1250, 1288, 0]"]
28["Segment<br>[1250, 1288, 0]"]
29["Segment<br>[1250, 1288, 0]"]
30["Segment<br>[1250, 1288, 0]"]
31["Segment<br>[1250, 1288, 0]"]
32["Segment<br>[1250, 1288, 0]"]
33["Segment<br>[1250, 1288, 0]"]
34["Segment<br>[1250, 1288, 0]"]
35["Segment<br>[1250, 1288, 0]"]
36["Segment<br>[1250, 1288, 0]"]
37["Segment<br>[1250, 1288, 0]"]
38["Segment<br>[1250, 1288, 0]"]
39["Segment<br>[1250, 1288, 0]"]
40["Segment<br>[1250, 1288, 0]"]
41["Segment<br>[1250, 1288, 0]"]
42["Segment<br>[1250, 1288, 0]"]
43["Segment<br>[1250, 1288, 0]"]
44["Segment<br>[1250, 1288, 0]"]
45["Segment<br>[1250, 1288, 0]"]
46["Segment<br>[1250, 1288, 0]"]
47["Segment<br>[1250, 1288, 0]"]
48["Segment<br>[1250, 1288, 0]"]
49["Segment<br>[1250, 1288, 0]"]
50["Segment<br>[1250, 1288, 0]"]
51["Segment<br>[1250, 1288, 0]"]
52["Segment<br>[1250, 1288, 0]"]
53["Segment<br>[1250, 1288, 0]"]
54["Segment<br>[1250, 1288, 0]"]
55["Segment<br>[1250, 1288, 0]"]
56["Segment<br>[1250, 1288, 0]"]
57["Segment<br>[1250, 1288, 0]"]
58["Segment<br>[1250, 1288, 0]"]
59["Segment<br>[1250, 1288, 0]"]
60["Segment<br>[1250, 1288, 0]"]
61["Segment<br>[1250, 1288, 0]"]
62["Segment<br>[1250, 1288, 0]"]
63["Segment<br>[1250, 1288, 0]"]
64["Segment<br>[1250, 1288, 0]"]
65["Segment<br>[1250, 1288, 0]"]
66["Segment<br>[1250, 1288, 0]"]
67["Segment<br>[1250, 1288, 0]"]
68["Segment<br>[1250, 1288, 0]"]
69["Segment<br>[1250, 1288, 0]"]
70["Segment<br>[1250, 1288, 0]"]
71["Segment<br>[1250, 1288, 0]"]
72["Segment<br>[1250, 1288, 0]"]
73["Segment<br>[1250, 1288, 0]"]
74["Segment<br>[1250, 1288, 0]"]
75["Segment<br>[1250, 1288, 0]"]
76["Segment<br>[1250, 1288, 0]"]
77["Segment<br>[1250, 1288, 0]"]
78["Segment<br>[1250, 1288, 0]"]
79["Segment<br>[1250, 1288, 0]"]
80["Segment<br>[1250, 1288, 0]"]
81["Segment<br>[1250, 1288, 0]"]
82["Segment<br>[1250, 1288, 0]"]
83["Segment<br>[1250, 1288, 0]"]
84["Segment<br>[1250, 1288, 0]"]
85["Segment<br>[1250, 1288, 0]"]
86["Segment<br>[1250, 1288, 0]"]
87["Segment<br>[1250, 1288, 0]"]
88["Segment<br>[1250, 1288, 0]"]
89["Segment<br>[1250, 1288, 0]"]
90["Segment<br>[1250, 1288, 0]"]
91["Segment<br>[1250, 1288, 0]"]
92["Segment<br>[1250, 1288, 0]"]
93["Segment<br>[1250, 1288, 0]"]
94["Segment<br>[1250, 1288, 0]"]
95["Segment<br>[1250, 1288, 0]"]
96["Segment<br>[1250, 1288, 0]"]
97["Segment<br>[1250, 1288, 0]"]
98["Segment<br>[1250, 1288, 0]"]
99["Segment<br>[1250, 1288, 0]"]
100["Segment<br>[1250, 1288, 0]"]
101["Segment<br>[1250, 1288, 0]"]
102["Segment<br>[1250, 1288, 0]"]
103["Segment<br>[1250, 1288, 0]"]
104["Segment<br>[1250, 1288, 0]"]
105["Segment<br>[1250, 1288, 0]"]
106["Segment<br>[1250, 1288, 0]"]
107["Segment<br>[1250, 1288, 0]"]
108["Segment<br>[1250, 1288, 0]"]
109["Segment<br>[1250, 1288, 0]"]
110["Segment<br>[1250, 1288, 0]"]
111["Segment<br>[1250, 1288, 0]"]
112["Segment<br>[1250, 1288, 0]"]
113["Segment<br>[1250, 1288, 0]"]
114["Segment<br>[1250, 1288, 0]"]
115["Segment<br>[1651, 1753, 0]"]
116["Segment<br>[1478, 1508, 0]"]
117["Segment<br>[1478, 1508, 0]"]
118["Segment<br>[1478, 1508, 0]"]
119["Segment<br>[1478, 1508, 0]"]
120["Segment<br>[1478, 1508, 0]"]
121["Segment<br>[1478, 1508, 0]"]
122["Segment<br>[1478, 1508, 0]"]
123["Segment<br>[1478, 1508, 0]"]
124["Segment<br>[1478, 1508, 0]"]
125["Segment<br>[1478, 1508, 0]"]
126["Segment<br>[1478, 1508, 0]"]
127["Segment<br>[1478, 1508, 0]"]
128["Segment<br>[1478, 1508, 0]"]
129["Segment<br>[1478, 1508, 0]"]
130["Segment<br>[1478, 1508, 0]"]
131["Segment<br>[1478, 1508, 0]"]
132["Segment<br>[1478, 1508, 0]"]
133["Segment<br>[1478, 1508, 0]"]
134["Segment<br>[1478, 1508, 0]"]
135["Segment<br>[1478, 1508, 0]"]
136["Segment<br>[1478, 1508, 0]"]
137["Segment<br>[1478, 1508, 0]"]
138["Segment<br>[1478, 1508, 0]"]
139["Segment<br>[1478, 1508, 0]"]
140["Segment<br>[1478, 1508, 0]"]
141["Segment<br>[1478, 1508, 0]"]
142["Segment<br>[1478, 1508, 0]"]
143["Segment<br>[1478, 1508, 0]"]
144["Segment<br>[1478, 1508, 0]"]
145["Segment<br>[1478, 1508, 0]"]
146["Segment<br>[1478, 1508, 0]"]
147["Segment<br>[1478, 1508, 0]"]
148["Segment<br>[1478, 1508, 0]"]
149["Segment<br>[1478, 1508, 0]"]
150["Segment<br>[1478, 1508, 0]"]
151["Segment<br>[1478, 1508, 0]"]
152["Segment<br>[1478, 1508, 0]"]
153["Segment<br>[1478, 1508, 0]"]
154["Segment<br>[1478, 1508, 0]"]
155["Segment<br>[1478, 1508, 0]"]
156["Segment<br>[1478, 1508, 0]"]
157["Segment<br>[1478, 1508, 0]"]
158["Segment<br>[1478, 1508, 0]"]
159["Segment<br>[1478, 1508, 0]"]
160["Segment<br>[1478, 1508, 0]"]
161["Segment<br>[1478, 1508, 0]"]
162["Segment<br>[1478, 1508, 0]"]
163["Segment<br>[1478, 1508, 0]"]
164["Segment<br>[1478, 1508, 0]"]
165["Segment<br>[1478, 1508, 0]"]
166["Segment<br>[1478, 1508, 0]"]
167["Segment<br>[1478, 1508, 0]"]
168["Segment<br>[1478, 1508, 0]"]
169["Segment<br>[1478, 1508, 0]"]
170["Segment<br>[1478, 1508, 0]"]
171["Segment<br>[1478, 1508, 0]"]
172["Segment<br>[1478, 1508, 0]"]
173["Segment<br>[1478, 1508, 0]"]
174["Segment<br>[1478, 1508, 0]"]
175["Segment<br>[1478, 1508, 0]"]
176["Segment<br>[1478, 1508, 0]"]
177["Segment<br>[1478, 1508, 0]"]
178["Segment<br>[1478, 1508, 0]"]
179["Segment<br>[1478, 1508, 0]"]
180["Segment<br>[1478, 1508, 0]"]
181["Segment<br>[1478, 1508, 0]"]
182["Segment<br>[1478, 1508, 0]"]
183["Segment<br>[1478, 1508, 0]"]
184["Segment<br>[1478, 1508, 0]"]
185["Segment<br>[1478, 1508, 0]"]
186["Segment<br>[1478, 1508, 0]"]
187["Segment<br>[1478, 1508, 0]"]
188["Segment<br>[1478, 1508, 0]"]
189["Segment<br>[1478, 1508, 0]"]
190["Segment<br>[1478, 1508, 0]"]
191["Segment<br>[1478, 1508, 0]"]
192["Segment<br>[1478, 1508, 0]"]
193["Segment<br>[1478, 1508, 0]"]
194["Segment<br>[1478, 1508, 0]"]
195["Segment<br>[1478, 1508, 0]"]
196["Segment<br>[1478, 1508, 0]"]
197["Segment<br>[1478, 1508, 0]"]
198["Segment<br>[1478, 1508, 0]"]
199["Segment<br>[1478, 1508, 0]"]
200["Segment<br>[1478, 1508, 0]"]
201["Segment<br>[1478, 1508, 0]"]
202["Segment<br>[1478, 1508, 0]"]
203["Segment<br>[1478, 1508, 0]"]
204["Segment<br>[1478, 1508, 0]"]
205["Segment<br>[1478, 1508, 0]"]
206["Segment<br>[1478, 1508, 0]"]
207["Segment<br>[1478, 1508, 0]"]
208["Segment<br>[1478, 1508, 0]"]
209["Segment<br>[1478, 1508, 0]"]
210["Segment<br>[1478, 1508, 0]"]
211["Segment<br>[1478, 1508, 0]"]
212["Segment<br>[1478, 1508, 0]"]
213["Segment<br>[1478, 1508, 0]"]
214["Segment<br>[1478, 1508, 0]"]
215["Segment<br>[1478, 1508, 0]"]
216["Segment<br>[1478, 1508, 0]"]
217["Segment<br>[1799, 1806, 0]"]
218[Solid2d]
end
subgraph path220 [Path]
220["Path<br>[2287, 2387, 0]"]
221["Segment<br>[2393, 2420, 0]"]
222["Segment<br>[2426, 2454, 0]"]
223["Segment<br>[2460, 2488, 0]"]
224["Segment<br>[2494, 2614, 0]"]
225["Segment<br>[2620, 2729, 0]"]
226["Segment<br>[2735, 2742, 0]"]
227[Solid2d]
end
1["Plane<br>[168, 185, 0]"]
2["Plane<br>[1012, 1029, 0]"]
6["Sweep Extrusion<br>[1091, 1119, 0]"]
7[Wall]
8["Cap Start"]
9["Cap End"]
10["SweepEdge Opposite"]
11["SweepEdge Adjacent"]
12["Plane<br>[1539, 1556, 0]"]
219["Sweep Extrusion<br>[1812, 1840, 0]"]
228["Sweep Extrusion<br>[2748, 2777, 0]"]
229[Wall]
230[Wall]
231[Wall]
232[Wall]
233["SweepEdge Opposite"]
234["SweepEdge Adjacent"]
235["SweepEdge Opposite"]
236["SweepEdge Adjacent"]
237["SweepEdge Opposite"]
238["SweepEdge Adjacent"]
239["SweepEdge Opposite"]
240["SweepEdge Adjacent"]
241["StartSketchOnFace<br>[2250, 2281, 0]"]
2 --- 3
3 --- 4
3 ---- 6
3 --- 5
4 --- 7
4 --- 10
4 --- 11
6 --- 7
6 --- 8
6 --- 9
6 --- 10
6 --- 11
9 --- 220
12 --- 13
13 --- 14
13 --- 15
13 --- 16
13 --- 17
13 --- 18
13 --- 19
13 --- 20
13 --- 21
13 --- 22
13 --- 23
13 --- 24
13 --- 25
13 --- 26
13 --- 27
13 --- 28
13 --- 29
13 --- 30
13 --- 31
13 --- 32
13 --- 33
13 --- 34
13 --- 35
13 --- 36
13 --- 37
13 --- 38
13 --- 39
13 --- 40
13 --- 41
13 --- 42
13 --- 43
13 --- 44
13 --- 45
13 --- 46
13 --- 47
13 --- 48
13 --- 49
13 --- 50
13 --- 51
13 --- 52
13 --- 53
13 --- 54
13 --- 55
13 --- 56
13 --- 57
13 --- 58
13 --- 59
13 --- 60
13 --- 61
13 --- 62
13 --- 63
13 --- 64
13 --- 65
13 --- 66
13 --- 67
13 --- 68
13 --- 69
13 --- 70
13 --- 71
13 --- 72
13 --- 73
13 --- 74
13 --- 75
13 --- 76
13 --- 77
13 --- 78
13 --- 79
13 --- 80
13 --- 81
13 --- 82
13 --- 83
13 --- 84
13 --- 85
13 --- 86
13 --- 87
13 --- 88
13 --- 89
13 --- 90
13 --- 91
13 --- 92
13 --- 93
13 --- 94
13 --- 95
13 --- 96
13 --- 97
13 --- 98
13 --- 99
13 --- 100
13 --- 101
13 --- 102
13 --- 103
13 --- 104
13 --- 105
13 --- 106
13 --- 107
13 --- 108
13 --- 109
13 --- 110
13 --- 111
13 --- 112
13 --- 113
13 --- 114
13 --- 115
13 --- 116
13 --- 117
13 --- 118
13 --- 119
13 --- 120
13 --- 121
13 --- 122
13 --- 123
13 --- 124
13 --- 125
13 --- 126
13 --- 127
13 --- 128
13 --- 129
13 --- 130
13 --- 131
13 --- 132
13 --- 133
13 --- 134
13 --- 135
13 --- 136
13 --- 137
13 --- 138
13 --- 139
13 --- 140
13 --- 141
13 --- 142
13 --- 143
13 --- 144
13 --- 145
13 --- 146
13 --- 147
13 --- 148
13 --- 149
13 --- 150
13 --- 151
13 --- 152
13 --- 153
13 --- 154
13 --- 155
13 --- 156
13 --- 157
13 --- 158
13 --- 159
13 --- 160
13 --- 161
13 --- 162
13 --- 163
13 --- 164
13 --- 165
13 --- 166
13 --- 167
13 --- 168
13 --- 169
13 --- 170
13 --- 171
13 --- 172
13 --- 173
13 --- 174
13 --- 175
13 --- 176
13 --- 177
13 --- 178
13 --- 179
13 --- 180
13 --- 181
13 --- 182
13 --- 183
13 --- 184
13 --- 185
13 --- 186
13 --- 187
13 --- 188
13 --- 189
13 --- 190
13 --- 191
13 --- 192
13 --- 193
13 --- 194
13 --- 195
13 --- 196
13 --- 197
13 --- 198
13 --- 199
13 --- 200
13 --- 201
13 --- 202
13 --- 203
13 --- 204
13 --- 205
13 --- 206
13 --- 207
13 --- 208
13 --- 209
13 --- 210
13 --- 211
13 --- 212
13 --- 213
13 --- 214
13 --- 215
13 --- 216
13 --- 217
13 ---- 219
13 --- 218
220 --- 221
220 --- 222
220 --- 223
220 --- 224
220 --- 225
220 --- 226
220 ---- 228
220 --- 227
221 --- 232
221 --- 239
221 --- 240
222 --- 231
222 --- 237
222 --- 238
223 --- 230
223 --- 235
223 --- 236
225 --- 229
225 --- 233
225 --- 234
228 --- 229
228 --- 230
228 --- 231
228 --- 232
228 --- 233
228 --- 234
228 --- 235
228 --- 236
228 --- 237
228 --- 238
228 --- 239
228 --- 240
9 <--x 241
```

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,117 @@
@(lengthUnit = m)
import "../../e2e/executor/inputs/2-5-long-m8-chc-screw.stl" as screw
// Set units
@settings(defaultLengthUnit = mm)
myScrew = screw
surface001 = startSketchOn(XY)
// Define parameters
nTeeth = 21
module = 0.5
pitchDiameter = module * nTeeth
pressureAngle = 20
addendum = module
deddendum = 1.25 * module
baseDiameter = pitchDiameter * cos(toRadians(pressureAngle))
tipDiameter = pitchDiameter + 2 * module
gearHeight = 3
// Interpolate points along the involute curve
cmo = 101
rs = map([0..cmo], fn(i) {
return baseDiameter / 2 + i / cmo * (tipDiameter - baseDiameter) / 2
})
// Calculate operating pressure angle
angles = map(rs, fn(r) {
return toDegrees( acos(baseDiameter / 2 / r))
})
// Calculate the involute function
invas = map(angles, fn(a) {
return tan(toRadians(a)) - toRadians(a)
})
// Map the involute curve
xs = map([0..cmo], fn(i) {
return rs[i] * cos(invas[i])
})
ys = map([0..cmo], fn(i) {
return rs[i] * sin(invas[i])
})
// Extrude the gear body
body = startSketchOn(XY)
|> circle(center = [0, 0], radius = baseDiameter / 2)
|> extrude(length = gearHeight)
toothAngle = 360 / nTeeth / 1.5
// Plot the involute curve
fn leftInvolute(i, sg) {
j = 100 - i // iterate backwards
return line(sg, endAbsolute = [xs[j], ys[j]])
}
fn rightInvolute(i, sg) {
x = rs[i] * cos(toRadians(-toothAngle + toDegrees(atan(ys[i] / xs[i]))))
y = -rs[i] * sin(toRadians(-toothAngle + toDegrees(atan(ys[i] / xs[i]))))
return line(sg, endAbsolute = [x, y])
}
// Draw gear teeth
start = startSketchOn(XY)
|> startProfileAt([xs[101], ys[101]], %)
teeth = reduce([0..100], start, leftInvolute)
|> arc({
angleStart = 0,
angleEnd = toothAngle,
radius = baseDiameter / 2
}, %)
|> reduce([1..101], %, rightInvolute)
|> close()
|> extrude(length = gearHeight)
|> patternCircular3d(
axis = [0, 0, 1],
center = [0, 0, 0],
instances = nTeeth,
arcDegrees = 360,
rotateDuplicates = true,
)
// Define the constants of the keyway and the bore hole
keywayWidth = 0.250
keywayDepth = keywayWidth / 2
holeDiam = 2
holeRadius = 1
startAngle = asin(keywayWidth / 2 / holeRadius)
// Sketch the keyway and center hole and extrude
keyWay = startSketchOn(body, face = END)
|> startProfileAt([
holeRadius * cos(startAngle),
holeRadius * sin(startAngle)
], %)
|> xLine(length = keywayDepth)
|> yLine(length = -keywayWidth)
|> xLine(length = -keywayDepth)
|> arc({
angleEnd = 180,
angleStart = -1 * toDegrees(startAngle) + 360,
radius = holeRadius
}, %)
|> arc({
angleEnd = toDegrees(startAngle),
angleStart = 180,
radius = holeRadius
}, %)
|> close()
|> extrude(length = -gearHeight)
myScrew
|> translate(y=10)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

View File

@ -0,0 +1,120 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Result of unparsing import_async.kcl
---
// Set units
@settings(defaultLengthUnit = mm)
@(lengthUnit = m)
import "../../e2e/executor/inputs/2-5-long-m8-chc-screw.stl" as screw
myScrew = screw
surface001 = startSketchOn(XY)
// Define parameters
nTeeth = 21
module = 0.5
pitchDiameter = module * nTeeth
pressureAngle = 20
addendum = module
deddendum = 1.25 * module
baseDiameter = pitchDiameter * cos(toRadians(pressureAngle))
tipDiameter = pitchDiameter + 2 * module
gearHeight = 3
// Interpolate points along the involute curve
cmo = 101
rs = map([0..cmo], fn(i) {
return baseDiameter / 2 + i / cmo * (tipDiameter - baseDiameter) / 2
})
// Calculate operating pressure angle
angles = map(rs, fn(r) {
return toDegrees( acos(baseDiameter / 2 / r))
})
// Calculate the involute function
invas = map(angles, fn(a) {
return tan(toRadians(a)) - toRadians(a)
})
// Map the involute curve
xs = map([0..cmo], fn(i) {
return rs[i] * cos(invas[i])
})
ys = map([0..cmo], fn(i) {
return rs[i] * sin(invas[i])
})
// Extrude the gear body
body = startSketchOn(XY)
|> circle(center = [0, 0], radius = baseDiameter / 2)
|> extrude(length = gearHeight)
toothAngle = 360 / nTeeth / 1.5
// Plot the involute curve
fn leftInvolute(i, sg) {
j = 100 - i // iterate backwards
return line(sg, endAbsolute = [xs[j], ys[j]])
}
fn rightInvolute(i, sg) {
x = rs[i] * cos(toRadians(-toothAngle + toDegrees(atan(ys[i] / xs[i]))))
y = -rs[i] * sin(toRadians(-toothAngle + toDegrees(atan(ys[i] / xs[i]))))
return line(sg, endAbsolute = [x, y])
}
// Draw gear teeth
start = startSketchOn(XY)
|> startProfileAt([xs[101], ys[101]], %)
teeth = reduce([0..100], start, leftInvolute)
|> arc({
angleStart = 0,
angleEnd = toothAngle,
radius = baseDiameter / 2
}, %)
|> reduce([1..101], %, rightInvolute)
|> close()
|> extrude(length = gearHeight)
|> patternCircular3d(
axis = [0, 0, 1],
center = [0, 0, 0],
instances = nTeeth,
arcDegrees = 360,
rotateDuplicates = true,
)
// Define the constants of the keyway and the bore hole
keywayWidth = 0.250
keywayDepth = keywayWidth / 2
holeDiam = 2
holeRadius = 1
startAngle = asin(keywayWidth / 2 / holeRadius)
// Sketch the keyway and center hole and extrude
keyWay = startSketchOn(body, face = END)
|> startProfileAt([
holeRadius * cos(startAngle),
holeRadius * sin(startAngle)
], %)
|> xLine(length = keywayDepth)
|> yLine(length = -keywayWidth)
|> xLine(length = -keywayDepth)
|> arc({
angleEnd = 180,
angleStart = -1 * toDegrees(startAngle) + 360,
radius = holeRadius
}, %)
|> arc({
angleEnd = toDegrees(startAngle),
angleStart = 180,
radius = holeRadius
}, %)
|> close()
|> extrude(length = -gearHeight)
myScrew
|> translate(y = 10)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,6 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Artifact graph flowchart axial-fan.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,754 @@
```mermaid
flowchart LR
subgraph path2 [Path]
2["Path<br>[323, 370, 5]"]
3["Segment<br>[376, 444, 5]"]
4["Segment<br>[450, 550, 5]"]
5["Segment<br>[556, 673, 5]"]
6["Segment<br>[679, 764, 5]"]
7["Segment<br>[770, 777, 5]"]
8[Solid2d]
end
subgraph path9 [Path]
9["Path<br>[788, 823, 5]"]
10["Segment<br>[788, 823, 5]"]
11[Solid2d]
end
subgraph path12 [Path]
12["Path<br>[838, 985, 5]"]
13["Segment<br>[838, 985, 5]"]
14[Solid2d]
end
subgraph path15 [Path]
15["Path<br>[1000, 1148, 5]"]
16["Segment<br>[1000, 1148, 5]"]
17[Solid2d]
end
subgraph path18 [Path]
18["Path<br>[1163, 1311, 5]"]
19["Segment<br>[1163, 1311, 5]"]
20[Solid2d]
end
subgraph path21 [Path]
21["Path<br>[1326, 1475, 5]"]
22["Segment<br>[1326, 1475, 5]"]
23[Solid2d]
end
subgraph path39 [Path]
39["Path<br>[1646, 1702, 5]"]
40["Segment<br>[1708, 1773, 5]"]
41["Segment<br>[1779, 1831, 5]"]
42["Segment<br>[1837, 1888, 5]"]
43["Segment<br>[1894, 1946, 5]"]
44["Segment<br>[1952, 2018, 5]"]
45["Segment<br>[2024, 2076, 5]"]
46["Segment<br>[2082, 2114, 5]"]
47["Segment<br>[2120, 2185, 5]"]
48["Segment<br>[2191, 2198, 5]"]
49[Solid2d]
end
subgraph path78 [Path]
78["Path<br>[2547, 2660, 5]"]
79["Segment<br>[2666, 2721, 5]"]
80["Segment<br>[2727, 2762, 5]"]
81["Segment<br>[2768, 2823, 5]"]
82["Segment<br>[2829, 2865, 5]"]
83["Segment<br>[2871, 2926, 5]"]
84["Segment<br>[2932, 2968, 5]"]
85["Segment<br>[2974, 3029, 5]"]
86["Segment<br>[3035, 3091, 5]"]
end
subgraph path113 [Path]
113["Path<br>[3240, 3291, 5]"]
114["Segment<br>[3240, 3291, 5]"]
115[Solid2d]
end
subgraph path120 [Path]
120["Path<br>[3470, 3529, 5]"]
121["Segment<br>[3535, 3603, 5]"]
122["Segment<br>[3609, 3709, 5]"]
123["Segment<br>[3715, 3832, 5]"]
124["Segment<br>[3838, 3923, 5]"]
125["Segment<br>[3929, 3936, 5]"]
126[Solid2d]
end
subgraph path127 [Path]
127["Path<br>[3947, 3998, 5]"]
128["Segment<br>[3947, 3998, 5]"]
129[Solid2d]
end
subgraph path130 [Path]
130["Path<br>[4013, 4160, 5]"]
131["Segment<br>[4013, 4160, 5]"]
132[Solid2d]
end
subgraph path133 [Path]
133["Path<br>[4175, 4323, 5]"]
134["Segment<br>[4175, 4323, 5]"]
135[Solid2d]
end
subgraph path136 [Path]
136["Path<br>[4338, 4486, 5]"]
137["Segment<br>[4338, 4486, 5]"]
138[Solid2d]
end
subgraph path139 [Path]
139["Path<br>[4501, 4650, 5]"]
140["Segment<br>[4501, 4650, 5]"]
141[Solid2d]
end
subgraph path157 [Path]
157["Path<br>[4795, 4833, 5]"]
158["Segment<br>[4795, 4833, 5]"]
159[Solid2d]
end
subgraph path165 [Path]
165["Path<br>[4906, 4942, 5]"]
166["Segment<br>[4906, 4942, 5]"]
167[Solid2d]
end
subgraph path181 [Path]
181["Path<br>[277, 327, 6]"]
182["Segment<br>[277, 327, 6]"]
183[Solid2d]
end
subgraph path191 [Path]
191["Path<br>[502, 537, 6]"]
192["Segment<br>[502, 537, 6]"]
193[Solid2d]
end
subgraph path203 [Path]
203["Path<br>[216, 255, 7]"]
204["Segment<br>[261, 291, 7]"]
205["Segment<br>[297, 336, 7]"]
206["Segment<br>[342, 366, 7]"]
207["Segment<br>[372, 396, 7]"]
208["Segment<br>[402, 443, 7]"]
209["Segment<br>[449, 487, 7]"]
210["Segment<br>[493, 516, 7]"]
211["Segment<br>[522, 539, 7]"]
212["Segment<br>[545, 566, 7]"]
213["Segment<br>[572, 659, 7]"]
214["Segment<br>[665, 702, 7]"]
215["Segment<br>[708, 745, 7]"]
216["Segment<br>[751, 758, 7]"]
217[Solid2d]
end
subgraph path243 [Path]
243["Path<br>[1100, 1212, 7]"]
244["Segment<br>[1220, 1330, 7]"]
245["Segment<br>[1338, 1672, 7]"]
246["Segment<br>[1680, 2016, 7]"]
247["Segment<br>[2024, 2255, 7]"]
248["Segment<br>[2263, 2270, 7]"]
249[Solid2d]
end
subgraph path251 [Path]
251["Path<br>[1100, 1212, 7]"]
252["Segment<br>[1220, 1330, 7]"]
253["Segment<br>[1338, 1672, 7]"]
254["Segment<br>[1680, 2016, 7]"]
255["Segment<br>[2024, 2255, 7]"]
256["Segment<br>[2263, 2270, 7]"]
257[Solid2d]
end
subgraph path259 [Path]
259["Path<br>[1100, 1212, 7]"]
264["Segment<br>[2263, 2270, 7]"]
265[Solid2d]
end
1["Plane<br>[300, 317, 5]"]
24["Sweep Extrusion<br>[1485, 1504, 5]"]
25[Wall]
26[Wall]
27[Wall]
28[Wall]
29["Cap Start"]
30["Cap End"]
31["SweepEdge Opposite"]
32["SweepEdge Adjacent"]
33["SweepEdge Opposite"]
34["SweepEdge Adjacent"]
35["SweepEdge Opposite"]
36["SweepEdge Adjacent"]
37["SweepEdge Opposite"]
38["SweepEdge Adjacent"]
50["Sweep Extrusion<br>[2338, 2358, 5]"]
51[Wall]
52[Wall]
53[Wall]
54[Wall]
55[Wall]
56[Wall]
57[Wall]
58[Wall]
59["SweepEdge Opposite"]
60["SweepEdge Adjacent"]
61["SweepEdge Opposite"]
62["SweepEdge Adjacent"]
63["SweepEdge Opposite"]
64["SweepEdge Adjacent"]
65["SweepEdge Opposite"]
66["SweepEdge Adjacent"]
67["SweepEdge Opposite"]
68["SweepEdge Adjacent"]
69["SweepEdge Opposite"]
70["SweepEdge Adjacent"]
71["SweepEdge Opposite"]
72["SweepEdge Adjacent"]
73["SweepEdge Opposite"]
74["SweepEdge Adjacent"]
75["Sweep Extrusion<br>[2338, 2358, 5]"]
76["Sweep Extrusion<br>[2338, 2358, 5]"]
77["Sweep Extrusion<br>[2338, 2358, 5]"]
87["Sweep Extrusion<br>[3097, 3132, 5]"]
88[Wall]
89[Wall]
90[Wall]
91[Wall]
92[Wall]
93[Wall]
94[Wall]
95[Wall]
96["Cap End"]
97["SweepEdge Opposite"]
98["SweepEdge Adjacent"]
99["SweepEdge Opposite"]
100["SweepEdge Adjacent"]
101["SweepEdge Opposite"]
102["SweepEdge Adjacent"]
103["SweepEdge Opposite"]
104["SweepEdge Adjacent"]
105["SweepEdge Opposite"]
106["SweepEdge Adjacent"]
107["SweepEdge Opposite"]
108["SweepEdge Adjacent"]
109["SweepEdge Opposite"]
110["SweepEdge Adjacent"]
111["SweepEdge Opposite"]
112["SweepEdge Adjacent"]
116["Sweep Extrusion<br>[3297, 3335, 5]"]
117[Wall]
118["SweepEdge Opposite"]
119["SweepEdge Adjacent"]
142["Sweep Extrusion<br>[4660, 4679, 5]"]
143[Wall]
144[Wall]
145[Wall]
146[Wall]
147["Cap Start"]
148["Cap End"]
149["SweepEdge Opposite"]
150["SweepEdge Adjacent"]
151["SweepEdge Opposite"]
152["SweepEdge Adjacent"]
153["SweepEdge Opposite"]
154["SweepEdge Adjacent"]
155["SweepEdge Opposite"]
156["SweepEdge Adjacent"]
160["Sweep Extrusion<br>[4839, 4859, 5]"]
161[Wall]
162["Cap End"]
163["SweepEdge Opposite"]
164["SweepEdge Adjacent"]
168["Sweep Extrusion<br>[4948, 4969, 5]"]
169[Wall]
170["SweepEdge Opposite"]
171["SweepEdge Adjacent"]
172["EdgeCut Fillet<br>[5010, 5521, 5]"]
173["EdgeCut Fillet<br>[5010, 5521, 5]"]
174["EdgeCut Fillet<br>[5010, 5521, 5]"]
175["EdgeCut Fillet<br>[5010, 5521, 5]"]
176["EdgeCut Fillet<br>[5010, 5521, 5]"]
177["EdgeCut Fillet<br>[5010, 5521, 5]"]
178["EdgeCut Fillet<br>[5010, 5521, 5]"]
179["EdgeCut Fillet<br>[5010, 5521, 5]"]
180["Plane<br>[204, 231, 6]"]
184["Sweep Extrusion<br>[333, 353, 6]"]
185[Wall]
186["Cap Start"]
187["Cap End"]
188["SweepEdge Opposite"]
189["SweepEdge Adjacent"]
190["Plane<br>[467, 495, 6]"]
194["Sweep Extrusion<br>[543, 564, 6]"]
195[Wall]
196["Cap Start"]
197["Cap End"]
198["SweepEdge Opposite"]
199["SweepEdge Adjacent"]
200["EdgeCut Fillet<br>[394, 452, 6]"]
201["EdgeCut Fillet<br>[394, 452, 6]"]
202["Plane<br>[193, 210, 7]"]
218["Sweep Revolve<br>[764, 846, 7]"]
219[Wall]
220[Wall]
221[Wall]
222[Wall]
223[Wall]
224[Wall]
225[Wall]
226[Wall]
227[Wall]
228[Wall]
229[Wall]
230[Wall]
231["SweepEdge Adjacent"]
232["SweepEdge Adjacent"]
233["SweepEdge Adjacent"]
234["SweepEdge Adjacent"]
235["SweepEdge Adjacent"]
236["SweepEdge Adjacent"]
237["SweepEdge Adjacent"]
238["SweepEdge Adjacent"]
239["SweepEdge Adjacent"]
240["SweepEdge Adjacent"]
241["SweepEdge Adjacent"]
242["Plane<br>[1053, 1091, 7]"]
250["Plane<br>[1053, 1091, 7]"]
258["Plane<br>[1053, 1091, 7]"]
260["SweepEdge Opposite"]
261["SweepEdge Opposite"]
262["SweepEdge Opposite"]
263["SweepEdge Opposite"]
266["Sweep Loft<br>[2389, 2509, 7]"]
267[Wall]
268[Wall]
269[Wall]
270[Wall]
271["Cap End"]
272["Cap End"]
273["SweepEdge Adjacent"]
274["SweepEdge Adjacent"]
275["SweepEdge Adjacent"]
276["SweepEdge Adjacent"]
277["StartSketchOnFace<br>[1597, 1640, 5]"]
278["StartSketchOnFace<br>[2498, 2541, 5]"]
279["StartSketchOnFace<br>[3197, 3234, 5]"]
280["StartSketchOnFace<br>[3421, 3458, 5]"]
281["StartSketchOnFace<br>[4746, 4789, 5]"]
282["StartSketchOnFace<br>[4861, 4900, 5]"]
283["StartSketchOnPlane<br>[244, 271, 6]"]
284["StartSketchOnPlane<br>[453, 496, 6]"]
285["StartSketchOnPlane<br>[1039, 1092, 7]"]
286["StartSketchOnPlane<br>[1039, 1092, 7]"]
287["StartSketchOnPlane<br>[1039, 1092, 7]"]
1 --- 2
1 --- 9
1 --- 12
1 --- 15
1 --- 18
1 --- 21
2 --- 3
2 --- 4
2 --- 5
2 --- 6
2 --- 7
2 ---- 24
2 --- 8
3 --- 25
3 --- 31
3 --- 32
4 --- 26
4 --- 33
4 --- 34
5 --- 27
5 --- 35
5 --- 36
6 --- 28
6 --- 37
6 --- 38
9 --- 10
9 --- 11
12 --- 13
12 --- 14
15 --- 16
15 --- 17
18 --- 19
18 --- 20
21 --- 22
21 --- 23
24 --- 25
24 --- 26
24 --- 27
24 --- 28
24 --- 29
24 --- 30
24 --- 31
24 --- 32
24 --- 33
24 --- 34
24 --- 35
24 --- 36
24 --- 37
24 --- 38
30 --- 39
30 --- 78
30 --- 157
39 --- 40
39 --- 41
39 --- 42
39 --- 43
39 --- 44
39 --- 45
39 --- 46
39 --- 47
39 --- 48
39 ---- 50
39 --- 49
40 --- 51
40 --- 59
40 --- 60
41 --- 52
41 --- 61
41 --- 62
42 --- 53
42 --- 63
42 --- 64
43 --- 54
43 --- 65
43 --- 66
44 --- 55
44 --- 67
44 --- 68
45 --- 56
45 --- 69
45 --- 70
46 --- 57
46 --- 71
46 --- 72
47 --- 58
47 --- 73
47 --- 74
50 --- 51
50 --- 52
50 --- 53
50 --- 54
50 --- 55
50 --- 56
50 --- 57
50 --- 58
50 --- 59
50 --- 60
50 --- 61
50 --- 62
50 --- 63
50 --- 64
50 --- 65
50 --- 66
50 --- 67
50 --- 68
50 --- 69
50 --- 70
50 --- 71
50 --- 72
50 --- 73
50 --- 74
78 --- 79
78 --- 80
78 --- 81
78 --- 82
78 --- 83
78 --- 84
78 --- 85
78 --- 86
78 ---- 87
79 --- 88
79 --- 97
79 --- 98
80 --- 89
80 --- 99
80 --- 100
81 --- 90
81 --- 101
81 --- 102
82 --- 91
82 --- 103
82 --- 104
83 --- 92
83 --- 105
83 --- 106
84 --- 93
84 --- 107
84 --- 108
85 --- 94
85 --- 109
85 --- 110
86 --- 95
86 --- 111
86 --- 112
87 --- 88
87 --- 89
87 --- 90
87 --- 91
87 --- 92
87 --- 93
87 --- 94
87 --- 95
87 --- 96
87 --- 97
87 --- 98
87 --- 99
87 --- 100
87 --- 101
87 --- 102
87 --- 103
87 --- 104
87 --- 105
87 --- 106
87 --- 107
87 --- 108
87 --- 109
87 --- 110
87 --- 111
87 --- 112
96 --- 113
96 --- 120
96 --- 127
96 --- 130
96 --- 133
96 --- 136
96 --- 139
113 --- 114
113 ---- 116
113 --- 115
114 --- 117
114 --- 118
114 --- 119
116 --- 117
116 --- 118
116 --- 119
120 --- 121
120 --- 122
120 --- 123
120 --- 124
120 --- 125
120 ---- 142
120 --- 126
121 --- 143
121 --- 149
121 --- 150
122 --- 144
122 --- 151
122 --- 152
123 --- 145
123 --- 153
123 --- 154
124 --- 146
124 --- 155
124 --- 156
127 --- 128
127 --- 129
130 --- 131
130 --- 132
133 --- 134
133 --- 135
136 --- 137
136 --- 138
139 --- 140
139 --- 141
142 --- 143
142 --- 144
142 --- 145
142 --- 146
142 --- 147
142 --- 148
142 --- 149
142 --- 150
142 --- 151
142 --- 152
142 --- 153
142 --- 154
142 --- 155
142 --- 156
157 --- 158
157 ---- 160
157 --- 159
158 --- 161
158 --- 163
158 --- 164
160 --- 161
160 --- 162
160 --- 163
160 --- 164
162 --- 165
165 --- 166
165 ---- 168
165 --- 167
166 --- 169
166 --- 170
166 --- 171
168 --- 169
168 --- 170
168 --- 171
32 <--x 172
34 <--x 173
36 <--x 174
38 <--x 175
150 <--x 176
152 <--x 177
154 <--x 178
156 <--x 179
180 --- 181
181 --- 182
181 ---- 184
181 --- 183
182 --- 185
182 --- 188
182 --- 189
182 --- 201
184 --- 185
184 --- 186
184 --- 187
184 --- 188
184 --- 189
190 --- 191
191 --- 192
191 ---- 194
191 --- 193
192 --- 195
192 --- 198
192 --- 199
194 --- 195
194 --- 196
194 --- 197
194 --- 198
194 --- 199
188 <--x 200
202 --- 203
203 --- 204
203 --- 205
203 --- 206
203 --- 207
203 --- 208
203 --- 209
203 --- 210
203 --- 211
203 --- 212
203 --- 213
203 --- 214
203 --- 215
203 --- 216
203 ---- 218
203 --- 217
204 --- 219
204 x--> 231
205 --- 220
205 --- 231
206 --- 221
206 --- 232
207 --- 222
207 --- 233
208 --- 223
208 --- 234
209 --- 224
209 --- 235
210 --- 225
210 --- 236
211 --- 226
211 --- 237
212 --- 227
212 --- 238
213 --- 228
213 --- 239
214 --- 229
214 --- 240
215 --- 230
215 --- 241
218 --- 219
218 --- 220
218 --- 221
218 --- 222
218 --- 223
218 --- 224
218 --- 225
218 --- 226
218 --- 227
218 --- 228
218 --- 229
218 --- 230
218 <--x 204
218 --- 231
218 <--x 205
218 <--x 206
218 --- 232
218 <--x 207
218 --- 233
218 <--x 208
218 --- 234
218 <--x 209
218 --- 235
218 <--x 210
218 --- 236
218 <--x 211
218 --- 237
218 <--x 212
218 --- 238
218 <--x 213
218 --- 239
218 <--x 214
218 --- 240
218 <--x 215
218 --- 241
242 --- 243
243 --- 244
243 --- 245
243 --- 246
243 --- 247
243 --- 248
243 ---- 266
243 --- 249
244 --- 267
244 --- 260
244 --- 273
245 --- 268
245 --- 261
245 --- 274
246 --- 269
246 --- 262
246 --- 275
247 --- 270
247 --- 263
247 --- 276
250 --- 251
251 --- 252
251 --- 253
251 --- 254
251 --- 255
251 --- 256
251 x---> 266
251 --- 257
258 --- 259
259 x--> 260
259 x--> 261
259 x--> 262
259 x--> 263
259 --- 264
259 x---> 266
259 --- 265
266 --- 260
266 --- 261
266 --- 262
266 --- 263
266 --- 267
266 --- 268
266 --- 269
266 --- 270
266 --- 271
266 --- 272
266 --- 273
266 --- 274
266 --- 275
266 --- 276
30 <--x 277
30 <--x 278
96 <--x 279
96 <--x 280
30 <--x 281
162 <--x 282
180 <--x 283
190 <--x 284
242 <--x 285
250 <--x 286
258 <--x 287
```

View File

@ -0,0 +1,220 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Result of parsing axial-fan.kcl
---
{
"Ok": {
"body": [
{
"commentStart": 0,
"end": 0,
"path": {
"type": "Kcl",
"filename": "fan-housing.kcl"
},
"preComments": [
"// Import all parts into assembly file"
],
"selector": {
"type": "None",
"alias": {
"commentStart": 0,
"end": 0,
"name": "fanHousing",
"start": 0,
"type": "Identifier"
}
},
"start": 0,
"type": "ImportStatement",
"type": "ImportStatement"
},
{
"commentStart": 0,
"end": 0,
"path": {
"type": "Kcl",
"filename": "motor.kcl"
},
"selector": {
"type": "None",
"alias": {
"commentStart": 0,
"end": 0,
"name": "motor",
"start": 0,
"type": "Identifier"
}
},
"start": 0,
"type": "ImportStatement",
"type": "ImportStatement"
},
{
"commentStart": 0,
"end": 0,
"path": {
"type": "Kcl",
"filename": "fan.kcl"
},
"selector": {
"type": "None",
"alias": {
"commentStart": 0,
"end": 0,
"name": "fan",
"start": 0,
"type": "Identifier"
}
},
"start": 0,
"type": "ImportStatement",
"type": "ImportStatement"
},
{
"commentStart": 0,
"end": 0,
"expression": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "fanHousing",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name",
"type": "Name"
},
"preComments": [
"",
"",
"// Produce the model for each imported part"
],
"start": 0,
"type": "ExpressionStatement",
"type": "ExpressionStatement"
},
{
"commentStart": 0,
"end": 0,
"expression": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "motor",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name",
"type": "Name"
},
"start": 0,
"type": "ExpressionStatement",
"type": "ExpressionStatement"
},
{
"commentStart": 0,
"end": 0,
"expression": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "fan",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name",
"type": "Name"
},
"start": 0,
"type": "ExpressionStatement",
"type": "ExpressionStatement"
}
],
"commentStart": 0,
"end": 0,
"innerAttrs": [
{
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "settings",
"start": 0,
"type": "Identifier"
},
"preComments": [
"// PC Fan",
"// A small axial fan, used to push or draw airflow over components to remove excess heat",
"",
"",
"// Set units"
],
"properties": [
{
"commentStart": 0,
"end": 0,
"key": {
"commentStart": 0,
"end": 0,
"name": "defaultLengthUnit",
"start": 0,
"type": "Identifier"
},
"start": 0,
"type": "ObjectProperty",
"value": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "mm",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name",
"type": "Name"
}
}
],
"start": 0,
"type": "Annotation"
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"startNodes": [
{
"commentStart": 0,
"end": 0,
"start": 0,
"type": "NonCodeNode",
"value": {
"type": "newLine"
}
}
]
},
"start": 0
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,18 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Variables in memory after executing axial-fan.kcl
---
{
"fan": {
"type": "Module",
"value": 7
},
"fanHousing": {
"type": "Module",
"value": 5
},
"motor": {
"type": "Module",
"value": 6
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

View File

@ -0,0 +1,544 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Artifact commands bottle.kcl
---
[
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "edge_lines_visible",
"hidden": false
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "object_visible",
"object_id": "[uuid]",
"hidden": true
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "object_visible",
"object_id": "[uuid]",
"hidden": true
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "set_scene_units",
"unit": "mm"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "make_plane",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"x_axis": {
"x": 1.0,
"y": 0.0,
"z": 0.0
},
"y_axis": {
"x": 0.0,
"y": 1.0,
"z": 0.0
},
"size": 60.0,
"clobber": false,
"hide": true
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "enable_sketch_mode",
"entity_id": "[uuid]",
"ortho": false,
"animated": false,
"adjust_camera": false,
"planar_normal": {
"x": 0.0,
"y": 0.0,
"z": 1.0
}
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "start_path"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "move_path_pen",
"path": "[uuid]",
"to": {
"x": -62.5,
"y": 0.0,
"z": 0.0
}
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "sketch_mode_disable"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "extend_path",
"path": "[uuid]",
"segment": {
"type": "line",
"end": {
"x": 0.0,
"y": 26.6667,
"z": 0.0
},
"relative": true
}
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "extend_path",
"path": "[uuid]",
"segment": {
"type": "arc_to",
"interior": {
"x": 0.0,
"y": 40.0,
"z": 0.0
},
"end": {
"x": 62.5,
"y": 26.6667,
"z": 0.0
},
"relative": false
}
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "extend_path",
"path": "[uuid]",
"segment": {
"type": "line",
"end": {
"x": 62.5,
"y": 0.0,
"z": 0.0
},
"relative": false
}
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "entity_mirror",
"ids": [
"[uuid]"
],
"axis": {
"x": 1.0,
"y": 0.0,
"z": 0.0
},
"point": {
"x": 0.0,
"y": 0.0,
"z": 0.0
}
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "entity_get_all_child_uuids",
"entity_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "close_path",
"path_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "enable_sketch_mode",
"entity_id": "[uuid]",
"ortho": false,
"animated": false,
"adjust_camera": false,
"planar_normal": {
"x": 0.0,
"y": 0.0,
"z": 1.0
}
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "extrude",
"target": "[uuid]",
"distance": 202.0,
"faces": null,
"opposite": "None"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "sketch_mode_disable"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "object_bring_to_front",
"object_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_extrusion_face_info",
"object_id": "[uuid]",
"edge_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_opposite_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_next_adjacent_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_opposite_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_next_adjacent_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_opposite_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_next_adjacent_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_opposite_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_next_adjacent_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_opposite_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_next_adjacent_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "enable_sketch_mode",
"entity_id": "[uuid]",
"ortho": false,
"animated": false,
"adjust_camera": false,
"planar_normal": null
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "start_path"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "move_path_pen",
"path": "[uuid]",
"to": {
"x": 22.5,
"y": 0.0,
"z": 0.0
}
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "sketch_mode_disable"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "extend_path",
"path": "[uuid]",
"segment": {
"type": "arc",
"center": {
"x": 0.0,
"y": 0.0
},
"radius": 22.5,
"start": {
"unit": "degrees",
"value": 0.0
},
"end": {
"unit": "degrees",
"value": 360.0
},
"relative": false
}
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "close_path",
"path_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "enable_sketch_mode",
"entity_id": "[uuid]",
"ortho": false,
"animated": false,
"adjust_camera": false,
"planar_normal": null
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "extrude",
"target": "[uuid]",
"distance": 18.0,
"faces": null,
"opposite": "None"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "sketch_mode_disable"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "object_bring_to_front",
"object_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_extrusion_face_info",
"object_id": "[uuid]",
"edge_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_opposite_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_next_adjacent_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_shell_face",
"object_id": "[uuid]",
"face_ids": [
"[uuid]"
],
"shell_thickness": 4.0,
"hollow": false
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "object_set_material_params_pbr",
"object_id": "[uuid]",
"color": {
"r": 0.0,
"g": 0.47058824,
"b": 0.7607843,
"a": 100.0
},
"metalness": 0.0,
"roughness": 0.0,
"ambient_occlusion": 0.0
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "set_scene_units",
"unit": "mm"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "set_scene_units",
"unit": "mm"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "set_scene_units",
"unit": "mm"
}
}
]

View File

@ -0,0 +1,6 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Artifact graph flowchart bottle.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,44 @@
```mermaid
flowchart LR
subgraph path2 [Path]
2["Path<br>[337, 378, 0]"]
3["Segment<br>[384, 415, 0]"]
4["Segment<br>[421, 528, 0]"]
5["Segment<br>[534, 556, 0]"]
6["Segment<br>[586, 593, 0]"]
7[Solid2d]
end
subgraph path10 [Path]
10["Path<br>[750, 800, 0]"]
11["Segment<br>[750, 800, 0]"]
12[Solid2d]
end
1["Plane<br>[314, 331, 0]"]
8["Sweep Extrusion<br>[599, 641, 0]"]
9["Plane<br>[750, 800, 0]"]
13["Sweep Extrusion<br>[806, 833, 0]"]
14[Wall]
15["Cap End"]
16["SweepEdge Opposite"]
17["SweepEdge Adjacent"]
18["StartSketchOnFace<br>[707, 744, 0]"]
1 --- 2
2 --- 3
2 --- 4
2 --- 5
2 --- 6
2 ---- 8
2 --- 7
9 --- 10
10 --- 11
10 ---- 13
10 --- 12
11 --- 14
11 --- 16
11 --- 17
13 --- 14
13 --- 15
13 --- 16
13 --- 17
9 <--x 18
```

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,152 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Operations executed bottle.kcl
---
[
{
"labeledArgs": {
"planeOrSolid": {
"value": {
"type": "Plane",
"artifact_id": "[uuid]"
},
"sourceRange": []
}
},
"name": "startSketchOn",
"sourceRange": [],
"type": "StdLibCall",
"unlabeledArg": null
},
{
"labeledArgs": {
"length": {
"value": {
"type": "Number",
"value": 202.0,
"ty": {
"type": "Default",
"len": {
"type": "Mm"
},
"angle": {
"type": "Degrees"
}
}
},
"sourceRange": []
}
},
"name": "extrude",
"sourceRange": [],
"type": "StdLibCall",
"unlabeledArg": {
"value": {
"type": "Sketch",
"value": {
"artifactId": "[uuid]"
}
},
"sourceRange": []
}
},
{
"labeledArgs": {
"face": {
"value": {
"type": "String",
"value": "end"
},
"sourceRange": []
}
},
"name": "startSketchOn",
"sourceRange": [],
"type": "StdLibCall",
"unlabeledArg": {
"value": {
"type": "Solid",
"value": {
"artifactId": "[uuid]"
}
},
"sourceRange": []
}
},
{
"labeledArgs": {
"length": {
"value": {
"type": "Number",
"value": 18.0,
"ty": {
"type": "Default",
"len": {
"type": "Mm"
},
"angle": {
"type": "Degrees"
}
}
},
"sourceRange": []
}
},
"name": "extrude",
"sourceRange": [],
"type": "StdLibCall",
"unlabeledArg": {
"value": {
"type": "Sketch",
"value": {
"artifactId": "[uuid]"
}
},
"sourceRange": []
}
},
{
"labeledArgs": {
"faces": {
"value": {
"type": "Array",
"value": [
{
"type": "String",
"value": "end"
}
]
},
"sourceRange": []
},
"thickness": {
"value": {
"type": "Number",
"value": 4.0,
"ty": {
"type": "Default",
"len": {
"type": "Mm"
},
"angle": {
"type": "Degrees"
}
}
},
"sourceRange": []
}
},
"name": "shell",
"sourceRange": [],
"type": "StdLibCall",
"unlabeledArg": {
"value": {
"type": "Solid",
"value": {
"artifactId": "[uuid]"
}
},
"sourceRange": []
}
}
]

View File

@ -0,0 +1,817 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Variables in memory after executing bottle.kcl
---
{
"bottleBody": {
"type": "Solid",
"value": {
"type": "Solid",
"id": "[uuid]",
"artifactId": "[uuid]",
"value": [],
"sketch": {
"type": "Sketch",
"id": "[uuid]",
"paths": [
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"from": [
-62.5,
0.0
],
"tag": null,
"to": [
-62.5,
26.6667
],
"type": "ToPoint",
"units": {
"type": "Mm"
}
},
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"from": [
-62.5,
26.6667
],
"p1": [
-62.5,
26.666666666666668
],
"p2": [
0.0,
40.0
],
"p3": [
62.5,
26.666666666666668
],
"tag": null,
"to": [
62.5,
26.6667
],
"type": "ArcThreePoint",
"units": {
"type": "Mm"
}
},
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"from": [
62.5,
26.6667
],
"tag": null,
"to": [
62.5,
0.0
],
"type": "ToPoint",
"units": {
"type": "Mm"
}
},
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"from": [
62.5,
0.0
],
"tag": null,
"to": [
-62.5,
0.0
],
"type": "ToPoint",
"units": {
"type": "Mm"
}
}
],
"on": {
"type": "plane",
"id": "[uuid]",
"artifactId": "[uuid]",
"value": "XY",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"units": {
"type": "Mm"
}
},
"xAxis": {
"x": 1.0,
"y": 0.0,
"z": 0.0,
"units": {
"type": "Mm"
}
},
"yAxis": {
"x": 0.0,
"y": 1.0,
"z": 0.0,
"units": {
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": 0.0,
"z": 1.0,
"units": {
"type": "Mm"
}
},
"units": {
"type": "Mm"
}
},
"start": {
"from": [
-62.5,
0.0
],
"to": [
-62.5,
0.0
],
"units": {
"type": "Mm"
},
"tag": null,
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
}
},
"height": 202.0,
"startCapId": "[uuid]",
"endCapId": "[uuid]",
"units": {
"type": "Mm"
}
}
},
"bottleHeight": {
"type": "Number",
"value": 220.0,
"ty": {
"type": "Default",
"len": {
"type": "Mm"
},
"angle": {
"type": "Degrees"
}
}
},
"bottleLength": {
"type": "Number",
"value": 125.0,
"ty": {
"type": "Default",
"len": {
"type": "Mm"
},
"angle": {
"type": "Degrees"
}
}
},
"bottleNeck": {
"type": "Solid",
"value": {
"type": "Solid",
"id": "[uuid]",
"artifactId": "[uuid]",
"value": [
{
"faceId": "[uuid]",
"id": "[uuid]",
"sourceRange": [],
"tag": null,
"type": "extrudeArc"
}
],
"sketch": {
"type": "Sketch",
"id": "[uuid]",
"paths": [
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"ccw": true,
"center": [
0.0,
0.0
],
"from": [
22.5,
0.0
],
"radius": 22.5,
"tag": null,
"to": [
22.5,
0.0
],
"type": "Circle",
"units": {
"type": "Mm"
}
}
],
"on": {
"type": "face",
"id": "[uuid]",
"artifactId": "[uuid]",
"value": "end",
"xAxis": {
"x": 1.0,
"y": 0.0,
"z": 0.0,
"units": {
"type": "Mm"
}
},
"yAxis": {
"x": 0.0,
"y": 1.0,
"z": 0.0,
"units": {
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": 0.0,
"z": 1.0,
"units": {
"type": "Mm"
}
},
"solid": {
"type": "Solid",
"id": "[uuid]",
"artifactId": "[uuid]",
"value": [],
"sketch": {
"type": "Sketch",
"id": "[uuid]",
"paths": [
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"from": [
-62.5,
0.0
],
"tag": null,
"to": [
-62.5,
26.6667
],
"type": "ToPoint",
"units": {
"type": "Mm"
}
},
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"from": [
-62.5,
26.6667
],
"p1": [
-62.5,
26.666666666666668
],
"p2": [
0.0,
40.0
],
"p3": [
62.5,
26.666666666666668
],
"tag": null,
"to": [
62.5,
26.6667
],
"type": "ArcThreePoint",
"units": {
"type": "Mm"
}
},
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"from": [
62.5,
26.6667
],
"tag": null,
"to": [
62.5,
0.0
],
"type": "ToPoint",
"units": {
"type": "Mm"
}
},
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"from": [
62.5,
0.0
],
"tag": null,
"to": [
-62.5,
0.0
],
"type": "ToPoint",
"units": {
"type": "Mm"
}
}
],
"on": {
"type": "plane",
"id": "[uuid]",
"artifactId": "[uuid]",
"value": "XY",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"units": {
"type": "Mm"
}
},
"xAxis": {
"x": 1.0,
"y": 0.0,
"z": 0.0,
"units": {
"type": "Mm"
}
},
"yAxis": {
"x": 0.0,
"y": 1.0,
"z": 0.0,
"units": {
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": 0.0,
"z": 1.0,
"units": {
"type": "Mm"
}
},
"units": {
"type": "Mm"
}
},
"start": {
"from": [
-62.5,
0.0
],
"to": [
-62.5,
0.0
],
"units": {
"type": "Mm"
},
"tag": null,
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
}
},
"height": 202.0,
"startCapId": "[uuid]",
"endCapId": "[uuid]",
"units": {
"type": "Mm"
}
},
"units": {
"type": "Mm"
}
},
"start": {
"from": [
22.5,
0.0
],
"to": [
22.5,
0.0
],
"units": {
"type": "Mm"
},
"tag": null,
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
}
},
"height": 18.0,
"startCapId": null,
"endCapId": "[uuid]",
"units": {
"type": "Mm"
}
}
},
"bottleShell": {
"type": "Solid",
"value": {
"type": "Solid",
"id": "[uuid]",
"artifactId": "[uuid]",
"value": [
{
"faceId": "[uuid]",
"id": "[uuid]",
"sourceRange": [],
"tag": null,
"type": "extrudeArc"
}
],
"sketch": {
"type": "Sketch",
"id": "[uuid]",
"paths": [
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"ccw": true,
"center": [
0.0,
0.0
],
"from": [
22.5,
0.0
],
"radius": 22.5,
"tag": null,
"to": [
22.5,
0.0
],
"type": "Circle",
"units": {
"type": "Mm"
}
}
],
"on": {
"type": "face",
"id": "[uuid]",
"artifactId": "[uuid]",
"value": "end",
"xAxis": {
"x": 1.0,
"y": 0.0,
"z": 0.0,
"units": {
"type": "Mm"
}
},
"yAxis": {
"x": 0.0,
"y": 1.0,
"z": 0.0,
"units": {
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": 0.0,
"z": 1.0,
"units": {
"type": "Mm"
}
},
"solid": {
"type": "Solid",
"id": "[uuid]",
"artifactId": "[uuid]",
"value": [],
"sketch": {
"type": "Sketch",
"id": "[uuid]",
"paths": [
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"from": [
-62.5,
0.0
],
"tag": null,
"to": [
-62.5,
26.6667
],
"type": "ToPoint",
"units": {
"type": "Mm"
}
},
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"from": [
-62.5,
26.6667
],
"p1": [
-62.5,
26.666666666666668
],
"p2": [
0.0,
40.0
],
"p3": [
62.5,
26.666666666666668
],
"tag": null,
"to": [
62.5,
26.6667
],
"type": "ArcThreePoint",
"units": {
"type": "Mm"
}
},
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"from": [
62.5,
26.6667
],
"tag": null,
"to": [
62.5,
0.0
],
"type": "ToPoint",
"units": {
"type": "Mm"
}
},
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"from": [
62.5,
0.0
],
"tag": null,
"to": [
-62.5,
0.0
],
"type": "ToPoint",
"units": {
"type": "Mm"
}
}
],
"on": {
"type": "plane",
"id": "[uuid]",
"artifactId": "[uuid]",
"value": "XY",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"units": {
"type": "Mm"
}
},
"xAxis": {
"x": 1.0,
"y": 0.0,
"z": 0.0,
"units": {
"type": "Mm"
}
},
"yAxis": {
"x": 0.0,
"y": 1.0,
"z": 0.0,
"units": {
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": 0.0,
"z": 1.0,
"units": {
"type": "Mm"
}
},
"units": {
"type": "Mm"
}
},
"start": {
"from": [
-62.5,
0.0
],
"to": [
-62.5,
0.0
],
"units": {
"type": "Mm"
},
"tag": null,
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
}
},
"height": 202.0,
"startCapId": "[uuid]",
"endCapId": "[uuid]",
"units": {
"type": "Mm"
}
},
"units": {
"type": "Mm"
}
},
"start": {
"from": [
22.5,
0.0
],
"to": [
22.5,
0.0
],
"units": {
"type": "Mm"
},
"tag": null,
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
}
},
"height": 18.0,
"startCapId": null,
"endCapId": "[uuid]",
"units": {
"type": "Mm"
}
}
},
"bottleWidth": {
"type": "Number",
"value": 80.0,
"ty": {
"type": "Default",
"len": {
"type": "Mm"
},
"angle": {
"type": "Degrees"
}
}
},
"neckDepth": {
"type": "Number",
"value": 18.0,
"ty": {
"type": "Default",
"len": {
"type": "Mm"
},
"angle": {
"type": "Degrees"
}
}
},
"neckDiameter": {
"type": "Number",
"value": 45.0,
"ty": {
"type": "Default",
"len": {
"type": "Mm"
},
"angle": {
"type": "Degrees"
}
}
},
"wallThickness": {
"type": "Number",
"value": 4.0,
"ty": {
"type": "Default",
"len": {
"type": "Mm"
},
"angle": {
"type": "Degrees"
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View File

@ -23,6 +23,7 @@ pub struct EngineConnection {
batch: Arc<RwLock<Vec<(WebSocketRequest, kcl_lib::SourceRange)>>>,
batch_end: Arc<RwLock<IndexMap<uuid::Uuid, (WebSocketRequest, kcl_lib::SourceRange)>>>,
core_test: Arc<RwLock<String>>,
ids_of_async_commands: Arc<RwLock<IndexMap<Uuid, kcl_lib::SourceRange>>>,
/// The default planes for the scene.
default_planes: Arc<RwLock<Option<DefaultPlanes>>>,
stats: EngineStats,
@ -37,6 +38,7 @@ impl EngineConnection {
batch_end: Arc::new(RwLock::new(IndexMap::new())),
core_test: result,
default_planes: Default::default(),
ids_of_async_commands: Arc::new(RwLock::new(IndexMap::new())),
stats: Default::default(),
})
}
@ -379,6 +381,10 @@ impl kcl_lib::EngineManager for EngineConnection {
Arc::new(RwLock::new(Vec::new()))
}
fn ids_of_async_commands(&self) -> Arc<RwLock<IndexMap<Uuid, kcl_lib::SourceRange>>> {
self.ids_of_async_commands.clone()
}
fn get_default_planes(&self) -> Arc<RwLock<Option<DefaultPlanes>>> {
self.default_planes.clone()
}
@ -391,6 +397,25 @@ impl kcl_lib::EngineManager for EngineConnection {
Ok(())
}
async fn inner_fire_modeling_cmd(
&self,
id: uuid::Uuid,
source_range: kcl_lib::SourceRange,
cmd: WebSocketRequest,
id_to_source_range: HashMap<Uuid, kcl_lib::SourceRange>,
) -> Result<(), KclError> {
// Pop off the id we care about.
self.ids_of_async_commands.write().await.swap_remove(&id);
// Add the response to our responses.
let response = self
.inner_send_modeling_cmd(id, source_range, cmd, id_to_source_range)
.await?;
self.responses().write().await.insert(id, response);
Ok(())
}
async fn inner_send_modeling_cmd(
&self,
id: uuid::Uuid,

View File

@ -9,6 +9,7 @@ use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub struct Context {
engine: Arc<Box<dyn EngineManager>>,
response_context: Arc<kcl_lib::wasm_engine::ResponseContext>,
fs: Arc<FileManager>,
mock_engine: Arc<Box<dyn EngineManager>>,
}
@ -22,9 +23,10 @@ impl Context {
) -> Result<Self, JsValue> {
console_error_panic_hook::set_once();
let response_context = Arc::new(kcl_lib::wasm_engine::ResponseContext::new());
Ok(Self {
engine: Arc::new(Box::new(
kcl_lib::wasm_engine::EngineConnection::new(engine_manager)
kcl_lib::wasm_engine::EngineConnection::new(engine_manager, response_context.clone())
.await
.map_err(|e| format!("{:?}", e))?,
)),
@ -34,6 +36,7 @@ impl Context {
.await
.map_err(|e| format!("{:?}", e))?,
)),
response_context,
})
}
@ -100,6 +103,12 @@ impl Context {
}
}
/// Send a response to kcl lib's engine.
#[wasm_bindgen(js_name = sendResponse)]
pub async fn send_response(&self, data: js_sys::Uint8Array) -> Result<(), JsValue> {
self.response_context.send_response(data).await
}
/// Execute a program in mock mode.
#[wasm_bindgen(js_name = executeMock)]
pub async fn execute_mock(

View File

@ -669,7 +669,10 @@ export class SceneEntities {
variableDeclarationName: string
}> {
const prepared = this.prepareTruncatedAst(sketchNodePaths, maybeModdedAst)
if (err(prepared)) return Promise.reject(prepared)
if (err(prepared)) {
this.tearDownSketch({ removeAxis: false })
return Promise.reject(prepared)
}
const { truncatedAst, variableDeclarationName } = prepared
const { execState } = await executeAstMock({
@ -695,6 +698,8 @@ export class SceneEntities {
const scale = this.sceneInfra.getClientSceneScaleFactor(dummy)
const callbacks: (() => SegmentOverlayPayload | null)[] = []
this.sceneInfra.pauseRendering()
this.tearDownSketch({ removeAxis: false })
for (const sketchInfo of sketchesInfo) {
const { sketch } = sketchInfo
@ -766,7 +771,11 @@ export class SceneEntities {
segPathToNode,
['CallExpression', 'CallExpressionKw']
)
if (err(_node1)) return
if (err(_node1)) {
this.tearDownSketch({ removeAxis: false })
this.sceneInfra.resumeRendering()
return
}
const callExpName = _node1.node?.callee?.name.name
const initSegment =
@ -862,6 +871,9 @@ export class SceneEntities {
)
position && this.intersectionPlane.position.set(...position)
this.sceneInfra.scene.add(group)
this.sceneInfra.resumeRendering()
this.sceneInfra.camControls.enableRotate = false
this.sceneInfra.overlayCallbacks(callbacks)
@ -882,7 +894,6 @@ export class SceneEntities {
) => {
if (trap(modifiedAst)) return Promise.reject(modifiedAst)
const nextAst = await this.kclManager.updateAst(modifiedAst, false)
this.tearDownSketch({ removeAxis: false })
this.sceneInfra.resetMouseListeners()
await this.setupSketch({
sketchEntryNodePath,
@ -956,7 +967,6 @@ export class SceneEntities {
const draftExpressionsIndices = { start: index, end: index }
if (shouldTearDown) this.tearDownSketch({ removeAxis: false })
this.sceneInfra.resetMouseListeners()
const { truncatedAst } = await this.setupSketch({
@ -967,6 +977,8 @@ export class SceneEntities {
position: origin,
maybeModdedAst: modifiedAst,
draftExpressionsIndices,
}).catch(() => {
return { truncatedAst: modifiedAst }
})
this.sceneInfra.setCallbacks({
onClick: async (args) => {
@ -1824,7 +1836,6 @@ export class SceneEntities {
const index = sg.paths.length // because we've added a new segment that's not in the memory yet
const draftExpressionsIndices = { start: index, end: index }
this.tearDownSketch({ removeAxis: false })
this.sceneInfra.resetMouseListeners()
const { truncatedAst } = await this.setupSketch({
@ -2055,7 +2066,6 @@ export class SceneEntities {
// Get the insertion index from the modified path
const insertIndex = Number(mod.pathToNode[1][0])
this.tearDownSketch({ removeAxis: false })
this.sceneInfra.resetMouseListeners()
const { truncatedAst } = await this.setupSketch({
@ -2477,7 +2487,6 @@ export class SceneEntities {
this.sceneInfra.setCallbacks({
onDragEnd: async () => {
if (addingNewSegmentStatus !== 'nothing') {
this.tearDownSketch({ removeAxis: false })
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.setupSketch({
sketchEntryNodePath,
@ -2552,7 +2561,6 @@ export class SceneEntities {
mod.modifiedAst
)
if (err(didReParse)) return
this.tearDownSketch({ removeAxis: false })
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.setupSketch({
sketchEntryNodePath: pathToNode,
@ -2664,8 +2672,8 @@ export class SceneEntities {
if (prev && ARC_SEGMENT_TYPES.includes(prev.userData.type)) {
const snapDirection = findTangentDirection(prev)
if (snapDirection) {
const SNAP_TOLERANCE_PIXELS = 12 * window.devicePixelRatio
const SNAP_MIN_DISTANCE_PIXELS = 5 * window.devicePixelRatio
const SNAP_TOLERANCE_PIXELS = 8 * window.devicePixelRatio
const SNAP_MIN_DISTANCE_PIXELS = 10 * window.devicePixelRatio
const orthoFactor = orthoScale(this.sceneInfra.camControls.camera)
// See if snapDirection intersects with any of the axes

View File

@ -264,6 +264,8 @@ export class SceneInfra {
hasBeenDragged: boolean
} | null = null
mouseDownVector: null | Vector2 = null
private isRenderingPaused = false
private lastFrameTime = 0
constructor(engineCommandManager: EngineCommandManager) {
// SCENE
@ -348,8 +350,20 @@ export class SceneInfra {
TWEEN.update() // This will update all tweens during the animation loop
if (!this.isFovAnimationInProgress) {
this.camControls.update()
this.renderer.render(this.scene, this.camControls.camera)
this.labelRenderer.render(this.scene, this.camControls.camera)
// If rendering is paused, only render if enough time has passed to maintain smooth animation
if (this.isRenderingPaused) {
const currentTime = performance.now()
if (currentTime - this.lastFrameTime > 1000 / 30) {
// Limit to 30fps while paused
this.renderer.render(this.scene, this.camControls.camera)
this.labelRenderer.render(this.scene, this.camControls.camera)
this.lastFrameTime = currentTime
}
} else {
this.renderer.render(this.scene, this.camControls.camera)
this.labelRenderer.render(this.scene, this.camControls.camera)
}
}
}
@ -685,6 +699,15 @@ export class SceneInfra {
const scale = this.getClientSceneScaleFactor(dummy)
return getLength(a, b) / scale
}
pauseRendering() {
this.isRenderingPaused = true
// Store the current time to prevent unnecessary updates
this.lastFrameTime = performance.now()
}
resumeRendering() {
this.isRenderingPaused = false
}
}
function baseUnitTomm(baseUnit: BaseUnit) {

View File

@ -221,7 +221,7 @@ class StraightSegment implements SegmentUtils {
const snapLine = createLine({
from: [0, 0],
to: [0, 0],
color: 0xcccccc,
color: 0x555555,
})
snapLine.name = STRAIGHT_SEGMENT_SNAP_LINE
segmentGroup.add(snapLine)
@ -294,6 +294,8 @@ class StraightSegment implements SegmentUtils {
if (snapLine) {
snapLine.visible = !!input.snap
if (snapLine.visible) {
// Without this three.js incorrectly culls the line in some cases when zoomed in too much
snapLine.frustumCulled = false
const snapLineFrom = to
const snapLineTo = new Vector3(to[0], to[1], 0).addScaledVector(
dir,

View File

@ -117,7 +117,6 @@ export default function CommandBarSelectionMixedInput({
Continue without selection
</button>
)}
<span data-testid="cmd-bar-arg-name" className="sr-only">
{arg.name}
</span>

View File

@ -3,6 +3,7 @@ import { useEffect, useState } from 'react'
import type { CommandLog } from '@src/lang/std/commandLog'
import { engineCommandManager } from '@src/lib/singletons'
import { reportRejection } from '@src/lib/trap'
import { parseJson } from '@src/lib/utils'
export function useEngineCommands(): [CommandLog[], () => void] {
const [engineCommands, setEngineCommands] = useState<CommandLog[]>(
@ -84,7 +85,7 @@ export const EngineCommands = () => {
data-testid="custom-cmd-send-button"
onClick={() => {
engineCommandManager
.sendSceneCommand(JSON.parse(customCmd))
.sendSceneCommand(parseJson(customCmd))
.catch(reportRejection)
}}
>

View File

@ -5,10 +5,7 @@ import { useModelingContext } from '@src/hooks/useModelingContext'
import { useNetworkContext } from '@src/hooks/useNetworkContext'
import { NetworkHealthState } from '@src/hooks/useNetworkStatus'
import { getArtifactOfTypes } from '@src/lang/std/artifactGraph'
import {
EngineCommandManagerEvents,
EngineConnectionStateType,
} from '@src/lang/std/engineConnection'
import { EngineCommandManagerEvents } from '@src/lang/std/engineConnection'
import { btnName } from '@src/lib/cameraControls'
import { PATHS } from '@src/lib/paths'
import { sendSelectEventToEngine } from '@src/lib/selections'
@ -430,8 +427,9 @@ export const EngineStream = (props: {
}
menuTargetElement={videoWrapperRef}
/>
{engineCommandManager.engineConnection?.state.type !==
EngineConnectionStateType.ConnectionEstablished && (
{![EngineStreamState.Playing, EngineStreamState.Paused].some(
(s) => s === engineStreamState.value
) && (
<Loading dataTestId="loading-engine" className="fixed inset-0">
Connecting to engine
</Loading>

View File

@ -330,6 +330,13 @@ export const ModelingMachineProvider = ({
}
if (setSelections.selectionType === 'singleCodeCursor') {
if (!setSelections.selection && editorManager.isShiftDown) {
// if the user is holding shift, but they didn't select anything
// don't nuke their other selections (frustrating to have one bad click ruin your
// whole selection)
selections = {
graphSelections: selectionRanges.graphSelections,
otherSelections: selectionRanges.otherSelections,
}
} else if (
!setSelections.selection &&
!editorManager.isShiftDown
@ -1487,7 +1494,6 @@ export const ModelingMachineProvider = ({
async ({ input: { sketchDetails, data } }) => {
if (!sketchDetails || !data)
return reject('No sketch details or data')
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
const result = await sceneEntitiesManager.setupDraftCircle(
sketchDetails.sketchEntryNodePath,
@ -1508,7 +1514,6 @@ export const ModelingMachineProvider = ({
async ({ input: { sketchDetails, data } }) => {
if (!sketchDetails || !data)
return reject('No sketch details or data')
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
const result =
await sceneEntitiesManager.setupDraftCircleThreePoint(
@ -1531,7 +1536,6 @@ export const ModelingMachineProvider = ({
async ({ input: { sketchDetails, data } }) => {
if (!sketchDetails || !data)
return reject('No sketch details or data')
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
const result = await sceneEntitiesManager.setupDraftRectangle(
sketchDetails.sketchEntryNodePath,
@ -1552,7 +1556,6 @@ export const ModelingMachineProvider = ({
async ({ input: { sketchDetails, data } }) => {
if (!sketchDetails || !data)
return reject('No sketch details or data')
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
const result = await sceneEntitiesManager.setupDraftCenterRectangle(
sketchDetails.sketchEntryNodePath,
sketchDetails.sketchNodePaths,
@ -1572,7 +1575,6 @@ export const ModelingMachineProvider = ({
async ({ input: { sketchDetails, data } }) => {
if (!sketchDetails || !data)
return reject('No sketch details or data')
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
const result = await sceneEntitiesManager.setupDraftArcThreePoint(
sketchDetails.sketchEntryNodePath,
sketchDetails.sketchNodePaths,
@ -1592,7 +1594,6 @@ export const ModelingMachineProvider = ({
async ({ input: { sketchDetails, data } }) => {
if (!sketchDetails || !data)
return reject('No sketch details or data')
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
const result = await sceneEntitiesManager.setupDraftArc(
sketchDetails.sketchEntryNodePath,
sketchDetails.sketchNodePaths,
@ -1612,9 +1613,6 @@ export const ModelingMachineProvider = ({
async ({ input: { sketchDetails, selectionRanges } }) => {
if (!sketchDetails) return
if (!sketchDetails.sketchEntryNodePath?.length) return
if (Object.keys(sceneEntitiesManager.activeSegments).length > 0) {
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
}
sceneInfra.resetMouseListeners()
await sceneEntitiesManager.setupSketch({
sketchEntryNodePath: sketchDetails?.sketchEntryNodePath || [],

View File

@ -355,6 +355,38 @@ const OperationItem = (props: {
}
}
function enterTranslateFlow() {
if (
props.item.type === 'StdLibCall' ||
props.item.type === 'KclStdLibCall' ||
props.item.type === 'GroupBegin'
) {
props.send({
type: 'enterTranslateFlow',
data: {
targetSourceRange: sourceRangeFromRust(props.item.sourceRange),
currentOperation: props.item,
},
})
}
}
function enterRotateFlow() {
if (
props.item.type === 'StdLibCall' ||
props.item.type === 'KclStdLibCall' ||
props.item.type === 'GroupBegin'
) {
props.send({
type: 'enterRotateFlow',
data: {
targetSourceRange: sourceRangeFromRust(props.item.sourceRange),
currentOperation: props.item,
},
})
}
}
function deleteOperation() {
if (
props.item.type === 'StdLibCall' ||
@ -418,13 +450,6 @@ const OperationItem = (props: {
...(props.item.type === 'StdLibCall' ||
props.item.type === 'KclStdLibCall'
? [
<ContextMenuItem
disabled={!stdLibMap[props.item.name]?.supportsAppearance}
onClick={enterAppearanceFlow}
data-testid="context-menu-set-appearance"
>
Set appearance
</ContextMenuItem>,
<ContextMenuItem
disabled={!stdLibMap[props.item.name]?.prepareToEdit}
onClick={enterEditFlow}
@ -432,15 +457,48 @@ const OperationItem = (props: {
>
Edit
</ContextMenuItem>,
<ContextMenuItem
disabled={!stdLibMap[props.item.name]?.supportsAppearance}
onClick={enterAppearanceFlow}
data-testid="context-menu-set-appearance"
>
Set appearance
</ContextMenuItem>,
]
: []),
...(props.item.type === 'StdLibCall' ||
props.item.type === 'KclStdLibCall' ||
props.item.type === 'GroupBegin'
? [
<ContextMenuItem
onClick={enterTranslateFlow}
data-testid="context-menu-set-translate"
disabled={
props.item.type !== 'GroupBegin' &&
!stdLibMap[props.item.name]?.supportsTransform
}
>
Set translate
</ContextMenuItem>,
<ContextMenuItem
onClick={enterRotateFlow}
data-testid="context-menu-set-rotate"
disabled={
props.item.type !== 'GroupBegin' &&
!stdLibMap[props.item.name]?.supportsTransform
}
>
Set rotate
</ContextMenuItem>,
<ContextMenuItem
onClick={deleteOperation}
hotkey="Delete"
data-testid="context-menu-delete"
>
Delete
</ContextMenuItem>,
]
: []),
<ContextMenuItem
onClick={deleteOperation}
hotkey="Delete"
data-testid="context-menu-delete"
>
Delete
</ContextMenuItem>,
],
[props.item, props.send]
)

View File

@ -0,0 +1,49 @@
import toast from 'react-hot-toast'
import { openExternalBrowserIfDesktop } from '@src/lib/openWindow'
export function ToastUnsupportedSelection({
toastId,
}: {
toastId: string
}) {
const githubIssueUrl = 'https://github.com/KittyCAD/modeling-app/issues/6368'
return (
<div className="inset-0 z-50 grid place-content-center rounded bg-chalkboard-10 dark:bg-chalkboard-90 shadow-md p-3">
<section>
<p className="text-sm text-chalkboard-70 dark:text-chalkboard-30">
Some faces and edges are not currently selectable.{' '}
<a
href={githubIssueUrl}
onClick={openExternalBrowserIfDesktop(githubIssueUrl)}
className="underline"
>
The team is working on it
</a>
.
</p>
</section>
</div>
)
}
/**
* Show a toast notification for when users try to select unsupported faces/edges
* @example
* // In your component or handler:
* import { showUnsupportedSelectionToast } from '@src/components/ToastUnsupportedSelection'
*
* // When user tries to select an unsupported face/edge
* showUnsupportedSelectionToast()
*/
export function showUnsupportedSelectionToast() {
const toastId = toast.custom(
(t) => <ToastUnsupportedSelection toastId={t.id} />,
{
duration: 4_000,
}
)
return toastId
}

View File

@ -37,7 +37,7 @@ export class KCLError extends Error {
filenames: { [x: number]: ModulePath | undefined },
defaultPlanes: DefaultPlanes | null
) {
super()
super(`${kind}: ${msg}`)
this.kind = kind
this.msg = msg
this.sourceRange = sourceRange

View File

@ -4,6 +4,7 @@ import path from 'node:path'
import { assertParse } from '@src/lang/wasm'
import { initPromise } from '@src/lang/wasmUtils'
import { enginelessExecutor } from '@src/lib/testHelpers'
import { parseJson } from '@src/lib/utils'
// The purpose of these tests is to act as a first line of defense
// if something gets real screwy with our KCL ecosystem.
@ -27,7 +28,7 @@ const manifestJsonStr = await fs.readFile(
path.resolve(DIR_KCL_SAMPLES, 'manifest.json'),
'utf-8'
)
const manifest = JSON.parse(manifestJsonStr)
const manifest: KclSampleFile[] = parseJson(manifestJsonStr)
process.chdir(DIR_KCL_SAMPLES)

View File

@ -54,23 +54,18 @@ export async function updateModelingState(
},
options?: {
focusPath?: Array<PathToNode>
skipUpdateAst?: boolean
}
): Promise<void> {
let updatedAst: {
newAst: Node<Program>
selections?: Selections
} = { newAst: ast }
// TODO: understand why this skip flag is needed for insertAstMod.
// It's unclear why we double casts the AST
if (!options?.skipUpdateAst) {
// Step 1: Update AST without executing (prepare selections)
updatedAst = await dependencies.kclManager.updateAst(
ast,
false, // Execution handled separately for error resilience
options
)
}
// Step 1: Update AST without executing (prepare selections)
updatedAst = await dependencies.kclManager.updateAst(
ast,
false, // Execution handled separately for error resilience
options
)
// Step 2: Update the code editor and save file
await dependencies.codeManager.updateEditorWithAstAndWriteToFile(

View File

@ -81,7 +81,10 @@ import type {
VariableMap,
} from '@src/lang/wasm'
import { isPathToNodeNumber, parse } from '@src/lang/wasm'
import type { KclExpressionWithVariable } from '@src/lib/commandTypes'
import type {
KclCommandValue,
KclExpressionWithVariable,
} from '@src/lib/commandTypes'
import { KCL_DEFAULT_CONSTANT_PREFIXES } from '@src/lib/constants'
import type { DefaultPlaneStr } from '@src/lib/planes'
import type { Selection } from '@src/lib/selections'
@ -227,6 +230,8 @@ export function addSketchTo(
Set the keyword argument to the given value.
Returns true if it overwrote an existing argument.
Returns false if no argument with the label existed before.
Returns 'no-mutate' if the label was found, but the value is constrained and not
mutated.
Also do some checks to see if it's actually trying to set a constraint on
a sketch line that's already fully constrained, and if so, duplicates the arg.
WILL BE FIXED SOON.
@ -235,7 +240,7 @@ export function mutateKwArg(
label: string,
node: CallExpressionKw,
val: Expr
): boolean {
): boolean | 'no-mutate' {
for (let i = 0; i < node.arguments.length; i++) {
const arg = node.arguments[i]
if (arg.label.name === label) {
@ -254,6 +259,8 @@ export function mutateKwArg(
})
return true
}
// The label was found, but the value is not a literal or static.
return 'no-mutate'
}
}
node.arguments.push(createLabeledArg(label, val))
@ -1824,3 +1831,20 @@ export function createNodeFromExprSnippet(
if (!node) return new Error('No node found')
return node
}
export function insertVariableAndOffsetPathToNode(
variable: KclCommandValue,
modifiedAst: Node<Program>,
pathToNode: PathToNode
) {
if ('variableName' in variable && variable.variableName) {
modifiedAst.body.splice(
variable.insertIndex,
0,
variable.variableDeclarationAst
)
if (typeof pathToNode[1][0] === 'number') {
pathToNode[1][0]++
}
}
}

View File

@ -0,0 +1,142 @@
import type { Node } from '@rust/kcl-lib/bindings/Node'
import {
createCallExpressionStdLibKw,
createLabeledArg,
createPipeExpression,
} from '@src/lang/create'
import { getNodeFromPath } from '@src/lang/queryAst'
import type {
CallExpressionKw,
Expr,
ExpressionStatement,
PathToNode,
PipeExpression,
Program,
VariableDeclarator,
} from '@src/lang/wasm'
import { err } from '@src/lib/trap'
export function setTranslate({
modifiedAst,
pathToNode,
x,
y,
z,
}: {
modifiedAst: Node<Program>
pathToNode: PathToNode
x: Expr
y: Expr
z: Expr
}): Error | { modifiedAst: Node<Program>; pathToNode: PathToNode } {
const noPercentSign = null
const call = createCallExpressionStdLibKw('translate', noPercentSign, [
createLabeledArg('x', x),
createLabeledArg('y', y),
createLabeledArg('z', z),
])
const potentialPipe = getNodeFromPath<PipeExpression>(
modifiedAst,
pathToNode,
['PipeExpression']
)
if (!err(potentialPipe) && potentialPipe.node.type === 'PipeExpression') {
setTransformInPipe(potentialPipe.node, call)
} else {
const error = createPipeWithTransform(modifiedAst, pathToNode, call)
if (err(error)) {
return error
}
}
return {
modifiedAst,
pathToNode, // TODO: check if this should be updated
}
}
export function setRotate({
modifiedAst,
pathToNode,
roll,
pitch,
yaw,
}: {
modifiedAst: Node<Program>
pathToNode: PathToNode
roll: Expr
pitch: Expr
yaw: Expr
}): Error | { modifiedAst: Node<Program>; pathToNode: PathToNode } {
const noPercentSign = null
const call = createCallExpressionStdLibKw('rotate', noPercentSign, [
createLabeledArg('roll', roll),
createLabeledArg('pitch', pitch),
createLabeledArg('yaw', yaw),
])
const potentialPipe = getNodeFromPath<PipeExpression>(
modifiedAst,
pathToNode,
['PipeExpression']
)
if (!err(potentialPipe) && potentialPipe.node.type === 'PipeExpression') {
setTransformInPipe(potentialPipe.node, call)
} else {
const error = createPipeWithTransform(modifiedAst, pathToNode, call)
if (err(error)) {
return error
}
}
return {
modifiedAst,
pathToNode, // TODO: check if this should be updated
}
}
function setTransformInPipe(
expression: PipeExpression,
call: Node<CallExpressionKw>
) {
const existingIndex = expression.body.findIndex(
(v) =>
v.type === 'CallExpressionKw' &&
v.callee.type === 'Name' &&
v.callee.name.name === call.callee.name.name
)
if (existingIndex > -1) {
expression.body[existingIndex] = call
} else {
expression.body.push(call)
}
}
function createPipeWithTransform(
modifiedAst: Node<Program>,
pathToNode: PathToNode,
call: Node<CallExpressionKw>
) {
const existingCall = getNodeFromPath<
VariableDeclarator | ExpressionStatement
>(modifiedAst, pathToNode, ['VariableDeclarator', 'ExpressionStatement'])
if (err(existingCall)) {
return new Error('Unsupported operation type.')
}
if (existingCall.node.type === 'ExpressionStatement') {
existingCall.node.expression = createPipeExpression([
existingCall.node.expression,
call,
])
} else if (existingCall.node.type === 'VariableDeclarator') {
existingCall.node.init = createPipeExpression([
existingCall.node.init,
call,
])
} else {
return new Error('Unsupported operation type.')
}
}

View File

@ -51,6 +51,7 @@ import { Reason, err } from '@src/lib/trap'
import { getAngle, isArray } from '@src/lib/utils'
import { ARG_INDEX_FIELD, LABELED_ARG_FIELD } from '@src/lang/queryAstConstants'
import type { KclCommandValue } from '@src/lib/commandTypes'
/**
* Retrieves a node from a given path within a Program node structure, optionally stopping at a specified node type.
@ -1052,3 +1053,9 @@ export function updatePathToNodesAfterEdit(
newPath[1][0] = newIndex // Update the body index
return newPath
}
export const valueOrVariable = (variable: KclCommandValue) => {
return 'variableName' in variable
? variable.variableIdentifierAst
: variable.valueAst
}

View File

@ -23,7 +23,7 @@ import {
getThemeColorForEngine,
} from '@src/lib/theme'
import { reportRejection } from '@src/lib/trap'
import { binaryToUuid, uuidv4 } from '@src/lib/utils'
import { binaryToUuid, parseJson, uuidv4 } from '@src/lib/utils'
const pingIntervalMs = 1_000
@ -390,7 +390,7 @@ class EngineConnection extends EventTarget {
this.websocket.addEventListener('open', this.onWebSocketOpen)
this.websocket?.addEventListener('message', ((event: MessageEvent) => {
const message: Models['WebSocketResponse_type'] = JSON.parse(event.data)
const message: Models['WebSocketResponse_type'] = parseJson(event.data)
const pending =
this.engineCommandManager.pendingCommands[message.request_id || '']
if (!('resp' in message)) return
@ -862,7 +862,7 @@ class EngineConnection extends EventTarget {
)
this.onDataChannelMessage = (event) => {
const result: UnreliableResponses = JSON.parse(event.data)
const result: UnreliableResponses = parseJson(event.data)
Object.values(
this.engineCommandManager.unreliableSubscriptions[result.type] ||
{}
@ -976,7 +976,7 @@ class EngineConnection extends EventTarget {
return
}
const message: Models['WebSocketResponse_type'] = JSON.parse(
const message: Models['WebSocketResponse_type'] = parseJson(
event.data
)
@ -1564,7 +1564,7 @@ export class EngineCommandManager extends EventTarget {
unreliableDataChannel.addEventListener(
'message',
(event: MessageEvent) => {
const result: UnreliableResponses = JSON.parse(event.data)
const result: UnreliableResponses = parseJson(event.data)
Object.values(
this.unreliableSubscriptions[result.type] || {}
).forEach(
@ -1608,7 +1608,7 @@ export class EngineCommandManager extends EventTarget {
message.request_id = binaryToUuid(message.request_id)
}
} else {
message = JSON.parse(event.data)
message = parseJson(event.data)
}
if (message === null) {
@ -1617,6 +1617,12 @@ export class EngineCommandManager extends EventTarget {
return
}
// In either case we want to send the response back over the wire to
// the rust side.
this.rustContext?.sendResponse(message).catch((err) => {
console.error('Error sending response to rust', err)
})
const pending = this.pendingCommands[message.request_id || '']
if (pending && !message.success) {
@ -1931,6 +1937,46 @@ export class EngineCommandManager extends EventTarget {
return e
})
}
/**
* A wrapper around the sendCommand where all inputs are JSON strings
*
* This one does not wait for a response.
*/
fireModelingCommandFromWasm(
id: string,
rangeStr: string,
commandStr: string,
idToRangeStr: string
): void | Error {
if (this.engineConnection === undefined)
return new Error('engineConnection is undefined')
if (
!this.engineConnection?.isReady() &&
!this.engineConnection.isUsingConnectionLite
)
return new Error('engineConnection is not ready')
if (id === undefined) return new Error('id is undefined')
if (rangeStr === undefined) return new Error('rangeStr is undefined')
if (commandStr === undefined) return new Error('commandStr is undefined')
const range: SourceRange = JSON.parse(rangeStr)
const command: EngineCommand = JSON.parse(commandStr)
const idToRangeMap: { [key: string]: SourceRange } =
JSON.parse(idToRangeStr)
// Current executeAst is stale, going to interrupt, a new executeAst will trigger
// Used in conjunction with rejectAllModelingCommands
if (this?.kclManager?.executeIsStale) {
return new Error(EXECUTE_AST_INTERRUPT_ERROR_MESSAGE)
}
// We purposely don't wait for a response here
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.sendCommand(id, {
command,
range,
idToRangeMap,
})
}
/**
* A wrapper around the sendCommand where all inputs are JSON strings
*/

View File

@ -40,7 +40,7 @@ import type { CoreDumpManager } from '@src/lib/coredump'
import openWindow from '@src/lib/openWindow'
import { Reason, err } from '@src/lib/trap'
import type { DeepPartial } from '@src/lib/types'
import { isArray } from '@src/lib/utils'
import { isArray, parseJson } from '@src/lib/utils'
import {
base64_decode,
change_kcl_settings,
@ -217,9 +217,7 @@ export const parse = (code: string | Error): ParseResult | Error => {
let errs = splitErrors(parsed[1])
return new ParseResult(parsed[0], errs.errors, errs.warnings)
} catch (e: any) {
// throw e
console.error(e.toString())
const parsed: RustKclError = JSON.parse(e.toString())
const parsed: RustKclError = parseJson(e.toString())
return new KCLError(
parsed.kind,
parsed.msg,
@ -381,7 +379,7 @@ export function sketchFromKclValue(
}
export const errFromErrWithOutputs = (e: any): KCLError => {
const parsed: KclErrorWithOutputs = JSON.parse(e.toString())
const parsed: KclErrorWithOutputs = parseJson(e.toString())
return new KCLError(
parsed.error.kind,
parsed.error.msg,

View File

@ -22,7 +22,11 @@ import type {
KclCommandValue,
StateMachineCommandSetConfig,
} from '@src/lib/commandTypes'
import { KCL_DEFAULT_DEGREE, KCL_DEFAULT_LENGTH } from '@src/lib/constants'
import {
KCL_DEFAULT_DEGREE,
KCL_DEFAULT_LENGTH,
KCL_DEFAULT_TRANSFORM,
} from '@src/lib/constants'
import type { components } from '@src/lib/machine-api'
import type { Selections } from '@src/lib/selections'
import { codeManager, kclManager } from '@src/lib/singletons'
@ -163,6 +167,20 @@ export type ModelingCommandSchema = {
nodeToEdit?: PathToNode
color: string
}
Translate: {
nodeToEdit?: PathToNode
selection: Selections
x: KclCommandValue
y: KclCommandValue
z: KclCommandValue
}
Rotate: {
nodeToEdit?: PathToNode
selection: Selections
roll: KclCommandValue
pitch: KclCommandValue
yaw: KclCommandValue
}
'Boolean Subtract': {
target: Selections
tool: Selections
@ -1024,6 +1042,88 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
// Add more fields
},
},
Translate: {
description: 'Set translation on solid or sketch.',
icon: 'dimension', // TODO: likely not the best icon
needsReview: true,
hide: DEV || IS_NIGHTLY_OR_DEBUG ? undefined : 'both',
args: {
nodeToEdit: {
description:
'Path to the node in the AST to edit. Never shown to the user.',
skip: true,
inputType: 'text',
required: false,
hidden: true,
},
selection: {
// selectionMixed allows for feature tree selection of module imports
inputType: 'selectionMixed',
multiple: false,
required: true,
skip: true,
selectionTypes: ['path'],
selectionFilter: ['object'],
hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit),
},
x: {
inputType: 'kcl',
defaultValue: KCL_DEFAULT_TRANSFORM,
required: true,
},
y: {
inputType: 'kcl',
defaultValue: KCL_DEFAULT_TRANSFORM,
required: true,
},
z: {
inputType: 'kcl',
defaultValue: KCL_DEFAULT_TRANSFORM,
required: true,
},
},
},
Rotate: {
description: 'Set rotation on solid or sketch.',
icon: 'angle', // TODO: likely not the best icon
needsReview: true,
hide: DEV || IS_NIGHTLY_OR_DEBUG ? undefined : 'both',
args: {
nodeToEdit: {
description:
'Path to the node in the AST to edit. Never shown to the user.',
skip: true,
inputType: 'text',
required: false,
hidden: true,
},
selection: {
// selectionMixed allows for feature tree selection of module imports
inputType: 'selectionMixed',
multiple: false,
required: true,
skip: true,
selectionTypes: ['path'],
selectionFilter: ['object'],
hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit),
},
roll: {
inputType: 'kcl',
defaultValue: KCL_DEFAULT_TRANSFORM,
required: true,
},
pitch: {
inputType: 'kcl',
defaultValue: KCL_DEFAULT_TRANSFORM,
required: true,
},
yaw: {
inputType: 'kcl',
defaultValue: KCL_DEFAULT_TRANSFORM,
required: true,
},
},
},
}
modelingMachineCommandConfig

View File

@ -3,7 +3,7 @@ import type { ApiError_type } from '@kittycad/lib/dist/types/src/models'
import type { Selections } from '@src/lib/selections'
import { engineCommandManager, kclManager } from '@src/lib/singletons'
import { uuidv4 } from '@src/lib/utils'
import { parseJson, uuidv4 } from '@src/lib/utils'
import type { CommandBarContext } from '@src/machines/commandBarMachine'
export const disableDryRunWithRetry = async (numberOfRetries = 3) => {
@ -54,7 +54,7 @@ export function parseEngineErrorMessage(engineError: string) {
return undefined
}
const errors = JSON.parse(parts[1]) as ApiError_type[]
const errors = parseJson<ApiError_type[]>(parts[1])
if (!errors[0]) {
return undefined
}

View File

@ -55,6 +55,9 @@ export const KCL_DEFAULT_CONSTANT_PREFIXES = {
/** The default KCL length expression */
export const KCL_DEFAULT_LENGTH = `5`
/** The default KCL transform arg value that means no transform */
export const KCL_DEFAULT_TRANSFORM = `0`
/** The default KCL degree expression */
export const KCL_DEFAULT_DEGREE = `360`

View File

@ -153,7 +153,6 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] {
EXECUTION_TYPE_REAL,
{ kclManager, editorManager, codeManager },
{
skipUpdateAst: true,
focusPath: [pathToImportNode, pathToInsertNode],
}
).catch(reportRejection)

View File

@ -1,6 +1,6 @@
import { executeAstMock } from '@src/lang/langHelpers'
import { parse, resultIsOk } from '@src/lang/wasm'
import type { KclExpression } from '@src/lib/commandTypes'
import { type CallExpressionKw, parse, resultIsOk } from '@src/lang/wasm'
import type { KclCommandValue, KclExpression } from '@src/lib/commandTypes'
import { rustContext } from '@src/lib/singletons'
import { err } from '@src/lib/trap'
@ -54,3 +54,23 @@ export async function stringToKclExpression(value: string) {
valueText: value,
} satisfies KclExpression
}
export async function retrieveArgFromPipedCallExpression(
callExpression: CallExpressionKw,
name: string
): Promise<KclCommandValue | undefined> {
const arg = callExpression.arguments.find(
(a) => a.label.type === 'Identifier' && a.label.name === name
)
if (
arg?.type === 'LabeledArg' &&
(arg.arg.type === 'Name' || arg.arg.type === 'Literal')
) {
const value = arg.arg.type === 'Name' ? arg.arg.name.name : arg.arg.raw
const result = await stringToKclExpression(value)
if (!(err(result) || 'errors' in result)) {
return result
}
}
return undefined
}

View File

@ -1,6 +1,7 @@
import type { OpKclValue, Operation } from '@rust/kcl-lib/bindings/Operation'
import type { CustomIconName } from '@src/components/CustomIcon'
import { getNodeFromPath } from '@src/lang/queryAst'
import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils'
import type { Artifact } from '@src/lang/std/artifactGraph'
import {
@ -10,13 +11,16 @@ import {
getSweepEdgeCodeRef,
getWallCodeRef,
} from '@src/lang/std/artifactGraph'
import { sourceRangeFromRust } from '@src/lang/wasm'
import { type PipeExpression, sourceRangeFromRust } from '@src/lang/wasm'
import type {
HelixModes,
ModelingCommandSchema,
} from '@src/lib/commandBarConfigs/modelingCommandConfig'
import type { KclExpression } from '@src/lib/commandTypes'
import { stringToKclExpression } from '@src/lib/kclHelpers'
import {
stringToKclExpression,
retrieveArgFromPipedCallExpression,
} from '@src/lib/kclHelpers'
import { isDefaultPlaneStr } from '@src/lib/planes'
import type { Selection, Selections } from '@src/lib/selections'
import { codeManager, kclManager, rustContext } from '@src/lib/singletons'
@ -46,6 +50,7 @@ interface StdLibCallInfo {
| PrepareToEditCallback
| PrepareToEditFailurePayload
supportsAppearance?: boolean
supportsTransform?: boolean
}
/**
@ -1008,6 +1013,7 @@ export const stdLibMap: Record<string, StdLibCallInfo> = {
icon: 'extrude',
prepareToEdit: prepareToEditExtrude,
supportsAppearance: true,
supportsTransform: true,
},
fillet: {
label: 'Fillet',
@ -1026,19 +1032,26 @@ export const stdLibMap: Record<string, StdLibCallInfo> = {
hollow: {
label: 'Hollow',
icon: 'hollow',
supportsAppearance: true,
supportsTransform: true,
},
import: {
label: 'Import',
icon: 'import',
supportsAppearance: true,
supportsTransform: true,
},
intersect: {
label: 'Intersect',
icon: 'booleanIntersect',
supportsAppearance: true,
supportsTransform: true,
},
loft: {
label: 'Loft',
icon: 'loft',
supportsAppearance: true,
supportsTransform: true,
},
offsetPlane: {
label: 'Offset Plane',
@ -1052,6 +1065,8 @@ export const stdLibMap: Record<string, StdLibCallInfo> = {
patternCircular3d: {
label: 'Circular Pattern',
icon: 'patternCircular3d',
supportsAppearance: true,
supportsTransform: true,
},
patternLinear2d: {
label: 'Linear Pattern',
@ -1060,18 +1075,22 @@ export const stdLibMap: Record<string, StdLibCallInfo> = {
patternLinear3d: {
label: 'Linear Pattern',
icon: 'patternLinear3d',
supportsAppearance: true,
supportsTransform: true,
},
revolve: {
label: 'Revolve',
icon: 'revolve',
prepareToEdit: prepareToEditRevolve,
supportsAppearance: true,
supportsTransform: true,
},
shell: {
label: 'Shell',
icon: 'shell',
prepareToEdit: prepareToEditShell,
supportsAppearance: true,
supportsTransform: true,
},
startSketchOn: {
label: 'Sketch',
@ -1284,7 +1303,6 @@ export async function enterEditFlow({
export async function enterAppearanceFlow({
operation,
artifact,
}: EnterEditFlowProps): Promise<Error | CommandBarMachineEvent> {
if (operation.type !== 'StdLibCall' && operation.type !== 'KclStdLibCall') {
return new Error(
@ -1300,7 +1318,6 @@ export async function enterAppearanceFlow({
sourceRangeFromRust(operation.sourceRange)
),
}
console.log('argDefaultValues', argDefaultValues)
return {
type: 'Find and select command',
data: {
@ -1315,3 +1332,101 @@ export async function enterAppearanceFlow({
'Appearance setting not yet supported for this operation. Please edit in the code editor.'
)
}
export async function enterTranslateFlow({
operation,
}: EnterEditFlowProps): Promise<Error | CommandBarMachineEvent> {
const isModuleImport = operation.type === 'GroupBegin'
const isSupportedStdLibCall =
(operation.type === 'KclStdLibCall' || operation.type === 'StdLibCall') &&
stdLibMap[operation.name]?.supportsTransform
if (!isModuleImport && !isSupportedStdLibCall) {
return new Error(
'Unsupported operation type. Please edit in the code editor.'
)
}
const nodeToEdit = getNodePathFromSourceRange(
kclManager.ast,
sourceRangeFromRust(operation.sourceRange)
)
let x: KclExpression | undefined = undefined
let y: KclExpression | undefined = undefined
let z: KclExpression | undefined = undefined
const pipe = getNodeFromPath<PipeExpression>(
kclManager.ast,
nodeToEdit,
'PipeExpression'
)
if (!err(pipe) && pipe.node.body) {
const translate = pipe.node.body.find(
(n) => n.type === 'CallExpressionKw' && n.callee.name.name === 'translate'
)
if (translate?.type === 'CallExpressionKw') {
x = await retrieveArgFromPipedCallExpression(translate, 'x')
y = await retrieveArgFromPipedCallExpression(translate, 'y')
z = await retrieveArgFromPipedCallExpression(translate, 'z')
}
}
// Won't be used since we provide nodeToEdit
const selection: Selections = { graphSelections: [], otherSelections: [] }
const argDefaultValues = { nodeToEdit, selection, x, y, z }
return {
type: 'Find and select command',
data: {
name: 'Translate',
groupId: 'modeling',
argDefaultValues,
},
}
}
export async function enterRotateFlow({
operation,
}: EnterEditFlowProps): Promise<Error | CommandBarMachineEvent> {
const isModuleImport = operation.type === 'GroupBegin'
const isSupportedStdLibCall =
(operation.type === 'KclStdLibCall' || operation.type === 'StdLibCall') &&
stdLibMap[operation.name]?.supportsTransform
if (!isModuleImport && !isSupportedStdLibCall) {
return new Error(
'Unsupported operation type. Please edit in the code editor.'
)
}
const nodeToEdit = getNodePathFromSourceRange(
kclManager.ast,
sourceRangeFromRust(operation.sourceRange)
)
let roll: KclExpression | undefined = undefined
let pitch: KclExpression | undefined = undefined
let yaw: KclExpression | undefined = undefined
const pipe = getNodeFromPath<PipeExpression>(
kclManager.ast,
nodeToEdit,
'PipeExpression'
)
if (!err(pipe) && pipe.node.body) {
const rotate = pipe.node.body.find(
(n) => n.type === 'CallExpressionKw' && n.callee.name.name === 'rotate'
)
if (rotate?.type === 'CallExpressionKw') {
roll = await retrieveArgFromPipedCallExpression(rotate, 'roll')
pitch = await retrieveArgFromPipedCallExpression(rotate, 'pitch')
yaw = await retrieveArgFromPipedCallExpression(rotate, 'yaw')
}
}
// Won't be used since we provide nodeToEdit
const selection: Selections = { graphSelections: [], otherSelections: [] }
const argDefaultValues = { nodeToEdit, selection, roll, pitch, yaw }
return {
type: 'Find and select command',
data: {
name: 'Rotate',
groupId: 'modeling',
argDefaultValues,
},
}
}

View File

@ -1,5 +1,6 @@
import toast from 'react-hot-toast'
import { BSON } from 'bson'
import type { Configuration } from '@rust/kcl-lib/bindings/Configuration'
import type { DefaultPlanes } from '@rust/kcl-lib/bindings/DefaultPlanes'
import type { KclError as RustKclError } from '@rust/kcl-lib/bindings/KclError'
@ -24,6 +25,8 @@ import { err, reportRejection } from '@src/lib/trap'
import type { DeepPartial } from '@src/lib/types'
import type { ModuleType } from '@src/lib/wasm_lib_wrapper'
import { getModule } from '@src/lib/wasm_lib_wrapper'
import type { Models } from '@kittycad/lib/dist/types/src'
import { parseJson } from '@src/lib/utils'
export default class RustContext {
private wasmInitFailed: boolean = true
@ -58,6 +61,7 @@ export default class RustContext {
// Create a new context instance
async create(): Promise<Context> {
this.rustInstance = getModule()
// We need this await here, DO NOT REMOVE it even if your editor says it's
// unnecessary. The constructor of the module is async and it will not
// resolve if you don't await it.
@ -137,7 +141,7 @@ export default class RustContext {
JSON.stringify(settings)
)
} catch (e: any) {
const parsed: RustKclError = JSON.parse(e.toString())
const parsed: RustKclError = parseJson(e.toString())
toast.error(parsed.msg, { id: toastId })
return
}
@ -203,6 +207,21 @@ export default class RustContext {
return this.defaultPlanes[key]
}
// Send a response back to the rust side, that we got back from the engine.
async sendResponse(
response: Models['WebSocketResponse_type']
): Promise<void> {
const instance = await this._checkInstance()
try {
const serialized = BSON.serialize(response)
await instance.sendResponse(serialized)
} catch (e: any) {
const err = errFromErrWithOutputs(e)
return Promise.reject(err)
}
}
// Helper to check if context instance exists
private async _checkInstance(): Promise<Context> {
if (!this.ctxInstance) {

View File

@ -45,6 +45,7 @@ import {
} from '@src/lib/utils'
import { engineStreamActor } from '@src/machines/appMachine'
import type { ModelingMachineEvent } from '@src/machines/modelingMachine'
import { showUnsupportedSelectionToast } from '@src/components/ToastUnsupportedSelection'
export const X_AXIS_UUID = 'ad792545-7fd3-482a-a602-a93924e3055b'
export const Y_AXIS_UUID = '680fd157-266f-4b8a-984f-cdf46b8bdf01'
@ -107,6 +108,18 @@ export async function getEventForSelectWithPoint({
}
let _artifact = kclManager.artifactGraph.get(data.entity_id)
if (!_artifact) {
// if there's no artifact but there is a data.entity_id, it means we don't recognize the engine entity
// we should still return an empty singleCodeCursor to plug into the selection logic
// (i.e. if the user is holding shift they can keep selecting)
// but we should also put up a toast
// toast.error('some edges or faces are not currently selectable')
showUnsupportedSelectionToast()
return {
type: 'Set selection',
data: { selectionType: 'singleCodeCursor' },
}
}
const codeRefs = getCodeRefsByArtifactId(
data.entity_id,
kclManager.artifactGraph

View File

@ -362,17 +362,33 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
],
},
{
id: 'transform',
icon: 'angle',
status: 'kcl-only',
title: 'Transform',
description: 'Apply a translation and/or rotation to a module',
onClick: () => undefined,
id: 'translate',
onClick: () =>
commandBarActor.send({
type: 'Find and select command',
data: { name: 'Translate', groupId: 'modeling' },
}),
status: DEV || IS_NIGHTLY_OR_DEBUG ? 'available' : 'kcl-only',
title: 'Translate',
description: 'Apply a translation to a solid or sketch.',
links: [
{
label: 'API docs',
url: 'https://zoo.dev/docs/kcl/translate',
},
],
},
{
id: 'rotate',
onClick: () =>
commandBarActor.send({
type: 'Find and select command',
data: { name: 'Rotate', groupId: 'modeling' },
}),
status: DEV || IS_NIGHTLY_OR_DEBUG ? 'available' : 'kcl-only',
title: 'Rotate',
description: 'Apply a rotation to a solid or sketch.',
links: [
{
label: 'API docs',
url: 'https://zoo.dev/docs/kcl/rotate',

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