Compare commits

..

8 Commits

49 changed files with 859 additions and 1790 deletions

View File

@ -6,7 +6,6 @@ if [ -z "${TAB_API_URL:-}" ] || [ -z "${TAB_API_KEY:-}" ]; then
fi
project="https://github.com/KittyCAD/modeling-app"
suite="${CI_SUITE:-unit}"
branch="${GITHUB_HEAD_REF:-${GITHUB_REF_NAME:-}}"
commit="${CI_COMMIT_SHA:-${GITHUB_SHA:-}}"
@ -14,7 +13,6 @@ echo "Uploading batch results"
curl --silent --request POST \
--header "X-API-Key: ${TAB_API_KEY}" \
--form "project=${project}" \
--form "suite=${suite}" \
--form "branch=${branch}" \
--form "commit=${commit}" \
--form "tests=@test-results/junit.xml" \

View File

@ -193,7 +193,6 @@ jobs:
TAB_API_KEY: ${{ secrets.TAB_API_KEY }}
CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
CI_PR_NUMBER: ${{ github.event.pull_request.number }}
CI_SUITE: unit:kcl
run-internal-kcl-samples:
name: cargo test (internal-kcl-samples)
runs-on:

View File

@ -156,7 +156,6 @@ jobs:
TAB_API_KEY: ${{ secrets.TAB_API_KEY }}
CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
CI_PR_NUMBER: ${{ github.event.pull_request.number }}
CI_SUITE: snapshots
TARGET: web
- name: Update snapshots
@ -168,7 +167,6 @@ jobs:
TAB_API_KEY: ${{ secrets.TAB_API_KEY }}
CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
CI_PR_NUMBER: ${{ github.event.pull_request.number }}
CI_SUITE: snapshots
TARGET: web
- uses: actions/upload-artifact@v4

View File

@ -105,19 +105,14 @@ export class CmdBarFixture {
expectState = async (expected: CmdBarSerialised) => {
return expect.poll(() => this._serialiseCmdBar()).toEqual(expected)
}
/**
* This method is used to progress the command bar to the next step, defaulting to clicking the next button.
* Optionally, with the `shouldUseKeyboard` parameter, it will hit `Enter` to progress.
* * TODO: This method assumes the user has a valid input to the current stage,
/** The method will use buttons OR press enter randomly to progress the cmdbar,
* this could have unexpected results depending on what's focused
*
* TODO: This method assumes the user has a valid input to the current stage,
* and assumes we are past the `pickCommand` step.
*/
progressCmdBar = async (shouldUseKeyboard = false) => {
progressCmdBar = async (shouldFuzzProgressMethod = true) => {
await this.page.waitForTimeout(2000)
if (shouldUseKeyboard) {
await this.page.keyboard.press('Enter')
return
}
const arrowButton = this.page.getByRole('button', {
name: 'arrow right Continue',
})

View File

@ -61,7 +61,6 @@ class MyAPIReporter implements Reporter {
const payload = {
// Required information
project: 'https://github.com/KittyCAD/modeling-app',
suite: process.env.CI_SUITE || 'e2e',
branch: process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME || '',
commit: process.env.CI_COMMIT_SHA || process.env.GITHUB_SHA || '',
test: test.titlePath().slice(2).join(' '),

View File

@ -1855,11 +1855,7 @@ sketch002 = startSketchOn(XZ)
},
stage: 'review',
})
// Confirm we can submit from the review step with just `Enter`
await cmdBar.progressCmdBar(true)
await cmdBar.expectState({
stage: 'commandBarClosed',
})
await cmdBar.progressCmdBar()
})
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
@ -1999,7 +1995,7 @@ profile001 = ${circleCode}`
},
stage: 'review',
})
await cmdBar.progressCmdBar(true)
await cmdBar.progressCmdBar()
await editor.expectEditor.toContain(sweepDeclaration)
})

View File

@ -281,14 +281,7 @@ impl ExecutorContext {
// Track exports.
if let ItemVisibility::Export = variable_declaration.visibility {
if matches!(body_type, BodyType::Root) {
exec_state.mod_local.module_exports.push(var_name);
} else {
exec_state.err(CompilationError::err(
variable_declaration.as_source_range(),
"Exports are only supported at the top-level of a file. Remove `export` or move it to the top-level.",
));
}
exec_state.mod_local.module_exports.push(var_name);
}
// Variable declaration can be the return value of a module.
last_expr = matches!(body_type, BodyType::Root).then_some(value);

View File

@ -996,27 +996,6 @@ mod import_cycle1 {
super::execute(TEST_NAME, false).await
}
}
mod import_only_at_top_level {
const TEST_NAME: &str = "import_only_at_top_level";
/// 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, false).await
}
}
mod import_function_not_sketch {
const TEST_NAME: &str = "import_function_not_sketch";
@ -1185,27 +1164,6 @@ mod import_foreign {
super::execute(TEST_NAME, false).await
}
}
mod export_var_only_at_top_level {
const TEST_NAME: &str = "export_var_only_at_top_level";
/// 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, false).await
}
}
mod assembly_non_default_units {
const TEST_NAME: &str = "assembly_non_default_units";
@ -3360,24 +3318,3 @@ mod nested_windows_main_kcl {
super::execute(TEST_NAME, true).await
}
}
mod nested_assembly {
const TEST_NAME: &str = "nested_assembly";
/// 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

@ -1,5 +0,0 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Artifact commands export_only_at_top_level.kcl
---
[]

View File

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

View File

@ -1,3 +0,0 @@
```mermaid
flowchart LR
```

View File

@ -1,148 +0,0 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Result of parsing export_only_at_top_level.kcl
---
{
"Ok": {
"body": [
{
"commentStart": 0,
"declaration": {
"commentStart": 0,
"end": 0,
"id": {
"commentStart": 0,
"end": 0,
"name": "main",
"start": 0,
"type": "Identifier"
},
"init": {
"body": {
"body": [
{
"commentStart": 0,
"declaration": {
"commentStart": 0,
"end": 0,
"id": {
"commentStart": 0,
"end": 0,
"name": "x",
"start": 0,
"type": "Identifier"
},
"init": {
"commentStart": 0,
"end": 0,
"raw": "2",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 2.0,
"suffix": "None"
}
},
"start": 0,
"type": "VariableDeclarator"
},
"end": 0,
"kind": "const",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration",
"visibility": "export"
},
{
"argument": {
"commentStart": 0,
"end": 0,
"raw": "0",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 0.0,
"suffix": "None"
}
},
"commentStart": 0,
"end": 0,
"start": 0,
"type": "ReturnStatement",
"type": "ReturnStatement"
}
],
"commentStart": 0,
"end": 0,
"start": 0
},
"commentStart": 0,
"end": 0,
"params": [],
"start": 0,
"type": "FunctionExpression",
"type": "FunctionExpression"
},
"start": 0,
"type": "VariableDeclarator"
},
"end": 0,
"kind": "fn",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"commentStart": 0,
"end": 0,
"expression": {
"callee": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "main",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name"
},
"commentStart": 0,
"end": 0,
"start": 0,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": null
},
"start": 0,
"type": "ExpressionStatement",
"type": "ExpressionStatement"
}
],
"commentStart": 0,
"end": 0,
"nonCodeMeta": {
"nonCodeNodes": {
"0": [
{
"commentStart": 0,
"end": 0,
"start": 0,
"type": "NonCodeNode",
"value": {
"type": "newLine"
}
}
]
},
"startNodes": []
},
"start": 0
}
}

View File

@ -1,15 +0,0 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Error from executing export_only_at_top_level.kcl
---
KCL Semantic error
× semantic: Exports are only supported at the top-level of a file. Remove
│ `export` or move it to the top-level.
╭─[2:3]
1 │ fn main() {
2 │ export x = 2
· ──────┬─────
· ╰── main
3 │ return 0
╰────

View File

@ -1,6 +0,0 @@
fn main() {
export x = 2
return 0
}
main()

View File

@ -1,5 +0,0 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Operations executed export_only_at_top_level.kcl
---
[]

View File

@ -1,10 +0,0 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Result of unparsing export_only_at_top_level.kcl
---
fn main() {
export x = 2
return 0
}
main()

View File

@ -1,32 +0,0 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Artifact commands import_only_at_top_level.kcl
---
[
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "edge_lines_visible",
"hidden": false
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "object_visible",
"object_id": "[uuid]",
"hidden": true
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "object_visible",
"object_id": "[uuid]",
"hidden": true
}
}
]

View File

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

View File

@ -1,3 +0,0 @@
```mermaid
flowchart LR
```

View File

@ -1,129 +0,0 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Result of parsing import_only_at_top_level.kcl
---
{
"Ok": {
"body": [
{
"commentStart": 0,
"declaration": {
"commentStart": 0,
"end": 0,
"id": {
"commentStart": 0,
"end": 0,
"name": "main",
"start": 0,
"type": "Identifier"
},
"init": {
"body": {
"body": [
{
"commentStart": 0,
"end": 0,
"path": {
"type": "Kcl",
"filename": "empty.kcl"
},
"selector": {
"type": "None",
"alias": null
},
"start": 0,
"type": "ImportStatement",
"type": "ImportStatement"
},
{
"argument": {
"commentStart": 0,
"end": 0,
"raw": "0",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 0.0,
"suffix": "None"
}
},
"commentStart": 0,
"end": 0,
"start": 0,
"type": "ReturnStatement",
"type": "ReturnStatement"
}
],
"commentStart": 0,
"end": 0,
"start": 0
},
"commentStart": 0,
"end": 0,
"params": [],
"start": 0,
"type": "FunctionExpression",
"type": "FunctionExpression"
},
"start": 0,
"type": "VariableDeclarator"
},
"end": 0,
"kind": "fn",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"commentStart": 0,
"end": 0,
"expression": {
"callee": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "main",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name"
},
"commentStart": 0,
"end": 0,
"start": 0,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": null
},
"start": 0,
"type": "ExpressionStatement",
"type": "ExpressionStatement"
}
],
"commentStart": 0,
"end": 0,
"nonCodeMeta": {
"nonCodeNodes": {
"0": [
{
"commentStart": 0,
"end": 0,
"start": 0,
"type": "NonCodeNode",
"value": {
"type": "newLine"
}
}
]
},
"startNodes": []
},
"start": 0
}
}

View File

@ -1,30 +0,0 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Error from executing import_only_at_top_level.kcl
---
KCL Semantic error
× semantic: Imports are only supported at the top-level of a file.
╭─[2:3]
1 │ fn main() {
2 │ import "empty.kcl"
· ─────────┬────────
· ╰── tests/import_only_at_top_level/input.kcl
3 │ return 0
╰────
╭─[6:1]
5 │
6 │ main()
· ───┬──
· ╰── tests/import_only_at_top_level/input.kcl
╰────
╰─▶ KCL Semantic error
× semantic: Imports are only supported at the top-level of a file.
╭─[2:3]
1 │ fn main() {
2 │ import "empty.kcl"
· ─────────┬────────
· ╰── tests/import_only_at_top_level/input.kcl
3 │ return 0
╰────

View File

@ -1,6 +0,0 @@
fn main() {
import "empty.kcl"
return 0
}
main()

View File

@ -1,32 +0,0 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Operations executed import_only_at_top_level.kcl
---
[
{
"type": "GroupBegin",
"group": {
"type": "ModuleInstance",
"name": "empty.kcl",
"moduleId": 0
},
"sourceRange": []
},
{
"type": "GroupBegin",
"group": {
"type": "FunctionCall",
"name": "main",
"functionSourceRange": [],
"unlabeledArg": null,
"labeledArgs": {}
},
"sourceRange": []
},
{
"type": "GroupEnd"
},
{
"type": "GroupEnd"
}
]

View File

@ -1,10 +0,0 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Result of unparsing import_only_at_top_level.kcl
---
fn main() {
import "empty.kcl"
return 0
}
main()

View File

@ -1,5 +0,0 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Result of unparsing tests/import_only_at_top_level/empty.kcl
---

View File

@ -1,184 +0,0 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Artifact commands nested_main_kcl.kcl
---
[
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "edge_lines_visible",
"hidden": false
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "object_visible",
"object_id": "[uuid]",
"hidden": true
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "object_visible",
"object_id": "[uuid]",
"hidden": true
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "make_plane",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"x_axis": {
"x": 1.0,
"y": 0.0,
"z": 0.0
},
"y_axis": {
"x": 0.0,
"y": 1.0,
"z": 0.0
},
"size": 60.0,
"clobber": false,
"hide": true
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "close_path",
"path_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "enable_sketch_mode",
"entity_id": "[uuid]",
"ortho": false,
"animated": false,
"adjust_camera": false,
"planar_normal": {
"x": 0.0,
"y": 0.0,
"z": 1.0
}
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "extend_path",
"path": "[uuid]",
"segment": {
"type": "arc",
"center": {
"x": 15.0,
"y": 0.0
},
"radius": 5.0,
"start": {
"unit": "degrees",
"value": 0.0
},
"end": {
"unit": "degrees",
"value": 360.0
},
"relative": false
}
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "move_path_pen",
"path": "[uuid]",
"to": {
"x": 20.0,
"y": 0.0,
"z": 0.0
}
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "sketch_mode_disable"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "start_path"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "object_bring_to_front",
"object_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "revolve",
"target": "[uuid]",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"axis": {
"x": 0.0,
"y": 1.0,
"z": 0.0
},
"axis_is_2d": true,
"angle": {
"unit": "degrees",
"value": 360.0
},
"tolerance": 0.0000001,
"opposite": "None"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_adjacency_info",
"object_id": "[uuid]",
"edge_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "solid3d_get_extrusion_face_info",
"object_id": "[uuid]",
"edge_id": "[uuid]"
}
}
]

View File

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

View File

@ -1,23 +0,0 @@
```mermaid
flowchart LR
subgraph path2 [Path]
2["Path<br>[43, 81, 1]"]
3["Segment<br>[43, 81, 1]"]
4[Solid2d]
end
1["Plane<br>[18, 35, 1]"]
5["Sweep Revolve<br>[89, 142, 1]"]
6[Wall]
%% face_code_ref=Missing NodePath
7["SweepEdge Adjacent"]
1 --- 2
2 --- 3
2 --- 4
2 ---- 5
5 <--x 3
3 --- 6
3 --- 7
5 --- 6
5 --- 7
6 --- 7
```

View File

@ -1,73 +0,0 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Result of parsing nested_main_kcl.kcl
---
{
"Ok": {
"body": [
{
"commentStart": 0,
"end": 0,
"path": {
"type": "Kcl",
"filename": "nested/foo/bar/main.kcl"
},
"selector": {
"type": "None",
"alias": {
"commentStart": 0,
"end": 0,
"name": "bar",
"start": 0,
"type": "Identifier"
}
},
"start": 0,
"type": "ImportStatement",
"type": "ImportStatement"
},
{
"commentStart": 0,
"end": 0,
"expression": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "bar",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name",
"type": "Name"
},
"start": 0,
"type": "ExpressionStatement",
"type": "ExpressionStatement"
}
],
"commentStart": 0,
"end": 0,
"nonCodeMeta": {
"nonCodeNodes": {
"0": [
{
"commentStart": 0,
"end": 0,
"start": 0,
"type": "NonCodeNode",
"value": {
"type": "newLine"
}
}
]
},
"startNodes": []
},
"start": 0
}
}

View File

@ -1,3 +0,0 @@
import "nested/foo/bar/main.kcl" as bar
bar

View File

@ -1,7 +0,0 @@
// A donut shape.
startSketchOn(XY)
|> circle( center = [15, 0], radius = 5 )
|> revolve(
angle = 360,
axis = Y,
)

View File

@ -1,3 +0,0 @@
import "imported.kcl" as imported
imported

View File

@ -1,18 +0,0 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Operations executed nested_main_kcl.kcl
---
[
{
"type": "GroupBegin",
"group": {
"type": "ModuleInstance",
"name": "main.kcl",
"moduleId": 0
},
"sourceRange": []
},
{
"type": "GroupEnd"
}
]

View File

@ -1,10 +0,0 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Variables in memory after executing nested_main_kcl.kcl
---
{
"bar": {
"type": "Module",
"value": 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

View File

@ -1,7 +0,0 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Result of unparsing nested_main_kcl.kcl
---
import "nested/foo/bar/main.kcl" as bar
bar

View File

@ -1,8 +0,0 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Result of unparsing tests/nested_assembly/nested/foo/bar/imported.kcl
---
// A donut shape.
startSketchOn(XY)
|> circle(center = [15, 0], radius = 5)
|> revolve(angle = 360, axis = Y)

View File

@ -1,7 +0,0 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Result of unparsing tests/nested_assembly/nested/foo/bar/main.kcl
---
import "imported.kcl" as imported
imported

View File

@ -1,5 +1,4 @@
import type React from 'react'
import { useMemo, useEffect, useRef, useState } from 'react'
import React, { useMemo, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { ActionButton } from '@src/components/ActionButton'
@ -122,7 +121,6 @@ function CommandBarHeader({ children }: React.PropsWithChildren<object>) {
data-is-current-arg={
argName === currentArgument?.name ? 'true' : 'false'
}
type="button"
disabled={!isReviewing && currentArgument?.name === argName}
onClick={() => {
commandBarActor.send({
@ -246,20 +244,13 @@ function CommandBarHeader({ children }: React.PropsWithChildren<object>) {
type ButtonProps = { bgClassName?: string; iconClassName?: string }
function ReviewingButton({ bgClassName, iconClassName }: ButtonProps) {
const buttonRef = useRef<HTMLButtonElement>(null)
useEffect(() => {
if (buttonRef.current) {
buttonRef.current.focus()
}
}, [])
return (
<ActionButton
Element="button"
ref={buttonRef}
autoFocus
type="submit"
form="review-form"
className="w-fit !p-0 rounded-sm hover:shadow focus:outline-current"
tabIndex={0}
className="w-fit !p-0 rounded-sm hover:shadow"
data-testid="command-bar-submit"
iconStart={{
icon: 'checkmark',
@ -278,8 +269,7 @@ function GatheringArgsButton({ bgClassName, iconClassName }: ButtonProps) {
Element="button"
type="submit"
form="arg-form"
className="w-fit !p-0 rounded-sm hover:shadow focus:outline-current"
tabIndex={0}
className="w-fit !p-0 rounded-sm hover:shadow"
data-testid="command-bar-continue"
iconStart={{
icon: 'arrowRight',

View File

@ -12,8 +12,12 @@ import { useLoaderData } from 'react-router-dom'
import type { Actor, ContextFrom, Prop, SnapshotFrom, StateFrom } from 'xstate'
import { assign, fromPromise } from 'xstate'
import type { OutputFormat3d } from '@rust/kcl-lib/bindings/ModelingCmd'
import type {
OutputFormat3d,
Point3d,
} from '@rust/kcl-lib/bindings/ModelingCmd'
import type { Node } from '@rust/kcl-lib/bindings/Node'
import type { Plane } from '@rust/kcl-lib/bindings/Plane'
import { useAppState } from '@src/AppState'
import { letEngineAnimateAndSyncCamAfter } from '@src/clientSideScene/CameraControls'
@ -34,16 +38,26 @@ import useModelingMachineCommands from '@src/hooks/useStateMachineCommands'
import { useKclContext } from '@src/lang/KclProvider'
import { updateModelingState } from '@src/lang/modelingWorkflows'
import {
insertNamedConstant,
replaceValueAtNodePath,
sketchOnExtrudedFace,
sketchOnOffsetPlane,
splitPipedProfile,
startSketchOnDefault,
} from '@src/lang/modifyAst'
import {
artifactIsPlaneWithPaths,
doesSketchPipeNeedSplitting,
getNodeFromPath,
isCursorInFunctionDefinition,
traverse,
} from '@src/lang/queryAst'
import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils'
import {
getFaceCodeRef,
getPathsFromArtifact,
getPlaneFromArtifact,
} from '@src/lang/std/artifactGraph'
import {
EngineConnectionStateType,
EngineConnectionEvents,
@ -52,6 +66,7 @@ import { err, reportRejection, trap, reject } from '@src/lib/trap'
import { isNonNullable, platform, uuidv4 } from '@src/lib/utils'
import { promptToEditFlow } from '@src/lib/promptToEdit'
import type { FileMeta } from '@src/lib/types'
import { kclEditorActor } from '@src/machines/kclEditorMachine'
import { commandBarActor } from '@src/lib/singletons'
import { useToken, useSettings } from '@src/lib/singletons'
import type { IndexLoaderData } from '@src/lib/types'
@ -83,13 +98,23 @@ import {
} from '@src/lib/singletons'
import type { MachineManager } from '@src/components/MachineManagerProvider'
import { MachineManagerContext } from '@src/components/MachineManagerProvider'
import { updateSelections } from '@src/lib/selections'
import { updateSketchDetailsNodePaths } from '@src/lang/util'
import {
handleSelectionBatch,
updateSelections,
type Selections,
} from '@src/lib/selections'
import {
crossProduct,
isCursorInSketchCommandRange,
updateSketchDetailsNodePaths,
} from '@src/lang/util'
import {
modelingMachineCommandConfig,
type ModelingCommandSchema,
} from '@src/lib/commandBarConfigs/modelingCommandConfig'
import type {
KclValue,
PathToNode,
PipeExpression,
Program,
VariableDeclaration,
@ -295,6 +320,229 @@ export const ModelingMachineProvider = ({
},
}
}),
'Set selection': assign(
({ context: { selectionRanges, sketchDetails }, event }) => {
// this was needed for ts after adding 'Set selection' action to on done modal events
const setSelections =
('data' in event &&
event.data &&
'selectionType' in event.data &&
event.data) ||
('output' in event &&
event.output &&
'selectionType' in event.output &&
event.output) ||
null
if (!setSelections) return {}
let selections: Selections = {
graphSelections: [],
otherSelections: [],
}
if (setSelections.selectionType === 'singleCodeCursor') {
if (!setSelections.selection && editorManager.isShiftDown) {
// if the user is holding shift, but they didn't select anything
// don't nuke their other selections (frustrating to have one bad click ruin your
// whole selection)
selections = {
graphSelections: selectionRanges.graphSelections,
otherSelections: selectionRanges.otherSelections,
}
} else if (
!setSelections.selection &&
!editorManager.isShiftDown
) {
selections = {
graphSelections: [],
otherSelections: [],
}
} else if (
setSelections.selection &&
!editorManager.isShiftDown
) {
selections = {
graphSelections: [setSelections.selection],
otherSelections: [],
}
} else if (setSelections.selection && editorManager.isShiftDown) {
// selecting and deselecting multiple objects
/**
* There are two scenarios:
* 1. General case:
* When selecting and deselecting edges,
* faces or segment (during sketch edit)
* we use its artifact ID to identify the selection
* 2. Initial sketch setup:
* The artifact is not yet created
* so we use the codeRef.range
*/
let updatedSelections: typeof selectionRanges.graphSelections
// 1. General case: Artifact exists, use its ID
if (setSelections.selection.artifact?.id) {
// check if already selected
const alreadySelected = selectionRanges.graphSelections.some(
(selection) =>
selection.artifact?.id ===
setSelections.selection?.artifact?.id
)
if (
alreadySelected &&
setSelections.selection?.artifact?.id
) {
// remove it
updatedSelections = selectionRanges.graphSelections.filter(
(selection) =>
selection.artifact?.id !==
setSelections.selection?.artifact?.id
)
} else {
// add it
updatedSelections = [
...selectionRanges.graphSelections,
setSelections.selection,
]
}
} else {
// 2. Initial sketch setup: Artifact not yet created use codeRef.range
const selectionRange = JSON.stringify(
setSelections.selection?.codeRef?.range
)
// check if already selected
const alreadySelected = selectionRanges.graphSelections.some(
(selection) => {
const existingRange = JSON.stringify(
selection.codeRef?.range
)
return existingRange === selectionRange
}
)
if (
alreadySelected &&
setSelections.selection?.codeRef?.range
) {
// remove it
updatedSelections = selectionRanges.graphSelections.filter(
(selection) =>
JSON.stringify(selection.codeRef?.range) !==
selectionRange
)
} else {
// add it
updatedSelections = [
...selectionRanges.graphSelections,
setSelections.selection,
]
}
}
selections = {
graphSelections: updatedSelections,
otherSelections: selectionRanges.otherSelections,
}
}
const {
engineEvents,
codeMirrorSelection,
updateSceneObjectColors,
} = handleSelectionBatch({
selections,
})
if (codeMirrorSelection) {
kclEditorActor.send({
type: 'setLastSelectionEvent',
data: {
codeMirrorSelection,
scrollIntoView: setSelections.scrollIntoView ?? false,
},
})
}
// If there are engine commands that need sent off, send them
// TODO: This should be handled outside of an action as its own
// actor, so that the system state is more controlled.
engineEvents &&
engineEvents.forEach((event) => {
engineCommandManager
.sendSceneCommand(event)
.catch(reportRejection)
})
updateSceneObjectColors()
return {
selectionRanges: selections,
}
}
if (setSelections.selectionType === 'mirrorCodeMirrorSelections') {
return {
selectionRanges: setSelections.selection,
}
}
if (
setSelections.selectionType === 'axisSelection' ||
setSelections.selectionType === 'defaultPlaneSelection'
) {
if (editorManager.isShiftDown) {
selections = {
graphSelections: selectionRanges.graphSelections,
otherSelections: [setSelections.selection],
}
} else {
selections = {
graphSelections: [],
otherSelections: [setSelections.selection],
}
}
return {
selectionRanges: selections,
}
}
if (setSelections.selectionType === 'completeSelection') {
const codeMirrorSelection = editorManager.createEditorSelection(
setSelections.selection
)
kclEditorActor.send({
type: 'setLastSelectionEvent',
data: {
codeMirrorSelection,
scrollIntoView: false,
},
})
if (!sketchDetails)
return {
selectionRanges: setSelections.selection,
}
return {
selectionRanges: setSelections.selection,
sketchDetails: {
...sketchDetails,
sketchEntryNodePath:
setSelections.updatedSketchEntryNodePath ||
sketchDetails?.sketchEntryNodePath ||
[],
sketchNodePaths:
setSelections.updatedSketchNodePaths ||
sketchDetails?.sketchNodePaths ||
[],
planeNodePath:
setSelections.updatedPlaneNodePath ||
sketchDetails?.planeNodePath ||
[],
},
}
}
return {}
}
),
},
guards: {
'has valid selection for deletion': ({
@ -304,6 +552,35 @@ export const ModelingMachineProvider = ({
if (selectionRanges.graphSelections.length <= 0) return false
return true
},
'is-error-free': () => {
return kclManager.errors.length === 0 && !kclManager.hasErrors()
},
'Selection is on face': ({ context: { selectionRanges }, event }) => {
if (event.type !== 'Enter sketch') return false
if (event.data?.forceNewSketch) return false
if (artifactIsPlaneWithPaths(selectionRanges)) {
return true
} else if (selectionRanges.graphSelections[0]?.artifact) {
// See if the selection is "close enough" to be coerced to the plane later
const maybePlane = getPlaneFromArtifact(
selectionRanges.graphSelections[0].artifact,
kclManager.artifactGraph
)
return !err(maybePlane)
}
if (
isCursorInFunctionDefinition(
kclManager.ast,
selectionRanges.graphSelections[0]
)
) {
return false
}
return !!isCursorInSketchCommandRange(
kclManager.artifactGraph,
selectionRanges
)
},
'Has exportable geometry': () =>
!kclManager.hasErrors() && kclManager.ast.body.length > 0,
},
@ -573,6 +850,123 @@ export const ModelingMachineProvider = ({
animateTargetId: input.planeId,
}
}),
'animate-to-sketch': fromPromise(
async ({ input: { selectionRanges } }) => {
const artifact = selectionRanges.graphSelections[0].artifact
const plane = getPlaneFromArtifact(
artifact,
kclManager.artifactGraph
)
if (err(plane)) return Promise.reject(plane)
// if the user selected a segment, make sure we enter the right sketch as there can be multiple on a plane
// but still works if the user selected a plane/face by defaulting to the first path
const mainPath =
artifact?.type === 'segment' || artifact?.type === 'solid2d'
? artifact?.pathId
: plane?.pathIds[0]
let sketch: KclValue | null = null
let planeVar: Plane | null = null
for (const variable of Object.values(
kclManager.execState.variables
)) {
// find programMemory that matches path artifact
if (
variable?.type === 'Sketch' &&
variable.value.artifactId === mainPath
) {
sketch = variable
break
}
if (
// if the variable is an sweep, check if the underlying sketch matches the artifact
variable?.type === 'Solid' &&
variable.value.sketch.on.type === 'plane' &&
variable.value.sketch.artifactId === mainPath
) {
sketch = {
type: 'Sketch',
value: variable.value.sketch,
}
break
}
if (
variable?.type === 'Plane' &&
plane.id === variable.value.id
) {
planeVar = variable.value
}
}
if (!sketch || sketch.type !== 'Sketch') {
if (artifact?.type !== 'plane')
return Promise.reject(new Error('No sketch'))
const planeCodeRef = getFaceCodeRef(artifact)
if (planeVar && planeCodeRef) {
const toTuple = (point: Point3d): [number, number, number] => [
point.x,
point.y,
point.z,
]
const planPath = getNodePathFromSourceRange(
kclManager.ast,
planeCodeRef.range
)
await letEngineAnimateAndSyncCamAfter(
engineCommandManager,
artifact.id
)
const normal = crossProduct(planeVar.xAxis, planeVar.yAxis)
return {
sketchEntryNodePath: [],
planeNodePath: planPath,
sketchNodePaths: [],
zAxis: toTuple(normal),
yAxis: toTuple(planeVar.yAxis),
origin: toTuple(planeVar.origin),
}
}
return Promise.reject(new Error('No sketch'))
}
const info = await sceneEntitiesManager.getSketchOrientationDetails(
sketch.value
)
await letEngineAnimateAndSyncCamAfter(
engineCommandManager,
info?.sketchDetails?.faceId || ''
)
const sketchArtifact = kclManager.artifactGraph.get(mainPath)
if (sketchArtifact?.type !== 'path')
return Promise.reject(new Error('No sketch artifact'))
const sketchPaths = getPathsFromArtifact({
artifact: kclManager.artifactGraph.get(plane.id),
sketchPathToNode: sketchArtifact?.codeRef?.pathToNode,
artifactGraph: kclManager.artifactGraph,
ast: kclManager.ast,
})
if (err(sketchPaths)) return Promise.reject(sketchPaths)
let codeRef = getFaceCodeRef(plane)
if (!codeRef) return Promise.reject(new Error('No plane codeRef'))
// codeRef.pathToNode is not always populated correctly
const planeNodePath = getNodePathFromSourceRange(
kclManager.ast,
codeRef.range
)
return {
sketchEntryNodePath: sketchArtifact.codeRef.pathToNode || [],
sketchNodePaths: sketchPaths,
planeNodePath,
zAxis: info.sketchDetails.zAxis || null,
yAxis: info.sketchDetails.yAxis || null,
origin: info.sketchDetails.origin.map(
(a) => a / sceneInfra._baseUnitMultiplier
) as [number, number, number],
animateTargetId: info?.sketchDetails?.faceId || '',
}
}
),
'Get horizontal info': fromPromise(
async ({ input: { selectionRanges, sketchDetails } }) => {
const { modifiedAst, pathToNodeMap, exprInsertIndex } =
@ -977,6 +1371,130 @@ export const ModelingMachineProvider = ({
}
}
),
'Apply named value constraint': fromPromise(
async ({ input: { selectionRanges, sketchDetails, data } }) => {
if (!sketchDetails) {
return Promise.reject(new Error('No sketch details'))
}
if (!data) {
return Promise.reject(new Error('No data from command flow'))
}
let pResult = parse(recast(kclManager.ast))
if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(new Error('Unexpected compilation error'))
let parsed = pResult.program
let result: {
modifiedAst: Node<Program>
pathToReplaced: PathToNode | null
exprInsertIndex: number
} = {
modifiedAst: parsed,
pathToReplaced: null,
exprInsertIndex: -1,
}
// If the user provided a constant name,
// we need to insert the named constant
// and then replace the node with the constant's name.
if ('variableName' in data.namedValue) {
const astAfterReplacement = replaceValueAtNodePath({
ast: parsed,
pathToNode: data.currentValue.pathToNode,
newExpressionString: data.namedValue.variableName,
})
if (trap(astAfterReplacement)) {
return Promise.reject(astAfterReplacement)
}
const parseResultAfterInsertion = parse(
recast(
insertNamedConstant({
node: astAfterReplacement.modifiedAst,
newExpression: data.namedValue,
})
)
)
result.exprInsertIndex = data.namedValue.insertIndex
if (
trap(parseResultAfterInsertion) ||
!resultIsOk(parseResultAfterInsertion)
)
return Promise.reject(parseResultAfterInsertion)
result = {
modifiedAst: parseResultAfterInsertion.program,
pathToReplaced: astAfterReplacement.pathToReplaced,
exprInsertIndex: result.exprInsertIndex,
}
} else if ('valueText' in data.namedValue) {
// If they didn't provide a constant name,
// just replace the node with the value.
const astAfterReplacement = replaceValueAtNodePath({
ast: parsed,
pathToNode: data.currentValue.pathToNode,
newExpressionString: data.namedValue.valueText,
})
if (trap(astAfterReplacement)) {
return Promise.reject(astAfterReplacement)
}
// The `replacer` function returns a pathToNode that assumes
// an identifier is also being inserted into the AST, creating an off-by-one error.
// This corrects that error, but TODO we should fix this upstream
// to avoid this kind of error in the future.
astAfterReplacement.pathToReplaced[1][0] =
(astAfterReplacement.pathToReplaced[1][0] as number) - 1
result = astAfterReplacement
}
pResult = parse(recast(result.modifiedAst))
if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(new Error('Unexpected compilation error'))
parsed = pResult.program
if (trap(parsed)) return Promise.reject(parsed)
if (!result.pathToReplaced)
return Promise.reject(new Error('No path to replaced node'))
const {
updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
} = updateSketchDetailsNodePaths({
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
exprInsertIndex: result.exprInsertIndex,
})
const updatedAst =
await sceneEntitiesManager.updateAstAndRejigSketch(
updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
parsed,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin
)
if (err(updatedAst)) return Promise.reject(updatedAst)
await codeManager.updateEditorWithAstAndWriteToFile(
updatedAst.newAst
)
const selection = updateSelections(
{ 0: result.pathToReplaced },
selectionRanges,
updatedAst.newAst
)
if (err(selection)) return Promise.reject(selection)
return {
selectionType: 'completeSelection',
selection,
updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
}
}
),
'set-up-draft-circle': fromPromise(
async ({ input: { sketchDetails, data } }) => {
if (!sketchDetails || !data)
@ -1090,6 +1608,38 @@ export const ModelingMachineProvider = ({
return result
}
),
'setup-client-side-sketch-segments': fromPromise(
async ({ input: { sketchDetails, selectionRanges } }) => {
if (!sketchDetails) return
if (!sketchDetails.sketchEntryNodePath?.length) return
sceneInfra.resetMouseListeners()
await sceneEntitiesManager.setupSketch({
sketchEntryNodePath: sketchDetails?.sketchEntryNodePath || [],
sketchNodePaths: sketchDetails.sketchNodePaths,
forward: sketchDetails.zAxis,
up: sketchDetails.yAxis,
position: sketchDetails.origin,
maybeModdedAst: kclManager.ast,
selectionRanges,
})
sceneInfra.resetMouseListeners()
sceneEntitiesManager.setupSketchIdleCallbacks({
sketchEntryNodePath: sketchDetails?.sketchEntryNodePath || [],
forward: sketchDetails.zAxis,
up: sketchDetails.yAxis,
position: sketchDetails.origin,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
// We will want to pass sketchTools here
// to add their interactions
})
// We will want to update the context with sketchTools.
// They'll be used for their .destroy() in tearDownSketch
return undefined
}
),
'split-sketch-pipe-if-needed': fromPromise(
async ({ input: { sketchDetails } }) => {
if (!sketchDetails) return reject('No sketch details')

View File

@ -14,8 +14,13 @@ import { IS_ML_EXPERIMENTAL, PROJECT_ENTRYPOINT } from '@src/lib/constants'
import toast from 'react-hot-toast'
import { reportRejection } from '@src/lib/trap'
import { relevantFileExtensions } from '@src/lang/wasmUtils'
import { getStringAfterLastSeparator, webSafePathSplit } from '@src/lib/paths'
import {
getStringAfterLastSeparator,
joinOSPaths,
webSafePathSplit,
} from '@src/lib/paths'
import { FILE_EXT } from '@src/lib/constants'
import { getAllSubDirectoriesAtProjectRoot } from '@src/machines/systemIO/snapshotContext'
function onSubmitKCLSampleCreation({
sample,
@ -87,6 +92,26 @@ function onSubmitKCLSampleCreation({
},
})
} else {
/**
* When adding assemblies to an existing project create the assembly into a unique sub directory
*/
if (!isProjectNew) {
requestedFiles.forEach((requestedFile) => {
const subDirectoryName = projectPathPart
const firstLevelDirectories = getAllSubDirectoriesAtProjectRoot({
projectFolderName: requestedFile.requestedProjectName,
})
const uniqueSubDirectoryName = getUniqueProjectName(
subDirectoryName,
firstLevelDirectories
)
requestedFile.requestedProjectName = joinOSPaths(
requestedFile.requestedProjectName,
uniqueSubDirectoryName
)
})
}
/**
* Bulk create the assembly and navigate to the project
*/
@ -278,10 +303,9 @@ export function createApplicationCommands({
return value
},
options: ({ argumentsToSubmit }) => {
const samples =
isDesktop() && argumentsToSubmit.method !== 'existingProject'
? everyKclSample
: kclSamplesManifestWithNoMultipleFiles
const samples = isDesktop()
? everyKclSample
: kclSamplesManifestWithNoMultipleFiles
return samples.map((sample) => {
return {
value: sample.pathFromProjectDirectoryToFirstFile,
@ -296,17 +320,10 @@ export function createApplicationCommands({
skip: true,
options: ({ argumentsToSubmit }, _) => {
if (isDesktop() && typeof argumentsToSubmit.sample === 'string') {
const kclSample = findKclSample(argumentsToSubmit.sample)
if (kclSample && kclSample.files.length > 1) {
return [
{ name: 'New project', value: 'newProject', isCurrent: true },
]
} else {
return [
{ name: 'New project', value: 'newProject', isCurrent: true },
{ name: 'Existing project', value: 'existingProject' },
]
}
return [
{ name: 'New project', value: 'newProject', isCurrent: true },
{ name: 'Existing project', value: 'existingProject' },
]
} else {
return [{ name: 'Overwrite', value: 'existingProject' }]
}

View File

@ -28,6 +28,7 @@ import type { Selections } from '@src/lib/selections'
import { codeManager, kclManager } from '@src/lib/singletons'
import { err } from '@src/lib/trap'
import type { SketchTool, modelingMachine } from '@src/machines/modelingMachine'
import { isDesktop } from '../isDesktop'
type OutputFormat = Models['OutputFormat3d_type']
type OutputTypeKey = OutputFormat['type']
@ -159,6 +160,10 @@ export type ModelingCommandSchema = {
nodeToEdit?: PathToNode
color: string
}
Insert: {
path: string
localName: string
}
Translate: {
nodeToEdit?: PathToNode
selection: Selections
@ -1011,6 +1016,74 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
// Add more fields
},
},
Insert: {
description: 'Insert from a file in the current project directory',
icon: 'import',
hide: 'web',
needsReview: true,
args: {
path: {
inputType: 'options',
required: true,
// options
validation: async ({ data }) => {
const importExists = kclManager.ast.body.find(
(n) =>
n.type === 'ImportStatement' &&
((n.path.type === 'Kcl' && n.path.filename === data.path) ||
(n.path.type === 'Foreign' && n.path.path === data.path))
)
if (importExists) {
return 'This file is already imported, use the Clone command instead.'
// TODO: see if we can transition to the clone command, see #6515
}
return true
},
},
localName: {
inputType: 'string',
required: true,
defaultValue: (context: CommandBarContext) => {
if (!context.argumentsToSubmit['path']) {
return
}
const path = context.argumentsToSubmit['path'] as string
return getPathFilenameInVariableCase(path)
},
validation: async ({ data }) => {
const variableExists = kclManager.variables[data.localName]
if (variableExists) {
return 'This variable name is already in use.'
}
return true
},
},
},
// onSubmit: (data) => {
// if (!data) {
// return new Error('No input provided')
// }
// const ast = kclManager.ast
// const { path, localName } = data
// const { modifiedAst, pathToNode } = addModuleImport({
// ast,
// path,
// localName,
// })
// updateModelingState(
// modifiedAst,
// EXECUTION_TYPE_REAL,
// { kclManager, editorManager, codeManager },
// {
// focusPath: [pathToNode],
// }
// ).catch(reportRejection)
// },
},
Translate: {
description: 'Set translation on solid or sketch.',
icon: 'move',

View File

@ -89,76 +89,76 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] {
}
},
},
{
name: 'Insert',
description: 'Insert from a file in the current project directory',
icon: 'import',
groupId: 'code',
hide: 'web',
needsReview: true,
args: {
path: {
inputType: 'options',
required: true,
options: commandProps.specialPropsForInsertCommand.providedOptions,
validation: async ({ data }) => {
const importExists = kclManager.ast.body.find(
(n) =>
n.type === 'ImportStatement' &&
((n.path.type === 'Kcl' && n.path.filename === data.path) ||
(n.path.type === 'Foreign' && n.path.path === data.path))
)
if (importExists) {
return 'This file is already imported, use the Clone command instead.'
// TODO: see if we can transition to the clone command, see #6515
}
// {
// name: 'Insert',
// description: 'Insert from a file in the current project directory',
// icon: 'import',
// groupId: 'code',
// hide: 'web',
// needsReview: true,
// args: {
// path: {
// inputType: 'options',
// required: true,
// options: commandProps.specialPropsForInsertCommand.providedOptions,
// validation: async ({ data }) => {
// const importExists = kclManager.ast.body.find(
// (n) =>
// n.type === 'ImportStatement' &&
// ((n.path.type === 'Kcl' && n.path.filename === data.path) ||
// (n.path.type === 'Foreign' && n.path.path === data.path))
// )
// if (importExists) {
// return 'This file is already imported, use the Clone command instead.'
// // TODO: see if we can transition to the clone command, see #6515
// }
return true
},
},
localName: {
inputType: 'string',
required: true,
defaultValue: (context: CommandBarContext) => {
if (!context.argumentsToSubmit['path']) {
return
}
// return true
// },
// },
// localName: {
// inputType: 'string',
// required: true,
// defaultValue: (context: CommandBarContext) => {
// if (!context.argumentsToSubmit['path']) {
// return
// }
const path = context.argumentsToSubmit['path'] as string
return getPathFilenameInVariableCase(path)
},
validation: async ({ data }) => {
const variableExists = kclManager.variables[data.localName]
if (variableExists) {
return 'This variable name is already in use.'
}
// const path = context.argumentsToSubmit['path'] as string
// return getPathFilenameInVariableCase(path)
// },
// validation: async ({ data }) => {
// const variableExists = kclManager.variables[data.localName]
// if (variableExists) {
// return 'This variable name is already in use.'
// }
return true
},
},
},
onSubmit: (data) => {
if (!data) {
return new Error('No input provided')
}
// return true
// },
// },
// },
// onSubmit: (data) => {
// if (!data) {
// return new Error('No input provided')
// }
const ast = kclManager.ast
const { path, localName } = data
const { modifiedAst, pathToNode } = addModuleImport({
ast,
path,
localName,
})
updateModelingState(
modifiedAst,
EXECUTION_TYPE_REAL,
{ kclManager, editorManager, codeManager },
{
focusPath: [pathToNode],
}
).catch(reportRejection)
},
},
// const ast = kclManager.ast
// const { path, localName } = data
// const { modifiedAst, pathToNode } = addModuleImport({
// ast,
// path,
// localName,
// })
// updateModelingState(
// modifiedAst,
// EXECUTION_TYPE_REAL,
// { kclManager, editorManager, codeManager },
// {
// focusPath: [pathToNode],
// }
// ).catch(reportRejection)
// },
// },
{
name: 'format-code',
displayName: 'Format Code',

View File

@ -1,11 +1,6 @@
import { DEFAULT_DEFAULT_LENGTH_UNIT } from '@src/lib/constants'
import { isPlaywright } from '@src/lib/isPlaywright'
import {
engineCommandManager,
kclManager,
sceneInfra,
settingsActor,
} from '@src/lib/singletons'
import { engineCommandManager, kclManager } from '@src/lib/singletons'
import {
engineStreamZoomToFit,
engineViewIsometricWithoutGeometryPresent,
@ -27,15 +22,6 @@ export async function resetCameraPosition() {
if (isPlaywright()) {
await engineStreamZoomToFit({ engineCommandManager, padding })
} else {
// Get user camera projection
const cameraProjection =
settingsActor.getSnapshot().context.modeling.cameraProjection.current
// We need to keep the users projection setting when resetting their camera
if (cameraProjection === 'perspective') {
await sceneInfra.camControls.usePerspectiveCamera()
}
// If the scene is empty you cannot use view_isometric, it will not move the camera
if (kclManager.isAstBodyEmpty(kclManager.ast)) {
await engineViewIsometricWithoutGeometryPresent({
@ -43,7 +29,6 @@ export async function resetCameraPosition() {
unit:
kclManager.fileSettings.defaultLengthUnit ||
DEFAULT_DEFAULT_LENGTH_UNIT,
cameraProjection,
})
} else {
await engineViewIsometricWithGeometryPresent({

View File

@ -12,7 +12,6 @@ import type {
CameraViewState_type,
UnitLength_type,
} from '@kittycad/lib/dist/types/src/models'
import type { CameraProjectionType } from '@rust/kcl-lib/bindings/CameraProjectionType'
export const uuidv4 = v4
@ -623,11 +622,9 @@ export async function engineViewIsometricWithGeometryPresent({
export async function engineViewIsometricWithoutGeometryPresent({
engineCommandManager,
unit,
cameraProjection,
}: {
engineCommandManager: EngineCommandManager
unit?: UnitLength_type
cameraProjection: CameraProjectionType
}) {
// If you load an empty scene with any file unit it will have an eye offset of this
const MAGIC_ENGINE_EYE_OFFSET = 1378.0057
@ -647,8 +644,8 @@ export async function engineViewIsometricWithoutGeometryPresent({
eye_offset: MAGIC_ENGINE_EYE_OFFSET,
fov_y: 45,
ortho_scale_factor: 1.4063792,
is_ortho: cameraProjection !== 'perspective',
ortho_scale_enabled: cameraProjection !== 'perspective',
is_ortho: true,
ortho_scale_enabled: true,
world_coord_system: 'right_handed_up_z',
}
await engineCommandManager.sendSceneCommand({

View File

@ -1,245 +0,0 @@
import {
modelingMachine,
modelingMachineDefaultContext,
} from '@src/machines/modelingMachine'
import { createActor } from 'xstate'
import { vi } from 'vitest'
import { assertParse, type CallExpressionKw } from '@src/lang/wasm'
import { initPromise } from '@src/lang/wasmUtils'
import {
codeManager,
engineCommandManager,
kclManager,
} from '@src/lib/singletons'
import { VITE_KC_DEV_TOKEN } from '@src/env'
import { line } from '@src/lang/std/sketch'
import { getNodeFromPath } from '@src/lang/queryAst'
import type { Node } from '@rust/kcl-lib/bindings/Node'
import { err } from '@src/lib/trap'
import {
createIdentifier,
createLiteral,
createVariableDeclaration,
} from '@src/lang/create'
// Store original method to restore in afterAll
beforeAll(async () => {
await initPromise
// THESE TEST WILL FAIL without VITE_KC_DEV_TOKEN set in .env.development.local
await new Promise((resolve) => {
engineCommandManager.start({
token: VITE_KC_DEV_TOKEN,
width: 256,
height: 256,
setMediaStream: () => {},
setIsStreamReady: () => {},
callbackOnEngineLiteConnect: () => {
resolve(true)
},
})
})
}, 30_000)
afterAll(() => {
// Restore the original method
engineCommandManager.tearDown()
})
// Define mock implementations that will be referenced in vi.mock calls
vi.mock('@src/components/SetHorVertDistanceModal', () => ({
createInfoModal: vi.fn(() => ({
open: vi.fn().mockResolvedValue({
value: '10',
segName: 'test',
valueNode: {},
newVariableInsertIndex: 0,
sign: 1,
}),
})),
GetInfoModal: vi.fn(),
}))
vi.mock('@src/components/SetAngleLengthModal', () => ({
createSetAngleLengthModal: vi.fn(() => ({
open: vi.fn().mockResolvedValue({
value: '45',
segName: 'test',
valueNode: {},
newVariableInsertIndex: 0,
sign: 1,
}),
})),
SetAngleLengthModal: vi.fn(),
}))
// Add this function before the test cases
// Utility function to wait for a condition to be met
const waitForCondition = async (
condition: () => boolean,
timeout = 5000,
interval = 100
) => {
const startTime = Date.now()
while (Date.now() - startTime < timeout) {
try {
if (condition()) {
return true
}
} catch {
// Ignore errors, keep polling
}
// Wait for the next interval
await new Promise((resolve) => setTimeout(resolve, interval))
}
// Last attempt before failing
return condition()
}
describe('modelingMachine - XState', () => {
describe('when initialized', () => {
it('should start in the idle state', () => {
const actor = createActor(modelingMachine, {
input: modelingMachineDefaultContext,
}).start()
const state = actor.getSnapshot().value
// The machine should start in the idle state
expect(state).toEqual({ idle: 'hidePlanes' })
})
})
describe('when in sketch mode', () => {
it('should transition to sketch state when entering sketch mode', async () => {
const code = `sketch001 = startSketchOn(XZ)
profile001 = startProfile(sketch001, at = [2263.04, -2721.2])
|> line(end = [16.27, 73.81])
|> line(end = [75.72, 18.41])
`
const ast = assertParse(code)
await kclManager.executeAst({ ast })
expect(kclManager.errors).toEqual([])
const indexOfInterest = code.indexOf('[16.27, 73.81]')
// segment artifact with that source range
const artifact = [...kclManager.artifactGraph].find(
([_, artifact]) =>
artifact?.type === 'segment' &&
artifact.codeRef.range[0] <= indexOfInterest &&
indexOfInterest <= artifact.codeRef.range[1]
)?.[1]
if (!artifact || !('codeRef' in artifact)) {
throw new Error('Artifact not found or invalid artifact structure')
}
const actor = createActor(modelingMachine, {
input: modelingMachineDefaultContext,
}).start()
// Send event to transition to sketch mode
actor.send({
type: 'Set selection',
data: {
selectionType: 'mirrorCodeMirrorSelections',
selection: {
graphSelections: [
{
artifact: artifact,
codeRef: artifact.codeRef,
},
],
otherSelections: [],
},
},
})
actor.send({ type: 'Enter sketch' })
// Check that we're in the sketch state
let state = actor.getSnapshot()
expect(state.value).toBe('animating to existing sketch')
// wait for it to transition
await waitForCondition(() => {
const snapshot = actor.getSnapshot()
return snapshot.value !== 'animating to existing sketch'
}, 5000)
// After the condition is met, do the actual assertion
expect(actor.getSnapshot().value).toEqual({
Sketch: { SketchIdle: 'scene drawn' },
})
const getConstraintInfo = line.getConstraintInfo
const callExp = getNodeFromPath<Node<CallExpressionKw>>(
kclManager.ast,
artifact.codeRef.pathToNode,
'CallExpressionKw'
)
if (err(callExp)) {
throw new Error('Failed to get CallExpressionKw node')
}
const constraintInfo = getConstraintInfo(
callExp.node,
codeManager.code,
artifact.codeRef.pathToNode
)
const first = constraintInfo[0]
// Now that we're in sketchIdle state, test the "Constrain with named value" event
actor.send({
type: 'Constrain with named value',
data: {
currentValue: {
valueText: first.value,
pathToNode: first.pathToNode,
variableName: 'test_variable',
},
// Use type assertion to mock the complex type
namedValue: {
valueText: '20',
variableName: 'test_variable',
insertIndex: 0,
valueCalculated: '20',
variableDeclarationAst: createVariableDeclaration(
'test_variable',
createLiteral('20')
),
variableIdentifierAst: createIdentifier('test_variable') as any,
valueAst: createLiteral('20'),
},
},
})
// Wait for the state to change in response to the constraint
await waitForCondition(() => {
const snapshot = actor.getSnapshot()
// Check if we've transitioned to a different state
return (
JSON.stringify(snapshot.value) !==
JSON.stringify({
Sketch: { SketchIdle: 'set up segments' },
})
)
}, 5000)
await waitForCondition(() => {
const snapshot = actor.getSnapshot()
// Check if we've transitioned to a different state
return (
JSON.stringify(snapshot.value) !==
JSON.stringify({ Sketch: 'Converting to named value' })
)
}, 5000)
expect(codeManager.code).toContain('line(end = [test_variable,')
}, 10_000)
})
})

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,6 @@
import type { FileEntry } from '@src/lib/project'
import { systemIOActor } from '@src/lib/singletons'
import { isArray } from '@src/lib/utils'
export const folderSnapshot = () => {
const { folders } = systemIOActor.getSnapshot().context
@ -9,3 +11,48 @@ export const defaultProjectFolderNameSnapshot = () => {
const { defaultProjectFolderName } = systemIOActor.getSnapshot().context
return defaultProjectFolderName
}
/**
* From the application project directory go down to a project folder and list all the folders at that directory level
* application project directory: /home/documents/zoo-modeling-app-projects/
*
* /home/documents/zoo-modeling-app-projects/car-door/
* ├── handle
* ├── main.kcl
* └── window
*
* The two folders are handle and window
*
* @param {Object} params
* @param {string} params.projectFolderName - The name with no path information.
* @returns {FileEntry[]} An array of subdirectory names found at the root level of the specified project folder.
*/
export const getAllSubDirectoriesAtProjectRoot = ({
projectFolderName,
}: { projectFolderName: string }): FileEntry[] => {
const subDirectories: FileEntry[] = []
const { folders } = systemIOActor.getSnapshot().context
const projectFolder = folders.find((folder) => {
return folder.name === projectFolderName
})
// Find the subdirectories
if (projectFolder) {
// 1st level
const children = projectFolder.children
if (children) {
children.forEach((childFileOrDirectory) => {
// 2nd level
const secondLevelChild = childFileOrDirectory.children
// if secondLevelChild is null then it is a file
if (secondLevelChild && isArray(secondLevelChild)) {
// this is a directory!
subDirectories.push(childFileOrDirectory)
}
})
}
}
return subDirectories
}