Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 11 additions & 12 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion modeling-cmds/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
14 changes: 12 additions & 2 deletions modeling-cmds/openapi/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -2577,18 +2577,23 @@
]
},
"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",
"format": "uint8",
"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": [
Expand Down Expand Up @@ -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": [
Expand Down
118 changes: 97 additions & 21 deletions modeling-cmds/src/exec_kcl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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<KclFile>, 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))]
Expand All @@ -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<u8>,
/// 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<String>,
}

impl std::fmt::Debug for KclFile {
Expand All @@ -60,7 +75,11 @@ impl std::fmt::Debug for KclFile {
impl KclFile {
/// Create a KCL file.
pub fn new(path: SafeFilepath, contents: Vec<u8>) -> Self {
Self { path, contents }
Self {
path,
contents,
original_path: None,
}
}
}

Expand All @@ -83,6 +102,12 @@ pub struct ExecKclProjectOk {
pub issues: Vec<CompilationIssue>,
/// IDs of the standard planes created for this execution.
pub default_planes: Option<Box<DefaultPlanes>>,
/// 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<serde_json::Value>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Builder)]
Expand All @@ -106,22 +131,16 @@ pub struct ExecKclProjectErr {
pub artifact_graph: ArtifactGraph,
/// IDs of the standard planes created before execution failed.
pub default_planes: Option<Box<DefaultPlanes>>,
// 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<serde_json::Value>,
/// All objects in all sketches.
pub scene_objects: Vec<Object>,
/// Which module ID was assigned to each KCL file in the project?
pub filenames: IndexMap<kcl_error::ModuleId, kcl_api::modules::ModulePath>,
}

#[cfg(feature = "arbitrary")]
Expand All @@ -137,6 +156,7 @@ impl<'a> arbitrary::Arbitrary<'a> for ExecKclProjectOk {
#[cfg(feature = "websocket")]
variables: Default::default(),
default_planes: Default::default(),
execution_data: None,
})
}
}
Expand All @@ -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());
}
}
2 changes: 1 addition & 1 deletion modeling-cmds/src/websocket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ pub enum OkWebSocketResponseData {
#[cfg(feature = "exec-kcl")]
ExecKclProject {
/// Result after executing KCL.
result: Result<crate::exec_kcl::ExecKclProjectOk, crate::exec_kcl::ExecKclProjectErr>,
result: Box<Result<crate::exec_kcl::ExecKclProjectOk, crate::exec_kcl::ExecKclProjectErr>>,
},

/// Request that the client end this connection and establish a new session
Expand Down
Loading