#[cfg(not(target_arch = "wasm32"))]
#[cfg(feature = "engine")]
pub mod conn;
pub mod conn_mock;
#[cfg(target_arch = "wasm32")]
#[cfg(feature = "engine")]
pub mod conn_wasm;
use std::{
collections::HashMap,
sync::{Arc, Mutex},
};
use kittycad::types::{Color, ModelingCmd, ModelingCmdReq, OkWebSocketResponseData, WebSocketRequest};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
errors::{KclError, KclErrorDetails},
executor::{DefaultPlanes, Point3d},
};
lazy_static::lazy_static! {
pub static ref GRID_OBJECT_ID: uuid::Uuid = uuid::Uuid::parse_str("cfa78409-653d-4c26-96f1-7c45fb784840").unwrap();
pub static ref GRID_SCALE_TEXT_OBJECT_ID: uuid::Uuid = uuid::Uuid::parse_str("10782f33-f588-4668-8bcd-040502d26590").unwrap();
}
#[async_trait::async_trait]
pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
fn batch(&self) -> Arc<Mutex<Vec<(kittycad::types::WebSocketRequest, crate::executor::SourceRange)>>>;
fn batch_end(
&self,
) -> Arc<Mutex<HashMap<uuid::Uuid, (kittycad::types::WebSocketRequest, crate::executor::SourceRange)>>>;
async fn default_planes(
&self,
_source_range: crate::executor::SourceRange,
) -> Result<DefaultPlanes, crate::errors::KclError>;
async fn clear_scene_post_hook(
&self,
source_range: crate::executor::SourceRange,
) -> Result<(), crate::errors::KclError>;
async fn inner_send_modeling_cmd(
&self,
id: uuid::Uuid,
source_range: crate::executor::SourceRange,
cmd: kittycad::types::WebSocketRequest,
id_to_source_range: std::collections::HashMap<uuid::Uuid, crate::executor::SourceRange>,
) -> Result<kittycad::types::WebSocketResponse, crate::errors::KclError>;
async fn clear_scene(&self, source_range: crate::executor::SourceRange) -> Result<(), crate::errors::KclError> {
self.batch_modeling_cmd(
uuid::Uuid::new_v4(),
source_range,
&kittycad::types::ModelingCmd::SceneClearAll {},
)
.await?;
self.flush_batch(false, source_range).await?;
self.clear_scene_post_hook(source_range).await?;
Ok(())
}
async fn batch_modeling_cmd(
&self,
id: uuid::Uuid,
source_range: crate::executor::SourceRange,
cmd: &kittycad::types::ModelingCmd,
) -> Result<(), crate::errors::KclError> {
let req = WebSocketRequest::ModelingCmdReq {
cmd: cmd.clone(),
cmd_id: id,
};
self.batch().lock().unwrap().push((req, source_range));
Ok(())
}
async fn batch_end_cmd(
&self,
id: uuid::Uuid,
source_range: crate::executor::SourceRange,
cmd: &kittycad::types::ModelingCmd,
) -> Result<(), crate::errors::KclError> {
let req = WebSocketRequest::ModelingCmdReq {
cmd: cmd.clone(),
cmd_id: id,
};
self.batch_end().lock().unwrap().insert(id, (req, source_range));
Ok(())
}
async fn send_modeling_cmd(
&self,
id: uuid::Uuid,
source_range: crate::executor::SourceRange,
cmd: kittycad::types::ModelingCmd,
) -> Result<kittycad::types::OkWebSocketResponseData, crate::errors::KclError> {
self.batch_modeling_cmd(id, source_range, &cmd).await?;
self.flush_batch(false, source_range).await
}
async fn flush_batch(
&self,
batch_end: bool,
source_range: crate::executor::SourceRange,
) -> Result<kittycad::types::OkWebSocketResponseData, crate::errors::KclError> {
let all_requests = if batch_end {
let mut requests = self.batch().lock().unwrap().clone();
requests.extend(self.batch_end().lock().unwrap().values().cloned());
requests
} else {
self.batch().lock().unwrap().clone()
};
if all_requests.is_empty() {
return Ok(OkWebSocketResponseData::Modeling {
modeling_response: kittycad::types::OkModelingCmdResponse::Empty {},
});
}
let requests: Vec<ModelingCmdReq> = all_requests
.iter()
.filter_map(|(val, _)| match val {
WebSocketRequest::ModelingCmdReq { cmd, cmd_id } => Some(kittycad::types::ModelingCmdReq {
cmd: cmd.clone(),
cmd_id: *cmd_id,
}),
_ => None,
})
.collect();
let batched_requests = WebSocketRequest::ModelingCmdBatchReq {
requests,
batch_id: uuid::Uuid::new_v4(),
responses: true,
};
let final_req = if all_requests.len() == 1 {
all_requests.first().unwrap().0.clone()
} else {
batched_requests
};
let mut id_to_source_range = std::collections::HashMap::new();
for (req, range) in all_requests.iter() {
match req {
WebSocketRequest::ModelingCmdReq { cmd: _, cmd_id } => {
id_to_source_range.insert(*cmd_id, *range);
}
_ => {
return Err(KclError::Engine(KclErrorDetails {
message: format!("The request is not a modeling command: {:?}", req),
source_ranges: vec![*range],
}));
}
}
}
self.batch().lock().unwrap().clear();
if batch_end {
self.batch_end().lock().unwrap().clear();
}
match final_req {
WebSocketRequest::ModelingCmdBatchReq {
ref requests,
batch_id,
responses: _,
} => {
let last_id = requests.last().unwrap().cmd_id;
let ws_resp = self
.inner_send_modeling_cmd(batch_id, source_range, final_req, id_to_source_range.clone())
.await?;
let response = self.parse_websocket_response(ws_resp, source_range)?;
if let kittycad::types::OkWebSocketResponseData::ModelingBatch { responses } = &response {
self.parse_batch_responses(last_id, id_to_source_range, responses.clone())
} else {
Err(KclError::Engine(KclErrorDetails {
message: format!("Failed to get batch response: {:?}", response),
source_ranges: vec![source_range],
}))
}
}
WebSocketRequest::ModelingCmdReq { cmd: _, cmd_id } => {
let source_range = id_to_source_range.get(&cmd_id).cloned().ok_or_else(|| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to get source range for command ID: {:?}", cmd_id),
source_ranges: vec![],
})
})?;
let ws_resp = self
.inner_send_modeling_cmd(cmd_id, source_range, final_req, id_to_source_range)
.await?;
self.parse_websocket_response(ws_resp, source_range)
}
_ => Err(KclError::Engine(KclErrorDetails {
message: format!("The final request is not a modeling command: {:?}", final_req),
source_ranges: vec![source_range],
})),
}
}
async fn make_default_plane(
&self,
x_axis: Point3d,
y_axis: Point3d,
color: Option<Color>,
source_range: crate::executor::SourceRange,
) -> Result<uuid::Uuid, KclError> {
let default_size = 100.0;
let default_origin = Point3d { x: 0.0, y: 0.0, z: 0.0 }.into();
let plane_id = uuid::Uuid::new_v4();
self.batch_modeling_cmd(
plane_id,
source_range,
&ModelingCmd::MakePlane {
clobber: false,
origin: default_origin,
size: default_size,
x_axis: x_axis.into(),
y_axis: y_axis.into(),
hide: Some(true),
},
)
.await?;
if let Some(color) = color {
self.batch_modeling_cmd(
uuid::Uuid::new_v4(),
source_range,
&ModelingCmd::PlaneSetColor { color, plane_id },
)
.await?;
}
Ok(plane_id)
}
async fn new_default_planes(&self, source_range: crate::executor::SourceRange) -> Result<DefaultPlanes, KclError> {
let plane_settings: HashMap<PlaneName, (Point3d, Point3d, Option<Color>)> = HashMap::from([
(
PlaneName::Xy,
(
Point3d { x: 1.0, y: 0.0, z: 0.0 },
Point3d { x: 0.0, y: 1.0, z: 0.0 },
Some(Color {
r: 0.7,
g: 0.28,
b: 0.28,
a: 0.4,
}),
),
),
(
PlaneName::Yz,
(
Point3d { x: 0.0, y: 1.0, z: 0.0 },
Point3d { x: 0.0, y: 0.0, z: 1.0 },
Some(Color {
r: 0.28,
g: 0.7,
b: 0.28,
a: 0.4,
}),
),
),
(
PlaneName::Xz,
(
Point3d { x: 1.0, y: 0.0, z: 0.0 },
Point3d { x: 0.0, y: 0.0, z: 1.0 },
Some(Color {
r: 0.28,
g: 0.28,
b: 0.7,
a: 0.4,
}),
),
),
(
PlaneName::NegXy,
(
Point3d {
x: -1.0,
y: 0.0,
z: 0.0,
},
Point3d { x: 0.0, y: 1.0, z: 0.0 },
None,
),
),
(
PlaneName::NegYz,
(
Point3d {
x: 0.0,
y: -1.0,
z: 0.0,
},
Point3d { x: 0.0, y: 0.0, z: 1.0 },
None,
),
),
(
PlaneName::NegXz,
(
Point3d {
x: -1.0,
y: 0.0,
z: 0.0,
},
Point3d { x: 0.0, y: 0.0, z: 1.0 },
None,
),
),
]);
let mut planes = HashMap::new();
for (name, (x_axis, y_axis, color)) in plane_settings {
planes.insert(
name,
self.make_default_plane(x_axis, y_axis, color, source_range).await?,
);
}
self.flush_batch(false, source_range).await?;
Ok(DefaultPlanes {
xy: planes[&PlaneName::Xy],
neg_xy: planes[&PlaneName::NegXy],
xz: planes[&PlaneName::Xz],
neg_xz: planes[&PlaneName::NegXz],
yz: planes[&PlaneName::Yz],
neg_yz: planes[&PlaneName::NegYz],
})
}
fn parse_websocket_response(
&self,
response: kittycad::types::WebSocketResponse,
source_range: crate::executor::SourceRange,
) -> Result<kittycad::types::OkWebSocketResponseData, crate::errors::KclError> {
if let Some(data) = &response.resp {
Ok(data.clone())
} else if let Some(errors) = &response.errors {
Err(KclError::Engine(KclErrorDetails {
message: format!("Modeling command failed: {:?}", errors),
source_ranges: vec![source_range],
}))
} else {
Err(KclError::Engine(KclErrorDetails {
message: "Modeling command failed: no response or errors".to_string(),
source_ranges: vec![source_range],
}))
}
}
fn parse_batch_responses(
&self,
id: uuid::Uuid,
id_to_source_range: std::collections::HashMap<uuid::Uuid, crate::executor::SourceRange>,
responses: HashMap<String, kittycad::types::BatchResponse>,
) -> Result<kittycad::types::OkWebSocketResponseData, crate::errors::KclError> {
for (cmd_id, resp) in responses.iter() {
let cmd_id = uuid::Uuid::parse_str(cmd_id).map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to parse command ID: {:?}", e),
source_ranges: vec![id_to_source_range[&id]],
})
})?;
if let Some(errors) = resp.errors.as_ref() {
let source_range = id_to_source_range.get(&cmd_id).cloned().ok_or_else(|| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to get source range for command ID: {:?}", cmd_id),
source_ranges: vec![],
})
})?;
return Err(KclError::Engine(KclErrorDetails {
message: format!("Modeling command failed: {:?}", errors),
source_ranges: vec![source_range],
}));
}
if let Some(response) = resp.response.as_ref() {
if cmd_id == id {
return Ok(kittycad::types::OkWebSocketResponseData::Modeling {
modeling_response: response.clone(),
});
} else {
continue;
}
}
}
Err(KclError::Engine(KclErrorDetails {
message: format!("Failed to find response for command ID: {:?}", id),
source_ranges: vec![],
}))
}
async fn modify_grid(&self, hidden: bool) -> Result<(), KclError> {
self.batch_modeling_cmd(
uuid::Uuid::new_v4(),
Default::default(),
&ModelingCmd::ObjectVisible {
hidden,
object_id: *GRID_OBJECT_ID,
},
)
.await?;
self.batch_modeling_cmd(
uuid::Uuid::new_v4(),
Default::default(),
&ModelingCmd::ObjectVisible {
hidden,
object_id: *GRID_SCALE_TEXT_OBJECT_ID,
},
)
.await?;
self.flush_batch(false, Default::default()).await?;
Ok(())
}
}
#[derive(Debug, Hash, Eq, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub enum PlaneName {
Xy,
NegXy,
Xz,
NegXz,
Yz,
NegYz,
}