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>
This commit is contained in:
Jess Frazelle
2025-04-17 17:22:19 -07:00
committed by GitHub
parent 0b9889e313
commit bd4bad0020
40 changed files with 5681335 additions and 155 deletions

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 - `zoo` (the default), forward: -Y, up: +Z, handedness: right
- `opengl`, forward: +Z, up: +Y, handedness: right - `opengl`, forward: +Z, up: +Y, handedness: right
- `vulkan`, forward: +Z, up: -Y, handedness: left - `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 ### 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 ### Examples

View File

@ -37,7 +37,7 @@ scale(
### Returns ### 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 ### Examples

View File

@ -28101,14 +28101,62 @@
"args": [ "args": [
{ {
"name": "solids", "name": "solids",
"type": "[Solid]", "type": "SolidOrImportedGeometry",
"schema": { "schema": {
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema", "$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
"title": "Array_of_Solid", "title": "SolidOrImportedGeometry",
"type": "array", "description": "Data for a solid or an imported geometry.",
"items": { "oneOf": [
"$ref": "#/components/schemas/Solid" {
}, "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": { "definitions": {
"Solid": { "Solid": {
"type": "object", "type": "object",
@ -34571,14 +34619,62 @@
], ],
"returnValue": { "returnValue": {
"name": "", "name": "",
"type": "[Solid]", "type": "SolidOrImportedGeometry",
"schema": { "schema": {
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema", "$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
"title": "Array_of_Solid", "title": "SolidOrImportedGeometry",
"type": "array", "description": "Data for a solid or an imported geometry.",
"items": { "oneOf": [
"$ref": "#/components/schemas/Solid" {
}, "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": { "definitions": {
"Solid": { "Solid": {
"type": "object", "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 _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)", "// 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 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": {
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema", "$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
"title": "SolidOrSketchOrImportedGeometry", "title": "SolidOrSketchOrImportedGeometry",
"description": "Data for a solid or an imported geometry.", "description": "Data for a solid, sketch, or an imported geometry.",
"oneOf": [ "oneOf": [
{ {
"description": "Data for an imported geometry.", "description": "Data for an imported geometry.",
@ -260975,7 +261072,7 @@
"schema": { "schema": {
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema", "$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
"title": "SolidOrSketchOrImportedGeometry", "title": "SolidOrSketchOrImportedGeometry",
"description": "Data for a solid or an imported geometry.", "description": "Data for a solid, sketch, or an imported geometry.",
"oneOf": [ "oneOf": [
{ {
"description": "Data for an imported geometry.", "description": "Data for an imported geometry.",
@ -262721,7 +262818,7 @@
"schema": { "schema": {
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema", "$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
"title": "SolidOrSketchOrImportedGeometry", "title": "SolidOrSketchOrImportedGeometry",
"description": "Data for a solid or an imported geometry.", "description": "Data for a solid, sketch, or an imported geometry.",
"oneOf": [ "oneOf": [
{ {
"description": "Data for an imported geometry.", "description": "Data for an imported geometry.",
@ -270879,7 +270976,7 @@
"schema": { "schema": {
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema", "$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
"title": "SolidOrSketchOrImportedGeometry", "title": "SolidOrSketchOrImportedGeometry",
"description": "Data for a solid or an imported geometry.", "description": "Data for a solid, sketch, or an imported geometry.",
"oneOf": [ "oneOf": [
{ {
"description": "Data for an imported geometry.", "description": "Data for an imported geometry.",
@ -319774,7 +319871,7 @@
"schema": { "schema": {
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema", "$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
"title": "SolidOrSketchOrImportedGeometry", "title": "SolidOrSketchOrImportedGeometry",
"description": "Data for a solid or an imported geometry.", "description": "Data for a solid, sketch, or an imported geometry.",
"oneOf": [ "oneOf": [
{ {
"description": "Data for an imported geometry.", "description": "Data for an imported geometry.",
@ -327932,7 +328029,7 @@
"schema": { "schema": {
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema", "$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
"title": "SolidOrSketchOrImportedGeometry", "title": "SolidOrSketchOrImportedGeometry",
"description": "Data for a solid or an imported geometry.", "description": "Data for a solid, sketch, or an imported geometry.",
"oneOf": [ "oneOf": [
{ {
"description": "Data for an imported geometry.", "description": "Data for an imported geometry.",

View File

@ -33,7 +33,7 @@ translate(
### Returns ### 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 ### Examples

View File

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

View File

@ -405,10 +405,6 @@ test.describe('Point-and-click assemblies tests', () => {
) )
await scene.settled(cmdBar) 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 expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
await toolbar.closePane('code') await toolbar.closePane('code')
await scene.expectPixelColor(partColor, partPoint, tolerance) await scene.expectPixelColor(partColor, partPoint, tolerance)
@ -453,10 +449,6 @@ test.describe('Point-and-click assemblies tests', () => {
) )
await scene.settled(cmdBar) 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 expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
await toolbar.closePane('code') await toolbar.closePane('code')
await scene.expectPixelColor(partColor, partPoint, tolerance) await scene.expectPixelColor(partColor, partPoint, tolerance)

View File

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

89
rust/Cargo.lock generated
View File

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

View File

@ -89,14 +89,17 @@ winnow = "=0.6.24"
zip = { workspace = true } zip = { workspace = true }
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
instant = { version = "0.1.13", features = ["wasm-bindgen", "inaccurate"] }
js-sys = { version = "0.3.72" } js-sys = { version = "0.3.72" }
tokio = { workspace = true, features = ["sync", "time"] } tokio = { workspace = true, features = ["sync", "time"] }
tower-lsp = { workspace = true, features = ["runtime-agnostic"] } tower-lsp = { workspace = true, features = ["runtime-agnostic"] }
wasm-bindgen = "0.2.99" wasm-bindgen = "0.2.99"
wasm-bindgen-futures = "0.4.49" wasm-bindgen-futures = "0.4.49"
wasm-timer = "0.2.5"
web-sys = { version = "0.3.76", features = ["console"] } web-sys = { version = "0.3.76", features = ["console"] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
instant = "0.1.13"
tokio = { workspace = true, features = ["full"] } tokio = { workspace = true, features = ["full"] }
tokio-tungstenite = { version = "0.24.0", features = [ tokio-tungstenite = { version = "0.24.0", features = [
"rustls-tls-native-roots", "rustls-tls-native-roots",

View File

@ -2109,7 +2109,7 @@ async fn kcl_test_better_type_names() {
}, },
None => todo!(), 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")] #[tokio::test(flavor = "multi_thread")]

View File

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

View File

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

View File

@ -12,7 +12,7 @@ use kcmc::{
WebSocketResponse, WebSocketResponse,
}, },
}; };
use kittycad_modeling_cmds::{self as kcmc}; use kittycad_modeling_cmds::{self as kcmc, websocket::ModelingCmdReq, ImportFiles, ModelingCmd};
use tokio::sync::RwLock; use tokio::sync::RwLock;
use uuid::Uuid; use uuid::Uuid;
@ -29,6 +29,8 @@ pub struct EngineConnection {
batch: Arc<RwLock<Vec<(WebSocketRequest, SourceRange)>>>, batch: Arc<RwLock<Vec<(WebSocketRequest, SourceRange)>>>,
batch_end: Arc<RwLock<IndexMap<uuid::Uuid, (WebSocketRequest, SourceRange)>>>, batch_end: Arc<RwLock<IndexMap<uuid::Uuid, (WebSocketRequest, SourceRange)>>>,
artifact_commands: Arc<RwLock<Vec<ArtifactCommand>>>, 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. /// The default planes for the scene.
default_planes: Arc<RwLock<Option<DefaultPlanes>>>, default_planes: Arc<RwLock<Option<DefaultPlanes>>>,
stats: EngineStats, stats: EngineStats,
@ -40,6 +42,8 @@ impl EngineConnection {
batch: Arc::new(RwLock::new(Vec::new())), batch: Arc::new(RwLock::new(Vec::new())),
batch_end: Arc::new(RwLock::new(IndexMap::new())), batch_end: Arc::new(RwLock::new(IndexMap::new())),
artifact_commands: Arc::new(RwLock::new(Vec::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(), default_planes: Default::default(),
stats: Default::default(), stats: Default::default(),
}) })
@ -57,7 +61,7 @@ impl crate::engine::EngineManager for EngineConnection {
} }
fn responses(&self) -> Arc<RwLock<IndexMap<Uuid, WebSocketResponse>>> { fn responses(&self) -> Arc<RwLock<IndexMap<Uuid, WebSocketResponse>>> {
Arc::new(RwLock::new(IndexMap::new())) self.responses.clone()
} }
fn stats(&self) -> &EngineStats { fn stats(&self) -> &EngineStats {
@ -68,6 +72,10 @@ impl crate::engine::EngineManager for EngineConnection {
self.artifact_commands.clone() 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>>> { fn get_default_planes(&self) -> Arc<RwLock<Option<DefaultPlanes>>> {
self.default_planes.clone() self.default_planes.clone()
} }
@ -80,6 +88,25 @@ impl crate::engine::EngineManager for EngineConnection {
Ok(()) 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( async fn inner_send_modeling_cmd(
&self, &self,
id: uuid::Uuid, id: uuid::Uuid,
@ -109,6 +136,20 @@ impl crate::engine::EngineManager for EngineConnection {
success: true, 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 { WebSocketRequest::ModelingCmdReq(_) => Ok(WebSocketResponse::Success(SuccessWebSocketResponse {
request_id: Some(id), request_id: Some(id),
resp: OkWebSocketResponseData::Modeling { resp: OkWebSocketResponseData::Modeling {

View File

@ -22,6 +22,15 @@ extern "C" {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub type EngineCommandManager; 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)] #[wasm_bindgen(method, js_name = sendModelingCommandFromWasm, catch)]
fn send_modeling_cmd_from_wasm( fn send_modeling_cmd_from_wasm(
this: &EngineCommandManager, this: &EngineCommandManager,
@ -38,33 +47,128 @@ extern "C" {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct EngineConnection { pub struct EngineConnection {
manager: Arc<EngineCommandManager>, manager: Arc<EngineCommandManager>,
response_context: Arc<ResponseContext>,
batch: Arc<RwLock<Vec<(WebSocketRequest, SourceRange)>>>, batch: Arc<RwLock<Vec<(WebSocketRequest, SourceRange)>>>,
batch_end: Arc<RwLock<IndexMap<uuid::Uuid, (WebSocketRequest, SourceRange)>>>, batch_end: Arc<RwLock<IndexMap<uuid::Uuid, (WebSocketRequest, SourceRange)>>>,
responses: Arc<RwLock<IndexMap<Uuid, WebSocketResponse>>>,
artifact_commands: Arc<RwLock<Vec<ArtifactCommand>>>, artifact_commands: Arc<RwLock<Vec<ArtifactCommand>>>,
ids_of_async_commands: Arc<RwLock<IndexMap<Uuid, SourceRange>>>,
/// The default planes for the scene. /// The default planes for the scene.
default_planes: Arc<RwLock<Option<DefaultPlanes>>>, default_planes: Arc<RwLock<Option<DefaultPlanes>>>,
stats: EngineStats, 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. // Safety: WebAssembly will only ever run in a single-threaded context.
unsafe impl Send for EngineConnection {} unsafe impl Send for EngineConnection {}
unsafe impl Sync for EngineConnection {} unsafe impl Sync for EngineConnection {}
impl 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)] #[allow(clippy::arc_with_non_send_sync)]
Ok(EngineConnection { Ok(EngineConnection {
manager: Arc::new(manager), manager: Arc::new(manager),
batch: Arc::new(RwLock::new(Vec::new())), batch: Arc::new(RwLock::new(Vec::new())),
batch_end: Arc::new(RwLock::new(IndexMap::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())), artifact_commands: Arc::new(RwLock::new(Vec::new())),
ids_of_async_commands: Arc::new(RwLock::new(IndexMap::new())),
default_planes: Default::default(), default_planes: Default::default(),
stats: 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( async fn do_send_modeling_cmd(
&self, &self,
id: uuid::Uuid, id: uuid::Uuid,
@ -151,7 +255,7 @@ impl crate::engine::EngineManager for EngineConnection {
} }
fn responses(&self) -> Arc<RwLock<IndexMap<Uuid, WebSocketResponse>>> { fn responses(&self) -> Arc<RwLock<IndexMap<Uuid, WebSocketResponse>>> {
self.responses.clone() self.response_context.responses.clone()
} }
fn stats(&self) -> &EngineStats { fn stats(&self) -> &EngineStats {
@ -162,6 +266,10 @@ impl crate::engine::EngineManager for EngineConnection {
self.artifact_commands.clone() 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>>> { fn get_default_planes(&self) -> Arc<RwLock<Option<DefaultPlanes>>> {
self.default_planes.clone() self.default_planes.clone()
} }
@ -193,6 +301,19 @@ impl crate::engine::EngineManager for EngineConnection {
Ok(()) 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( async fn inner_send_modeling_cmd(
&self, &self,
id: uuid::Uuid, 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) .do_send_modeling_cmd(id, source_range, cmd, id_to_source_range)
.await?; .await?;
let mut responses = self.responses.write().await; self.response_context.add(id, ws_result.clone()).await;
responses.insert(id, ws_result.clone());
drop(responses);
Ok(ws_result) 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. /// Get the artifact commands that have accumulated so far.
fn artifact_commands(&self) -> Arc<RwLock<Vec<ArtifactCommand>>>; 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. /// Take the batch of commands that have accumulated so far and clear them.
async fn take_batch(&self) -> Vec<(WebSocketRequest, SourceRange)> { async fn take_batch(&self) -> Vec<(WebSocketRequest, SourceRange)> {
std::mem::take(&mut *self.batch().write().await) 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) 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. /// Take the responses that have accumulated so far and clear them.
async fn take_responses(&self) -> IndexMap<Uuid, WebSocketResponse> { async fn take_responses(&self) -> IndexMap<Uuid, WebSocketResponse> {
std::mem::take(&mut *self.responses().write().await) 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) { async fn clear_queues(&self) {
self.batch().write().await.clear(); self.batch().write().await.clear();
self.batch_end().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. /// Send a modeling command and wait for the response message.
async fn inner_send_modeling_cmd( async fn inner_send_modeling_cmd(
&self, &self,
@ -180,6 +198,68 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
Ok(()) 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. /// Set the visibility of edges.
async fn set_edge_visibility( async fn set_edge_visibility(
&self, &self,
@ -342,6 +422,36 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
self.run_batch(requests, source_range).await 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. /// Run the batch for the specific commands.
async fn run_batch( async fn run_batch(
&self, &self,

View File

@ -15,6 +15,8 @@ use crate::{
std::{args::TyF64, sketch::PlaneData}, std::{args::TyF64, sketch::PlaneData},
}; };
use super::ExecutorContext;
type Point2D = kcmc::shared::Point2d<f64>; type Point2D = kcmc::shared::Point2d<f64>;
type Point3D = kcmc::shared::Point3d<f64>; type Point3D = kcmc::shared::Point3d<f64>;
@ -76,9 +78,45 @@ pub struct ImportedGeometry {
pub value: Vec<String>, pub value: Vec<String>,
#[serde(skip)] #[serde(skip)]
pub meta: Vec<Metadata>, 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)] #[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)] #[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")] #[serde(tag = "type", rename_all = "camelCase")]
@ -128,11 +166,61 @@ impl From<SolidOrSketchOrImportedGeometry> for crate::execution::KclValue {
} }
impl SolidOrSketchOrImportedGeometry { 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 { match self {
SolidOrSketchOrImportedGeometry::ImportedGeometry(s) => vec![s.id], SolidOrSketchOrImportedGeometry::ImportedGeometry(s) => {
SolidOrSketchOrImportedGeometry::SolidSet(s) => s.iter().map(|s| s.id).collect(), let id = s.id(ctx).await?;
SolidOrSketchOrImportedGeometry::SketchSet(s) => s.iter().map(|s| s.id).collect(),
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}, coord::{System, KITTYCAD},
each_cmd as mcmd, each_cmd as mcmd,
format::InputFormat3d, format::InputFormat3d,
ok_response::OkModelingCmdResponse,
shared::FileImportFormat, shared::FileImportFormat,
units::UnitLength, units::UnitLength,
websocket::OkWebSocketResponseData,
ImportFile, ModelingCmd, ImportFile, ModelingCmd,
}; };
use kittycad_modeling_cmds as kcmc; 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> { pub async fn send_to_engine(pre: PreImportedGeometry, ctxt: &ExecutorContext) -> Result<ImportedGeometry, KclError> {
if ctxt.no_engine_commands().await { let imported_geometry = ImportedGeometry::new(
return Ok(ImportedGeometry { pre.id,
id: pre.id, pre.command.files.iter().map(|f| f.path.to_string()).collect(),
value: pre.command.files.iter().map(|f| f.path.to_string()).collect(), vec![pre.source_range.into()],
meta: vec![pre.source_range.into()], );
});
}
let resp = ctxt ctxt.engine
.engine .async_modeling_cmd(pre.id, pre.source_range, &ModelingCmd::from(pre.command.clone()))
.send_modeling_cmd(pre.id, pre.source_range, &ModelingCmd::from(pre.command.clone()))
.await?; .await?;
let OkWebSocketResponseData::Modeling { Ok(imported_geometry)
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()],
})
} }
/// Get the source format from the extension. /// Get the source format from the extension.

View File

@ -947,7 +947,7 @@ impl ExecutorContext {
exec_state.global.artifact_graph.clone(), exec_state.global.artifact_graph.clone(),
module_id_to_module_path, module_id_to_module_path,
exec_state.global.id_to_source.clone(), 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; cache::write_old_memory((mem, exec_state.global.module_infos.clone())).await;
} }
let session_data = self.engine.get_session_data().await; let session_data = self.engine.get_session_data().await;
Ok((env_ref, session_data)) Ok((env_ref, session_data))
} }
@ -984,6 +985,9 @@ impl ExecutorContext {
) )
.await; .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 // If we errored out and early-returned, there might be commands which haven't been executed
// and should be dropped. // and should be dropped.
self.engine.clear_queues().await; 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)), 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 // No easy way to make a Face, Sketch, Solid, or Helix
KclValue::ImportedGeometry(crate::execution::ImportedGeometry { KclValue::ImportedGeometry(crate::execution::ImportedGeometry::new(
id: uuid::Uuid::nil(), uuid::Uuid::nil(),
value: Vec::new(), Vec::new(),
meta: Vec::new(), Vec::new(),
}), )),
// Other values don't have types // Other values don't have types
] ]
} }

View File

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

View File

@ -2579,3 +2579,24 @@ mod tangent_to_3_point_arc {
super::execute(TEST_NAME, true).await 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}, errors::{KclError, KclErrorDetails},
execution::{ execution::{
types::{NumericType, PrimitiveType, RuntimeType}, types::{NumericType, PrimitiveType, RuntimeType},
ExecState, KclValue, Solid, ExecState, KclValue, SolidOrImportedGeometry,
}, },
std::Args, std::Args,
}; };
@ -43,7 +43,11 @@ struct AppearanceData {
/// Set the appearance of a solid. This only works on solids, not sketches or individual paths. /// Set the appearance of a solid. This only works on solids, not sketches or individual paths.
pub async fn appearance(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn appearance(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let 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 color: String = args.get_kw_arg("color")?;
let count_ty = RuntimeType::Primitive(PrimitiveType::Number(NumericType::count())); 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 /// 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 { #[stdlib {
name = "appearance", name = "appearance",
keywords = true, keywords = true,
@ -282,14 +299,16 @@ pub async fn appearance(exec_state: &mut ExecState, args: Args) -> Result<KclVal
} }
}] }]
async fn inner_appearance( async fn inner_appearance(
solids: Vec<Solid>, solids: SolidOrImportedGeometry,
color: String, color: String,
metalness: Option<f64>, metalness: Option<f64>,
roughness: Option<f64>, roughness: Option<f64>,
exec_state: &mut ExecState, exec_state: &mut ExecState,
args: Args, args: Args,
) -> Result<Vec<Solid>, KclError> { ) -> Result<SolidOrImportedGeometry, KclError> {
for solid in &solids { let mut solids = solids.clone();
for solid_id in solids.ids(&args.ctx).await? {
// Set the material properties. // Set the material properties.
let rgb = rgba_simple::RGB::<f32>::from_hex(&color).map_err(|err| { let rgb = rgba_simple::RGB::<f32>::from_hex(&color).map_err(|err| {
KclError::Semantic(KclErrorDetails { KclError::Semantic(KclErrorDetails {
@ -308,7 +327,7 @@ async fn inner_appearance(
args.batch_modeling_cmd( args.batch_modeling_cmd(
exec_state.next_uuid(), exec_state.next_uuid(),
ModelingCmd::from(mcmd::ObjectSetMaterialParamsPbr { ModelingCmd::from(mcmd::ObjectSetMaterialParamsPbr {
object_id: solid.id, object_id: solid_id,
color, color,
metalness: metalness.unwrap_or_default() as f32 / 100.0, metalness: metalness.unwrap_or_default() as f32 / 100.0,
roughness: roughness.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, 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)] #[derive(Debug, Clone)]
pub struct Arg { pub struct Arg {
/// The evaluated argument. /// The evaluated argument.
@ -220,18 +223,19 @@ impl Args {
ty.human_friendly_type(), ty.human_friendly_type(),
); );
let suggestion = match (ty, actual_type_name) { let suggestion = match (ty, actual_type_name) {
(RuntimeType::Primitive(PrimitiveType::Solid), "Sketch") => Some( (RuntimeType::Primitive(PrimitiveType::Solid), "Sketch") => Some(ERROR_STRING_SKETCH_TO_SOLID_HELPER),
"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(ERROR_STRING_SKETCH_TO_SOLID_HELPER)
(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`",
),
_ => None, _ => None,
}; };
let message = match suggestion { let mut message = match suggestion {
None => msg_base, None => msg_base,
Some(sugg) => format!("{msg_base}. {sugg}"), 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 { KclError::Semantic(KclErrorDetails {
source_ranges: arg.source_ranges(), source_ranges: arg.source_ranges(),
message, message,
@ -343,18 +347,20 @@ impl Args {
ty.human_friendly_type(), ty.human_friendly_type(),
); );
let suggestion = match (ty, actual_type_name) { let suggestion = match (ty, actual_type_name) {
(RuntimeType::Primitive(PrimitiveType::Solid), "Sketch") => Some( (RuntimeType::Primitive(PrimitiveType::Solid), "Sketch") => Some(ERROR_STRING_SKETCH_TO_SOLID_HELPER),
"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(ERROR_STRING_SKETCH_TO_SOLID_HELPER)
(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`",
),
_ => None, _ => None,
}; };
let message = match suggestion { let mut message = match suggestion {
None => msg_base, None => msg_base,
Some(sugg) => format!("{msg_base}. {sugg}"), 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 { KclError::Semantic(KclErrorDetails {
source_ranges: arg.source_ranges(), source_ranges: arg.source_ranges(),
message, 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 { impl<'a> FromKclValue<'a> for super::sketch::SketchData {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> { fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
// Order is critical since PlaneData is a subset of Plane. // 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?; 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(); let id = exec_state.next_uuid();
args.batch_modeling_cmd( args.batch_modeling_cmd(
@ -409,7 +410,8 @@ async fn inner_translate(
args.flush_batch_for_solids(exec_state, solids).await?; 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(); let id = exec_state.next_uuid();
args.batch_modeling_cmd( args.batch_modeling_cmd(
@ -774,7 +776,8 @@ async fn inner_rotate(
args.flush_batch_for_solids(exec_state, solids).await?; 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(); let id = exec_state.next_uuid();
if let (Some(axis), Some(angle)) = (axis, angle) { 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)

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: Arc<RwLock<Vec<(WebSocketRequest, kcl_lib::SourceRange)>>>,
batch_end: Arc<RwLock<IndexMap<uuid::Uuid, (WebSocketRequest, kcl_lib::SourceRange)>>>, batch_end: Arc<RwLock<IndexMap<uuid::Uuid, (WebSocketRequest, kcl_lib::SourceRange)>>>,
core_test: Arc<RwLock<String>>, core_test: Arc<RwLock<String>>,
ids_of_async_commands: Arc<RwLock<IndexMap<Uuid, kcl_lib::SourceRange>>>,
/// The default planes for the scene. /// The default planes for the scene.
default_planes: Arc<RwLock<Option<DefaultPlanes>>>, default_planes: Arc<RwLock<Option<DefaultPlanes>>>,
stats: EngineStats, stats: EngineStats,
@ -37,6 +38,7 @@ impl EngineConnection {
batch_end: Arc::new(RwLock::new(IndexMap::new())), batch_end: Arc::new(RwLock::new(IndexMap::new())),
core_test: result, core_test: result,
default_planes: Default::default(), default_planes: Default::default(),
ids_of_async_commands: Arc::new(RwLock::new(IndexMap::new())),
stats: Default::default(), stats: Default::default(),
}) })
} }
@ -379,6 +381,10 @@ impl kcl_lib::EngineManager for EngineConnection {
Arc::new(RwLock::new(Vec::new())) 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>>> { fn get_default_planes(&self) -> Arc<RwLock<Option<DefaultPlanes>>> {
self.default_planes.clone() self.default_planes.clone()
} }
@ -391,6 +397,25 @@ impl kcl_lib::EngineManager for EngineConnection {
Ok(()) 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( async fn inner_send_modeling_cmd(
&self, &self,
id: uuid::Uuid, id: uuid::Uuid,

View File

@ -9,6 +9,7 @@ use wasm_bindgen::prelude::*;
#[wasm_bindgen] #[wasm_bindgen]
pub struct Context { pub struct Context {
engine: Arc<Box<dyn EngineManager>>, engine: Arc<Box<dyn EngineManager>>,
response_context: Arc<kcl_lib::wasm_engine::ResponseContext>,
fs: Arc<FileManager>, fs: Arc<FileManager>,
mock_engine: Arc<Box<dyn EngineManager>>, mock_engine: Arc<Box<dyn EngineManager>>,
} }
@ -22,9 +23,10 @@ impl Context {
) -> Result<Self, JsValue> { ) -> Result<Self, JsValue> {
console_error_panic_hook::set_once(); console_error_panic_hook::set_once();
let response_context = Arc::new(kcl_lib::wasm_engine::ResponseContext::new());
Ok(Self { Ok(Self {
engine: Arc::new(Box::new( 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 .await
.map_err(|e| format!("{:?}", e))?, .map_err(|e| format!("{:?}", e))?,
)), )),
@ -34,6 +36,7 @@ impl Context {
.await .await
.map_err(|e| format!("{:?}", e))?, .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. /// Execute a program in mock mode.
#[wasm_bindgen(js_name = executeMock)] #[wasm_bindgen(js_name = executeMock)]
pub async fn execute_mock( pub async fn execute_mock(

View File

@ -1617,6 +1617,12 @@ export class EngineCommandManager extends EventTarget {
return 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 || ''] const pending = this.pendingCommands[message.request_id || '']
if (pending && !message.success) { if (pending && !message.success) {
@ -1931,6 +1937,46 @@ export class EngineCommandManager extends EventTarget {
return e 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 * A wrapper around the sendCommand where all inputs are JSON strings
*/ */

View File

@ -1,5 +1,6 @@
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { BSON } from 'bson'
import type { Configuration } from '@rust/kcl-lib/bindings/Configuration' import type { Configuration } from '@rust/kcl-lib/bindings/Configuration'
import type { DefaultPlanes } from '@rust/kcl-lib/bindings/DefaultPlanes' import type { DefaultPlanes } from '@rust/kcl-lib/bindings/DefaultPlanes'
import type { KclError as RustKclError } from '@rust/kcl-lib/bindings/KclError' import type { KclError as RustKclError } from '@rust/kcl-lib/bindings/KclError'
@ -24,6 +25,7 @@ import { err, reportRejection } from '@src/lib/trap'
import type { DeepPartial } from '@src/lib/types' import type { DeepPartial } from '@src/lib/types'
import type { ModuleType } from '@src/lib/wasm_lib_wrapper' import type { ModuleType } from '@src/lib/wasm_lib_wrapper'
import { getModule } from '@src/lib/wasm_lib_wrapper' import { getModule } from '@src/lib/wasm_lib_wrapper'
import type { Models } from '@kittycad/lib/dist/types/src'
export default class RustContext { export default class RustContext {
private wasmInitFailed: boolean = true private wasmInitFailed: boolean = true
@ -58,6 +60,7 @@ export default class RustContext {
// Create a new context instance // Create a new context instance
async create(): Promise<Context> { async create(): Promise<Context> {
this.rustInstance = getModule() this.rustInstance = getModule()
// We need this await here, DO NOT REMOVE it even if your editor says it's // 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 // unnecessary. The constructor of the module is async and it will not
// resolve if you don't await it. // resolve if you don't await it.
@ -203,6 +206,21 @@ export default class RustContext {
return this.defaultPlanes[key] 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 // Helper to check if context instance exists
private async _checkInstance(): Promise<Context> { private async _checkInstance(): Promise<Context> {
if (!this.ctxInstance) { if (!this.ctxInstance) {