diff --git a/Cargo.lock b/Cargo.lock index d2ac59f1..81901811 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1617,7 +1617,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2541,7 +2541,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2718,8 +2718,7 @@ dependencies = [ [[package]] name = "kcl-api" version = "0.2.192" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f686362cc0bfa733c00a3ba587a922a1e4e2e86c70c88fc862b4be293ecd592e" +source = "git+https://github.com/KittyCAD/modeling-app?branch=achalmers/zk-exec-remote-2#f0c49d1f25bdea90ce3ef01a22de65c7f4ef9232" dependencies = [ "arbitrary", "euler", @@ -2734,14 +2733,14 @@ dependencies = [ "serde", "serde_json", "ts-rs", + "typed-path", "uuid", ] [[package]] name = "kcl-error" version = "0.2.192" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb3b814c9588b41bee841bf292fa63cd2367384e3ccf056199b464e19be217f5" +source = "git+https://github.com/KittyCAD/modeling-app?branch=achalmers/zk-exec-remote-2#f0c49d1f25bdea90ce3ef01a22de65c7f4ef9232" dependencies = [ "miette", "schemars 0.8.22", @@ -4539,7 +4538,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4607,7 +4606,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -5216,7 +5215,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -5449,7 +5448,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix 1.1.2", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -5458,7 +5457,7 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2111ef44dae28680ae9752bb89409e7310ca33a8c621ebe7b106cf5c928b3ac0" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -6579,7 +6578,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 741f4adb..c3fd88da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,8 +27,8 @@ syn = "3.0.3" [patch.crates-io] kittycad-unit-conversion-derive = { path = "unit-conversion-derive" } -# kcl-api = { git = "https://github.com/KittyCAD/modeling-app", branch = "main" } -# kcl-error = { git = "https://github.com/KittyCAD/modeling-app", branch = "main" } +kcl-api = { git = "https://github.com/KittyCAD/modeling-app", branch = "achalmers/zk-exec-remote-2" } +kcl-error = { git = "https://github.com/KittyCAD/modeling-app", branch = "achalmers/zk-exec-remote-2" } [profile.bench] debug = true diff --git a/modeling-cmds/Cargo.toml b/modeling-cmds/Cargo.toml index cfa1e1e3..cafb0437 100644 --- a/modeling-cmds/Cargo.toml +++ b/modeling-cmds/Cargo.toml @@ -25,7 +25,7 @@ webrtc = ["dep:webrtc"] unstable_exhaustive = [] python = ["dep:pyo3", "dep:pyo3-stub-gen"] arbitrary = ["dep:arbitrary", "uuid/arbitrary", "chrono/arbitrary", "kcl-api?/arbitrary", "kittycad-point/arbitrary"] -exec-kcl = ["dep:kcl-error", "dep:kcl-api"] +exec-kcl = ["dep:kcl-error", "dep:kcl-api", "dep:serde_json"] [dependencies] anyhow = "1.0.101" diff --git a/modeling-cmds/openapi/api.json b/modeling-cmds/openapi/api.json index 2529ea6d..41bf09cb 100644 --- a/modeling-cmds/openapi/api.json +++ b/modeling-cmds/openapi/api.json @@ -2577,11 +2577,11 @@ ] }, "KclFile": { - "description": "Region-creation algorithm version.", + "description": "A file uploaded with a KCL project.", "type": "object", "properties": { "contents": { - "description": "Contents of the file, as UTF-8 encoded bytes.", + "description": "Raw file contents. KCL source is UTF-8; imported assets may be binary.", "type": "array", "items": { "type": "integer", @@ -2589,6 +2589,11 @@ "minimum": 0 } }, + "original_path": { + "nullable": true, + "description": "Original client path, used only as a name in the uploaded in-memory filesystem. This can be absolute or contain parent components; it never grants access to the engine's filesystem. Preserves import resolution and diagnostic names.", + "type": "string" + }, "path": { "description": "Where is the file, relative to the project directory?", "allOf": [ @@ -2621,6 +2626,11 @@ "items": { "$ref": "#/components/schemas/KclFile" } + }, + "include_execution_data": { + "description": "Include interpreter data needed for saved diagnostics and sketch rendering. Clients requesting this must verify the response contains execution_data.", + "default": false, + "type": "boolean" } }, "required": [ diff --git a/modeling-cmds/src/exec_kcl.rs b/modeling-cmds/src/exec_kcl.rs index 8adedf8f..c9ee678f 100644 --- a/modeling-cmds/src/exec_kcl.rs +++ b/modeling-cmds/src/exec_kcl.rs @@ -2,7 +2,7 @@ use bon::Builder; use indexmap::IndexMap; #[cfg(feature = "websocket")] use kcl_api::artifact::ArtifactGraph; -use kcl_api::{kcl_value_view::KclValueView, DefaultPlanes}; +use kcl_api::{kcl_value_view::KclValueView, DefaultPlanes, Object}; use kcl_error::{CompilationIssue, KclError}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -22,16 +22,25 @@ pub struct KclProject { /// This is the first KCL file to be executed, /// the root of the KCL module tree. pub entrypoint: SafeFilepath, + /// Include interpreter data needed for saved diagnostics and sketch rendering. + /// Clients requesting this must verify the response contains execution_data. + #[serde(default)] + #[builder(default)] + pub include_execution_data: bool, } impl KclProject { /// Create a new KCL project. pub fn new(files: Vec, entrypoint: SafeFilepath) -> Self { - Self { files, entrypoint } + Self { + files, + entrypoint, + include_execution_data: false, + } } } -/// Region-creation algorithm version. +/// A file uploaded with a KCL project. #[derive(Clone, PartialEq, Serialize, Deserialize, JsonSchema, Default, Builder)] #[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] @@ -40,12 +49,18 @@ impl KclProject { pub struct KclFile { /// Where is the file, relative to the project directory? pub path: SafeFilepath, - /// Contents of the file, as UTF-8 encoded bytes. + /// Raw file contents. KCL source is UTF-8; imported assets may be binary. #[serde( serialize_with = "serde_bytes::serialize", deserialize_with = "serde_bytes::deserialize" )] pub contents: Vec, + /// Original client path, used only as a name in the uploaded in-memory filesystem. + /// This can be absolute or contain parent components; it never grants access + /// to the engine's filesystem. Preserves import resolution and diagnostic names. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "ts-rs", ts(optional))] + pub original_path: Option, } impl std::fmt::Debug for KclFile { @@ -60,7 +75,11 @@ impl std::fmt::Debug for KclFile { impl KclFile { /// Create a KCL file. pub fn new(path: SafeFilepath, contents: Vec) -> Self { - Self { path, contents } + Self { + path, + contents, + original_path: None, + } } } @@ -83,6 +102,12 @@ pub struct ExecKclProjectOk { pub issues: Vec, /// IDs of the standard planes created for this execution. pub default_planes: Option>, + /// Interpreter-specific data for saved source diagnostics and sketch rendering. + /// Contains solved scene objects and module sources; its schema follows the deployed + /// interpreter release, just like KCL execution semantics. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "ts-rs", ts(optional))] + pub execution_data: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Builder)] @@ -106,22 +131,16 @@ pub struct ExecKclProjectErr { pub artifact_graph: ArtifactGraph, /// IDs of the standard planes created before execution failed. pub default_planes: Option>, - // TODO: Add fields to this as we make KCL data serializable. - // Should be a usable subset of `KclErrorWithOutputs`. -} - -impl ExecKclProjectErr { - /// Used when the project execution had a fatal error. - pub fn fatal_error(error: KclError) -> Self { - Self { - error: Some(error), - non_fatal: Default::default(), - operations: Default::default(), - variables: Default::default(), - artifact_graph: Default::default(), - default_planes: Default::default(), - } - } + /// Interpreter-specific data for saved source diagnostics and sketch rendering. + /// Contains solved scene objects and module sources; its schema follows the deployed + /// interpreter release, just like KCL execution semantics. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "ts-rs", ts(optional))] + pub execution_data: Option, + /// All objects in all sketches. + pub scene_objects: Vec, + /// Which module ID was assigned to each KCL file in the project? + pub filenames: IndexMap, } #[cfg(feature = "arbitrary")] @@ -137,6 +156,7 @@ impl<'a> arbitrary::Arbitrary<'a> for ExecKclProjectOk { #[cfg(feature = "websocket")] variables: Default::default(), default_planes: Default::default(), + execution_data: None, }) } } @@ -151,6 +171,62 @@ impl<'a> arbitrary::Arbitrary<'a> for ExecKclProjectErr { variables: Default::default(), artifact_graph: Default::default(), default_planes: Default::default(), + scene_objects: Default::default(), + filenames: Default::default(), + execution_data: None, }) } } + +#[cfg(all(test, feature = "websocket"))] +mod tests { + use super::*; + + #[test] + fn existing_projects_deserialize_without_native_metadata() { + let project: KclProject = serde_json::from_value(serde_json::json!({ + "files": [{"path": "main.kcl", "contents": [120]}], + "entrypoint": "main.kcl" + })) + .unwrap(); + assert!(!project.include_execution_data); + assert!(project.files[0].original_path.is_none()); + assert_eq!(project.files[0].contents, b"x"); + } + + #[test] + fn native_project_metadata_and_binary_assets_round_trip() { + let mut file = KclFile::new(SafeFilepath::validate("files/0").unwrap(), vec![0, 255, 128]); + file.original_path = Some("/client/project/../part.step".to_owned()); + let mut project = KclProject::new(vec![file], SafeFilepath::validate("files/0").unwrap()); + project.include_execution_data = true; + let saved: KclProject = serde_json::from_slice(&serde_json::to_vec(&project).unwrap()).unwrap(); + assert_eq!(saved, project); + } + + #[test] + fn execution_data_is_optional_on_success_and_failure_responses() { + let success = ExecKclProjectOk::builder() + .artifact_graph(Default::default()) + .operations(Default::default()) + .variables(Default::default()) + .issues(vec![]) + .build(); + let success_json = serde_json::to_value(success).unwrap(); + assert!(!success_json.as_object().unwrap().contains_key("execution_data")); + let success: ExecKclProjectOk = serde_json::from_value(success_json).unwrap(); + assert!(success.execution_data.is_none()); + let failure = ExecKclProjectErr::builder() + .artifact_graph(Default::default()) + .operations(Default::default()) + .variables(Default::default()) + .non_fatal(vec![]) + .filenames(Default::default()) + .scene_objects(Default::default()) + .build(); + let failure_json = serde_json::to_value(failure).unwrap(); + assert!(!failure_json.as_object().unwrap().contains_key("execution_data")); + let failure: ExecKclProjectErr = serde_json::from_value(failure_json).unwrap(); + assert!(failure.execution_data.is_none()); + } +} diff --git a/modeling-cmds/src/websocket.rs b/modeling-cmds/src/websocket.rs index b22f023a..250b6420 100644 --- a/modeling-cmds/src/websocket.rs +++ b/modeling-cmds/src/websocket.rs @@ -238,7 +238,7 @@ pub enum OkWebSocketResponseData { #[cfg(feature = "exec-kcl")] ExecKclProject { /// Result after executing KCL. - result: Result, + result: Box>, }, /// Request that the client end this connection and establish a new session