#[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 indexmap::IndexMap;
use kcmc::{
each_cmd as mcmd,
length_unit::LengthUnit,
ok_response::OkModelingCmdResponse,
shared::Color,
websocket::{
BatchResponse, ModelingBatch, ModelingCmdReq, ModelingSessionData, OkWebSocketResponseData, WebSocketRequest,
WebSocketResponse,
},
ModelingCmd,
};
use kittycad_modeling_cmds as kcmc;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::{
errors::{KclError, KclErrorDetails},
execution::{DefaultPlanes, IdGenerator, Point3d},
SourceRange,
};
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();
}
#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub enum ExecutionKind {
#[default]
Normal,
Isolated,
}
impl ExecutionKind {
pub fn is_isolated(&self) -> bool {
matches!(self, ExecutionKind::Isolated)
}
}
#[async_trait::async_trait]
pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
fn batch(&self) -> Arc<Mutex<Vec<(WebSocketRequest, SourceRange)>>>;
fn batch_end(&self) -> Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, SourceRange)>>>;
fn execution_kind(&self) -> ExecutionKind;
fn replace_execution_kind(&self, execution_kind: ExecutionKind) -> ExecutionKind;
async fn default_planes(
&self,
id_generator: &mut IdGenerator,
_source_range: SourceRange,
) -> Result<DefaultPlanes, crate::errors::KclError>;
async fn clear_scene_post_hook(
&self,
id_generator: &mut IdGenerator,
source_range: SourceRange,
) -> Result<(), crate::errors::KclError>;
async fn inner_send_modeling_cmd(
&self,
id: uuid::Uuid,
source_range: SourceRange,
cmd: WebSocketRequest,
id_to_source_range: HashMap<uuid::Uuid, SourceRange>,
) -> Result<kcmc::websocket::WebSocketResponse, crate::errors::KclError>;
async fn clear_scene(
&self,
id_generator: &mut IdGenerator,
source_range: SourceRange,
) -> Result<(), crate::errors::KclError> {
self.batch_modeling_cmd(
uuid::Uuid::new_v4(),
source_range,
&ModelingCmd::SceneClearAll(mcmd::SceneClearAll {}),
)
.await?;
self.flush_batch(false, source_range).await?;
self.clear_scene_post_hook(id_generator, source_range).await?;
Ok(())
}
async fn set_edge_visibility(
&self,
visible: bool,
source_range: SourceRange,
) -> Result<(), crate::errors::KclError> {
self.batch_modeling_cmd(
uuid::Uuid::new_v4(),
source_range,
&ModelingCmd::from(mcmd::EdgeLinesVisible { hidden: !visible }),
)
.await?;
Ok(())
}
async fn set_units(
&self,
units: crate::UnitLength,
source_range: SourceRange,
) -> Result<(), crate::errors::KclError> {
self.batch_modeling_cmd(
uuid::Uuid::new_v4(),
source_range,
&ModelingCmd::from(mcmd::SetSceneUnits { unit: units.into() }),
)
.await?;
Ok(())
}
async fn reapply_settings(
&self,
settings: &crate::ExecutorSettings,
source_range: SourceRange,
) -> Result<(), crate::errors::KclError> {
self.set_edge_visibility(settings.highlight_edges, source_range).await?;
self.set_units(settings.units, source_range).await?;
self.modify_grid(!settings.show_grid, source_range).await?;
self.flush_batch(false, source_range).await?;
Ok(())
}
async fn batch_modeling_cmd(
&self,
id: uuid::Uuid,
source_range: SourceRange,
cmd: &ModelingCmd,
) -> Result<(), crate::errors::KclError> {
let execution_kind = self.execution_kind();
if execution_kind.is_isolated() {
return Err(KclError::Semantic(KclErrorDetails { message: "Cannot send modeling commands while importing. Wrap your code in a function if you want to import the file.".to_owned(), source_ranges: vec![source_range] }));
}
let req = WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
cmd: cmd.clone(),
cmd_id: id.into(),
});
self.batch().lock().unwrap().push((req, source_range));
Ok(())
}
async fn batch_end_cmd(
&self,
id: uuid::Uuid,
source_range: SourceRange,
cmd: &ModelingCmd,
) -> Result<(), crate::errors::KclError> {
let req = WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
cmd: cmd.clone(),
cmd_id: id.into(),
});
self.batch_end().lock().unwrap().insert(id, (req, source_range));
Ok(())
}
async fn send_modeling_cmd(
&self,
id: uuid::Uuid,
source_range: SourceRange,
cmd: ModelingCmd,
) -> Result<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: SourceRange,
) -> Result<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: OkModelingCmdResponse::Empty {},
});
}
let requests: Vec<ModelingCmdReq> = all_requests
.iter()
.filter_map(|(val, _)| match val {
WebSocketRequest::ModelingCmdReq(ModelingCmdReq { cmd, cmd_id }) => Some(ModelingCmdReq {
cmd: cmd.clone(),
cmd_id: *cmd_id,
}),
_ => None,
})
.collect();
let batched_requests = WebSocketRequest::ModelingCmdBatchReq(ModelingBatch {
requests,
batch_id: uuid::Uuid::new_v4().into(),
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 = HashMap::new();
for (req, range) in all_requests.iter() {
match req {
WebSocketRequest::ModelingCmdReq(ModelingCmdReq { cmd: _, cmd_id }) => {
id_to_source_range.insert(Uuid::from(*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(ModelingBatch {
ref requests,
batch_id,
responses: _,
}) => {
let last_id = requests.last().unwrap().cmd_id;
let ws_resp = self
.inner_send_modeling_cmd(batch_id.into(), source_range, final_req, id_to_source_range.clone())
.await?;
let response = self.parse_websocket_response(ws_resp, source_range)?;
if let OkWebSocketResponseData::ModelingBatch { responses } = response {
let responses = responses.into_iter().map(|(k, v)| (Uuid::from(k), v)).collect();
self.parse_batch_responses(last_id.into(), id_to_source_range, responses)
} else {
Err(KclError::Engine(KclErrorDetails {
message: format!("Failed to get batch response: {:?}", response),
source_ranges: vec![source_range],
}))
}
}
WebSocketRequest::ModelingCmdReq(ModelingCmdReq { cmd: _, cmd_id }) => {
let source_range = id_to_source_range.get(cmd_id.as_ref()).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.into(), 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,
plane_id: uuid::Uuid,
x_axis: Point3d,
y_axis: Point3d,
color: Option<Color>,
source_range: SourceRange,
) -> Result<uuid::Uuid, KclError> {
let default_size = 100.0;
let default_origin = Point3d { x: 0.0, y: 0.0, z: 0.0 }.into();
self.batch_modeling_cmd(
plane_id,
source_range,
&ModelingCmd::from(mcmd::MakePlane {
clobber: false,
origin: default_origin,
size: LengthUnit(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::from(mcmd::PlaneSetColor { color, plane_id }),
)
.await?;
}
Ok(plane_id)
}
async fn new_default_planes(
&self,
id_generator: &mut IdGenerator,
source_range: SourceRange,
) -> Result<DefaultPlanes, KclError> {
let plane_settings: Vec<(PlaneName, Uuid, Point3d, Point3d, Option<Color>)> = vec![
(
PlaneName::Xy,
id_generator.next_uuid(),
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,
id_generator.next_uuid(),
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,
id_generator.next_uuid(),
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,
id_generator.next_uuid(),
Point3d {
x: -1.0,
y: 0.0,
z: 0.0,
},
Point3d { x: 0.0, y: 1.0, z: 0.0 },
None,
),
(
PlaneName::NegYz,
id_generator.next_uuid(),
Point3d {
x: 0.0,
y: -1.0,
z: 0.0,
},
Point3d { x: 0.0, y: 0.0, z: 1.0 },
None,
),
(
PlaneName::NegXz,
id_generator.next_uuid(),
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, plane_id, x_axis, y_axis, color) in plane_settings {
planes.insert(
name,
self.make_default_plane(plane_id, 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: WebSocketResponse,
source_range: SourceRange,
) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
match response {
WebSocketResponse::Success(success) => Ok(success.resp),
WebSocketResponse::Failure(fail) => {
let _request_id = fail.request_id;
Err(KclError::Engine(KclErrorDetails {
message: format!("Modeling command failed: {:?}", fail.errors),
source_ranges: vec![source_range],
}))
}
}
}
fn parse_batch_responses(
&self,
id: uuid::Uuid,
id_to_source_range: HashMap<uuid::Uuid, SourceRange>,
responses: HashMap<uuid::Uuid, BatchResponse>,
) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
#[expect(
clippy::iter_over_hash_type,
reason = "modeling command uses a HashMap and keys are random, so we don't really have a choice"
)]
for (cmd_id, resp) in responses.iter() {
match resp {
BatchResponse::Success { response } => {
if cmd_id == &id {
return Ok(OkWebSocketResponseData::Modeling {
modeling_response: response.clone(),
});
} else {
continue;
}
}
BatchResponse::Failure { errors } => {
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],
}));
}
}
}
Err(KclError::Engine(KclErrorDetails {
message: format!("Failed to find response for command ID: {:?}", id),
source_ranges: vec![],
}))
}
async fn modify_grid(&self, hidden: bool, source_range: SourceRange) -> Result<(), KclError> {
self.batch_modeling_cmd(
uuid::Uuid::new_v4(),
source_range,
&ModelingCmd::from(mcmd::ObjectVisible {
hidden,
object_id: *GRID_OBJECT_ID,
}),
)
.await?;
self.batch_modeling_cmd(
uuid::Uuid::new_v4(),
source_range,
&ModelingCmd::from(mcmd::ObjectVisible {
hidden,
object_id: *GRID_SCALE_TEXT_OBJECT_ID,
}),
)
.await?;
Ok(())
}
fn get_session_data(&self) -> Option<ModelingSessionData> {
None
}
}
#[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,
}