1use std::collections::HashMap;
2use std::sync::Arc;
3use std::sync::atomic::Ordering::Relaxed;
4
5use anyhow::Result;
6pub use engine_transport::EngineTransport;
7use indexmap::IndexMap;
8use kcmc::ModelingCmd;
9use kcmc::each_cmd as mcmd;
10use kcmc::shared::Color;
11use kcmc::websocket::BatchResponse;
12use kcmc::websocket::ModelingCmdReq;
13use kcmc::websocket::ModelingSessionData;
14use kcmc::websocket::OkWebSocketResponseData;
15use kcmc::websocket::WebSocketRequest;
16use kcmc::websocket::WebSocketResponse;
17use kittycad_modeling_cmds::ModelingCmdEndpoint;
18use kittycad_modeling_cmds::length_unit::LengthUnit;
19use kittycad_modeling_cmds::ok_response::OkModelingCmdResponse;
20use kittycad_modeling_cmds::websocket::ModelingBatch;
21use kittycad_modeling_cmds::{self as kcmc};
22use tokio::sync::RwLock;
23use uuid::Uuid;
24use web_time::Instant;
25
26use crate::ExecutorSettings;
27use crate::SourceRange;
28use crate::engine::AsyncTasks;
29use crate::engine::DEFAULT_PLANE_INFO;
30use crate::engine::EngineBatchContext;
31use crate::engine::EngineStats;
32use crate::engine::GRID_OBJECT_ID;
33use crate::engine::GRID_SCALE_TEXT_OBJECT_ID;
34use crate::engine::GridScaleBehavior;
35use crate::engine::PlaneName;
36use crate::errors::KclError;
37use crate::errors::KclErrorDetails;
38use crate::execution::DefaultPlanes;
39use crate::execution::IdGenerator;
40use crate::execution::PlaneInfo;
41use crate::settings::types::default_backface_color;
42use crate::settings::types::default_backface_color_struct;
43
44pub enum TransportCloseError {}
45
46mod engine_transport;
47mod mock_transport;
48#[cfg(target_arch = "wasm32")]
49pub mod wasm_transport;
50#[cfg(not(target_arch = "wasm32"))]
51pub mod ws_transport;
52
53#[derive(Clone, Debug)]
55pub struct ResponseInformation {
56 responses: Arc<RwLock<IndexMap<uuid::Uuid, WebSocketResponse>>>,
58}
59
60impl ResponseInformation {
61 pub fn new(responses: Arc<RwLock<IndexMap<uuid::Uuid, WebSocketResponse>>>) -> Self {
63 Self { responses }
64 }
65
66 pub async fn add(&self, id: Uuid, response: WebSocketResponse) {
68 self.responses.write().await.insert(id, response);
69 }
70}
71
72#[derive(bon::Builder)]
73pub struct EngineManager {
74 pub transport: Arc<Box<dyn EngineTransport>>,
77 responses: ResponseInformation,
78 pending_errors: Arc<RwLock<Vec<String>>>,
79 socket_health: Arc<RwLock<SocketHealth>>,
80 ids_of_async_commands: Arc<RwLock<IndexMap<Uuid, SourceRange>>>,
81
82 #[builder(default)]
84 default_planes: Arc<RwLock<Option<DefaultPlanes>>>,
85 session_data: Arc<RwLock<Option<ModelingSessionData>>>,
87
88 #[builder(default)]
89 stats: EngineStats,
90
91 #[builder(default)]
92 async_tasks: AsyncTasks,
93}
94
95impl std::fmt::Debug for EngineManager {
96 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97 f.debug_struct("EngineManager")
98 .field("responses", &self.responses)
99 .field("pending_errors", &self.pending_errors)
100 .field("socket_health", &self.socket_health)
101 .field("ids_of_async_commands", &self.ids_of_async_commands)
102 .field("default_planes", &self.default_planes)
103 .field("session_data", &self.session_data)
104 .field("stats", &self.stats)
105 .field("async_tasks", &self.async_tasks)
106 .finish()
107 }
108}
109
110impl EngineManager {
111 #[cfg(target_arch = "wasm32")]
112 pub fn new_wasm_transport(
113 manager: wasm_transport::EngineCommandManager,
114 response_context: Arc<wasm_transport::ResponseContext>,
115 ) -> Self {
116 let session_data: Arc<RwLock<Option<ModelingSessionData>>> = Arc::new(RwLock::new(None));
117 let ids_of_async_commands: Arc<RwLock<IndexMap<Uuid, SourceRange>>> = Arc::new(RwLock::new(IndexMap::new()));
118 let socket_health = Arc::new(RwLock::new(SocketHealth::Active));
119 let pending_errors = Arc::new(RwLock::new(Vec::new()));
120 let responses = response_context.response_information();
121
122 Self {
123 transport: Arc::new(Box::new(wasm_transport::WasmTransport::new(manager))),
124 responses,
125 pending_errors,
126 socket_health,
127 ids_of_async_commands,
128 default_planes: Default::default(),
129 session_data,
130 stats: Default::default(),
131 async_tasks: Default::default(),
132 }
133 }
134
135 #[cfg(not(target_arch = "wasm32"))]
136 pub async fn new_websocket_transport(ws: reqwest::Upgraded, heartbeats: Option<u64>) -> Self {
137 use crate::engine::engine_manager::ws_transport::WebSocketTransport;
138
139 let session_data: Arc<RwLock<Option<ModelingSessionData>>> = Arc::new(RwLock::new(None));
140 let ids_of_async_commands: Arc<RwLock<IndexMap<Uuid, SourceRange>>> = Arc::new(RwLock::new(IndexMap::new()));
141 let socket_health = Arc::new(RwLock::new(SocketHealth::Active));
142 let pending_errors = Arc::new(RwLock::new(Vec::new()));
143 let responses = ResponseInformation {
144 responses: Arc::new(RwLock::new(IndexMap::new())),
145 };
146
147 let transport = WebSocketTransport::spawn(
148 ws,
149 heartbeats,
150 responses.clone(),
151 Arc::clone(&session_data),
152 Arc::clone(&pending_errors),
153 Arc::clone(&socket_health),
154 )
155 .await;
156
157 Self {
158 transport: Arc::new(Box::new(transport)),
159 responses,
160 pending_errors,
161 socket_health,
162 ids_of_async_commands,
163 default_planes: Default::default(),
164 session_data,
165 stats: Default::default(),
166 async_tasks: Default::default(),
167 }
168 }
169
170 pub fn new_mock() -> Self {
173 let session_data: Arc<RwLock<Option<ModelingSessionData>>> = Arc::new(RwLock::new(None));
174 let ids_of_async_commands: Arc<RwLock<IndexMap<Uuid, SourceRange>>> = Arc::new(RwLock::new(IndexMap::new()));
175 let socket_health = Arc::new(RwLock::new(SocketHealth::Active));
176 let pending_errors = Arc::new(RwLock::new(Vec::new()));
177 let responses = ResponseInformation {
178 responses: Arc::new(RwLock::new(IndexMap::new())),
179 };
180 Self {
181 transport: Arc::new(Box::new(mock_transport::MockTransport::new(responses.clone()))),
182 responses,
183 pending_errors,
184 socket_health,
185 ids_of_async_commands,
186 default_planes: Default::default(),
187 session_data,
188 stats: Default::default(),
189 async_tasks: Default::default(),
190 }
191 }
192
193 async fn take_ids_of_async_commands(&self) -> IndexMap<Uuid, SourceRange> {
195 std::mem::take(&mut *self.ids_of_async_commands().write().await)
196 }
197
198 pub async fn take_responses(&self) -> IndexMap<Uuid, WebSocketResponse> {
200 std::mem::take(&mut *self.responses().write().await)
201 }
202
203 pub async fn clear_scene(
204 &self,
205 batch_context: &EngineBatchContext,
206 id_generator: &mut IdGenerator,
207 source_range: SourceRange,
208 ) -> Result<(), crate::errors::KclError> {
209 self.clear_queues(batch_context).await;
211
212 self.batch_modeling_cmd(
213 batch_context,
214 id_generator.next_uuid(),
215 source_range,
216 &ModelingCmd::SceneClearAll(mcmd::SceneClearAll::default()),
217 )
218 .await?;
219
220 self.flush_batch(batch_context, false, source_range).await?;
223
224 self.clear_scene_post_hook(batch_context, id_generator, source_range)
226 .await?;
227
228 Ok(())
229 }
230
231 pub async fn ensure_async_command_completed(
233 &self,
234 id: uuid::Uuid,
235 source_range: Option<SourceRange>,
236 ) -> Result<OkWebSocketResponseData, KclError> {
237 let source_range = if let Some(source_range) = source_range {
238 source_range
239 } else {
240 self.ids_of_async_commands()
242 .read()
243 .await
244 .get(&id)
245 .cloned()
246 .unwrap_or_default()
247 };
248
249 const ASYNC_CMD_TIMEOUT_SECS: u64 = 600;
257 let current_time = Instant::now();
258 while current_time.elapsed().as_secs() < ASYNC_CMD_TIMEOUT_SECS {
259 let responses = self.responses().read().await.clone();
260 let Some(resp) = responses.get(&id) else {
261 #[cfg(target_arch = "wasm32")]
264 {
265 let duration = web_time::Duration::from_millis(1);
266 wasm_timer::Delay::new(duration).await.map_err(|err| {
267 KclError::new_internal(KclErrorDetails::new(
268 format!("Failed to sleep: {:?}", err),
269 vec![source_range],
270 ))
271 })?;
272 }
273 #[cfg(not(target_arch = "wasm32"))]
274 tokio::task::yield_now().await;
275 continue;
276 };
277
278 let response = self.parse_websocket_response(resp.clone(), source_range)?;
281 return Ok(response);
282 }
283
284 Err(KclError::new_engine(KclErrorDetails::new(
285 format!(
286 "async command timed out after {ASYNC_CMD_TIMEOUT_SECS}s (client-side ceiling, not an engine error)"
287 ),
288 vec![source_range],
289 )))
290 }
291
292 pub async fn ensure_async_commands_completed(&self, batch_context: &EngineBatchContext) -> Result<(), KclError> {
294 let ids = self.take_ids_of_async_commands().await;
296
297 for (id, source_range) in ids {
299 self.ensure_async_command_completed(id, Some(source_range)).await?;
300 }
301
302 if let Err(err) = self.async_tasks().join_all().await {
308 crate::log::logln!(
309 "Error waiting for async tasks (this is typically fine and just means that an edge became something else): {:?}",
310 err
311 );
312 }
313
314 self.flush_batch(batch_context, true, SourceRange::default()).await?;
316
317 Ok(())
318 }
319
320 async fn set_edge_visibility(
322 &self,
323 batch_context: &EngineBatchContext,
324 visible: bool,
325 source_range: SourceRange,
326 id_generator: &mut IdGenerator,
327 ) -> Result<(), crate::errors::KclError> {
328 self.batch_modeling_cmd(
329 batch_context,
330 id_generator.next_uuid(),
331 source_range,
332 &ModelingCmd::from(mcmd::EdgeLinesVisible::builder().hidden(!visible).build()),
333 )
334 .await?;
335
336 Ok(())
337 }
338
339 pub async fn reapply_settings(
341 &self,
342 batch_context: &EngineBatchContext,
343 settings: &crate::ExecutorSettings,
344 source_range: SourceRange,
345 id_generator: &mut IdGenerator,
346 grid_scale_unit: GridScaleBehavior,
347 ) -> Result<(), crate::errors::KclError> {
348 if settings.geometry_only {
349 return Ok(());
350 }
351 self.set_edge_visibility(batch_context, settings.highlight_edges, source_range, id_generator)
353 .await?;
354
355 self.modify_grid(
358 batch_context,
359 !settings.show_grid,
360 grid_scale_unit,
361 source_range,
362 id_generator,
363 )
364 .await?;
365
366 self.set_user_colors(batch_context, settings, source_range, id_generator)
368 .await?;
369
370 self.flush_batch(batch_context, false, source_range).await?;
374
375 Ok(())
376 }
377
378 pub async fn batch_modeling_cmd(
380 &self,
381 batch_context: &EngineBatchContext,
382 id: uuid::Uuid,
383 source_range: SourceRange,
384 cmd: &ModelingCmd,
385 ) -> Result<(), crate::errors::KclError> {
386 let req = WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
387 cmd: cmd.clone(),
388 cmd_id: id.into(),
389 });
390
391 batch_context.push(req, source_range).await;
393 self.stats().commands_batched.fetch_add(1, Relaxed);
394
395 Ok(())
396 }
397
398 pub async fn batch_modeling_cmds(
403 &self,
404 batch_context: &EngineBatchContext,
405 source_range: SourceRange,
406 cmds: &[ModelingCmdReq],
407 ) -> Result<(), crate::errors::KclError> {
408 let mut extended_cmds = Vec::with_capacity(cmds.len());
410 for cmd in cmds {
411 extended_cmds.push((WebSocketRequest::ModelingCmdReq(cmd.clone()), source_range));
412 }
413 self.stats().commands_batched.fetch_add(extended_cmds.len(), Relaxed);
414 batch_context.extend(extended_cmds).await;
415
416 Ok(())
417 }
418
419 pub async fn batch_end_cmd(
423 &self,
424 batch_context: &EngineBatchContext,
425 id: uuid::Uuid,
426 source_range: SourceRange,
427 cmd: &ModelingCmd,
428 ) -> Result<(), crate::errors::KclError> {
429 let req = WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
430 cmd: cmd.clone(),
431 cmd_id: id.into(),
432 });
433
434 batch_context.insert_end(id, req, source_range).await;
436 self.stats().commands_batched.fetch_add(1, Relaxed);
437 Ok(())
438 }
439
440 pub async fn send_modeling_cmd(
442 &self,
443 batch_context: &EngineBatchContext,
444 id: uuid::Uuid,
445 source_range: SourceRange,
446 cmd: &ModelingCmd,
447 ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
448 let mut requests = batch_context.take_batch().await;
449
450 requests.push((
452 WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
453 cmd: cmd.clone(),
454 cmd_id: id.into(),
455 }),
456 source_range,
457 ));
458 self.stats().commands_batched.fetch_add(1, Relaxed);
459
460 self.run_batch(requests, source_range).await
462 }
463
464 pub async fn async_modeling_cmd(
467 &self,
468 id: uuid::Uuid,
469 source_range: SourceRange,
470 cmd: &ModelingCmd,
471 ) -> Result<(), crate::errors::KclError> {
472 self.ids_of_async_commands().write().await.insert(id, source_range);
474
475 self.transport
477 .inner_fire_modeling_cmd(
478 id,
479 source_range,
480 WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
481 cmd: cmd.clone(),
482 cmd_id: id.into(),
483 }),
484 HashMap::from([(id, source_range)]),
485 )
486 .await?;
487
488 Ok(())
489 }
490
491 async fn run_batch(
493 &self,
494 orig_requests: Vec<(WebSocketRequest, SourceRange)>,
495 source_range: SourceRange,
496 ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
497 if orig_requests.is_empty() {
499 return Ok(OkWebSocketResponseData::Modeling {
500 modeling_response: OkModelingCmdResponse::Empty {},
501 });
502 }
503
504 let requests: Vec<ModelingCmdReq> = orig_requests
505 .iter()
506 .filter_map(|(val, _)| match val {
507 WebSocketRequest::ModelingCmdReq(ModelingCmdReq { cmd, cmd_id }) => Some(ModelingCmdReq {
508 cmd: cmd.clone(),
509 cmd_id: *cmd_id,
510 }),
511 _ => None,
512 })
513 .collect();
514
515 let batched_requests = WebSocketRequest::ModelingCmdBatchReq(ModelingBatch {
516 requests,
517 batch_id: uuid::Uuid::new_v4().into(),
518 responses: true,
519 });
520
521 let final_req = if orig_requests.len() == 1 {
522 orig_requests.first().unwrap().0.clone()
524 } else {
525 batched_requests
526 };
527
528 let mut id_to_source_range = HashMap::new();
531
532 let mut id_to_command = HashMap::new();
533 for (req, range) in orig_requests.iter() {
534 match req {
535 WebSocketRequest::ModelingCmdReq(ModelingCmdReq { cmd, cmd_id }) => {
536 let id = Uuid::from(*cmd_id);
537 id_to_source_range.insert(id, *range);
538 id_to_command.insert(id, ModelingCmdEndpoint::from(cmd));
539 }
540 _ => {
541 return Err(KclError::new_engine(KclErrorDetails::new(
542 format!("The request is not a modeling command: {req:?}"),
543 vec![*range],
544 )));
545 }
546 }
547 }
548
549 self.stats().batches_sent.fetch_add(1, Relaxed);
550
551 match final_req {
553 WebSocketRequest::ModelingCmdBatchReq(ModelingBatch {
554 ref requests,
555 batch_id,
556 responses: _,
557 }) => {
558 let last_id = requests.last().unwrap().cmd_id;
560 let ws_resp = self
561 .inner_send_modeling_cmd(batch_id.into(), source_range, final_req, id_to_source_range.clone())
562 .await?;
563 let response = self.parse_websocket_response(ws_resp, source_range)?;
564
565 if let OkWebSocketResponseData::ModelingBatch { responses } = response {
567 self.parse_batch_responses(last_id.into(), id_to_source_range, id_to_command, responses)
568 } else {
569 Err(KclError::new_engine(KclErrorDetails::new(
571 format!("Failed to get batch response: {response:?}"),
572 vec![source_range],
573 )))
574 }
575 }
576 WebSocketRequest::ModelingCmdReq(ModelingCmdReq { cmd: _, cmd_id }) => {
577 let source_range = id_to_source_range.get(cmd_id.as_ref()).cloned().ok_or_else(|| {
585 KclError::new_engine(KclErrorDetails::new(
586 format!("Failed to get source range for command ID: {cmd_id:?}"),
587 vec![],
588 ))
589 })?;
590 let ws_resp = self
591 .inner_send_modeling_cmd(cmd_id.into(), source_range, final_req, id_to_source_range)
592 .await?;
593 self.parse_websocket_response(ws_resp, source_range)
594 }
595 _ => Err(KclError::new_engine(KclErrorDetails::new(
596 format!("The final request is not a modeling command: {final_req:?}"),
597 vec![source_range],
598 ))),
599 }
600 }
601
602 pub async fn flush_batch(
604 &self,
605 batch_context: &EngineBatchContext,
606 batch_end: bool,
609 source_range: SourceRange,
610 ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
611 let all_requests = if batch_end {
612 let mut requests = batch_context.take_batch().await;
613 requests.extend(batch_context.take_batch_end().await.values().cloned());
614 requests
615 } else {
616 batch_context.take_batch().await
617 };
618
619 self.run_batch(all_requests, source_range).await
620 }
621
622 async fn make_default_plane(
623 &self,
624 batch_context: &EngineBatchContext,
625 plane_id: uuid::Uuid,
626 info: &PlaneInfo,
627 color: Option<Color>,
628 source_range: SourceRange,
629 id_generator: &mut IdGenerator,
630 ) -> Result<uuid::Uuid, KclError> {
631 let default_size = 100.0;
633
634 self.batch_modeling_cmd(
635 batch_context,
636 plane_id,
637 source_range,
638 &ModelingCmd::from(
639 mcmd::MakePlane::builder()
640 .clobber(false)
641 .origin(info.origin.into())
642 .size(LengthUnit(default_size))
643 .x_axis(info.x_axis.into())
644 .y_axis(info.y_axis.into())
645 .hide(true)
646 .build(),
647 ),
648 )
649 .await?;
650
651 if let Some(color) = color {
652 self.batch_modeling_cmd(
654 batch_context,
655 id_generator.next_uuid(),
656 source_range,
657 &ModelingCmd::from(mcmd::PlaneSetColor::builder().color(color).plane_id(plane_id).build()),
658 )
659 .await?;
660 }
661
662 Ok(plane_id)
663 }
664
665 async fn new_default_planes(
666 &self,
667 batch_context: &EngineBatchContext,
668 id_generator: &mut IdGenerator,
669 source_range: SourceRange,
670 ) -> Result<DefaultPlanes, KclError> {
671 let plane_opacity = 0.1;
672 let plane_settings: Vec<(PlaneName, Uuid, Option<Color>)> = vec![
673 (
674 PlaneName::Xy,
675 id_generator.next_uuid(),
676 Some(Color::from_rgba(0.7, 0.28, 0.28, plane_opacity)),
677 ),
678 (
679 PlaneName::Yz,
680 id_generator.next_uuid(),
681 Some(Color::from_rgba(0.28, 0.7, 0.28, plane_opacity)),
682 ),
683 (
684 PlaneName::Xz,
685 id_generator.next_uuid(),
686 Some(Color::from_rgba(0.28, 0.28, 0.7, plane_opacity)),
687 ),
688 (PlaneName::NegXy, id_generator.next_uuid(), None),
689 (PlaneName::NegYz, id_generator.next_uuid(), None),
690 (PlaneName::NegXz, id_generator.next_uuid(), None),
691 ];
692
693 let mut planes = HashMap::new();
694 for (name, plane_id, color) in plane_settings {
695 let info = DEFAULT_PLANE_INFO.get(&name).ok_or_else(|| {
696 KclError::new_engine(KclErrorDetails::new(
698 format!("Failed to get default plane info for: {name:?}"),
699 vec![source_range],
700 ))
701 })?;
702 planes.insert(
703 name,
704 self.make_default_plane(batch_context, plane_id, info, color, source_range, id_generator)
705 .await?,
706 );
707 }
708
709 self.flush_batch(batch_context, false, source_range).await?;
711
712 Ok(DefaultPlanes {
713 xy: planes[&PlaneName::Xy],
714 neg_xy: planes[&PlaneName::NegXy],
715 xz: planes[&PlaneName::Xz],
716 neg_xz: planes[&PlaneName::NegXz],
717 yz: planes[&PlaneName::Yz],
718 neg_yz: planes[&PlaneName::NegYz],
719 })
720 }
721
722 fn parse_websocket_response(
723 &self,
724 response: WebSocketResponse,
725 source_range: SourceRange,
726 ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
727 match response {
728 WebSocketResponse::Success(success) => Ok(success.resp),
729 WebSocketResponse::Failure(fail) => {
730 let _request_id = fail.request_id;
731 if fail.errors.is_empty() {
732 return Err(KclError::new_engine(KclErrorDetails::new(
733 "Failure response with no error details".to_owned(),
734 vec![source_range],
735 )));
736 }
737 Err(KclError::new_engine(KclErrorDetails::new(
738 fail.errors
739 .iter()
740 .map(|e| e.message.clone())
741 .collect::<Vec<_>>()
742 .join("\n"),
743 vec![source_range],
744 )))
745 }
746 }
747 }
748
749 fn parse_batch_responses(
750 &self,
751 id: uuid::Uuid,
753 id_to_source_range: HashMap<uuid::Uuid, SourceRange>,
755 id_to_command: HashMap<uuid::Uuid, ModelingCmdEndpoint>,
757 responses: HashMap<kcmc::id::ModelingCmdId, BatchResponse>,
759 ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
760 let mut any_err: Option<crate::errors::KclError> = None;
761 let mut target_ok: Option<OkWebSocketResponseData> = None;
762 #[expect(
765 clippy::iter_over_hash_type,
766 reason = "modeling command uses a HashMap and keys are random, so we don't really have a choice"
767 )]
768 for (cmd_id, resp) in responses.iter() {
769 let cmd_id = Uuid::from(*cmd_id);
770 match resp {
771 BatchResponse::Success { response } if cmd_id == id => {
772 target_ok = Some(OkWebSocketResponseData::Modeling {
775 modeling_response: response.clone(),
776 });
777 }
778 BatchResponse::Success { .. } => continue,
779 BatchResponse::Failure { errors } => {
780 let command = id_to_command
781 .get(&cmd_id)
782 .map(ModelingCmdEndpoint::to_string)
783 .unwrap_or("[missing entry]".to_string());
784 let source_range = id_to_source_range.get(&cmd_id).cloned().ok_or_else(|| {
786 KclError::new_engine(KclErrorDetails::new(
787 format!("Failed to get source range for command {command} with ID: {cmd_id:?}"),
788 vec![],
789 ))
790 })?;
791 if errors.is_empty() {
792 any_err = Some(KclError::new_engine(KclErrorDetails::new(
793 format!("Failure response for batch with no error details at command {command}"),
794 vec![source_range],
795 )));
796 break;
797 }
798 let errors = errors.iter().map(|e| e.message.clone()).collect::<Vec<_>>().join("\n");
799 any_err = Some(KclError::new_engine(KclErrorDetails::new(
800 format!("command {command} resulted in errors: \n {errors}"),
801 vec![source_range],
802 )));
803 break;
804 }
805 }
806 }
807
808 match (any_err, target_ok) {
809 (Some(err), _) => Err(err),
810 (None, Some(ok)) => Ok(ok),
811 (None, None) => {
812 Err(KclError::new_engine(KclErrorDetails::new(
815 format!("Failed to find response for command ID: {id:?}"),
816 vec![],
817 )))
818 }
819 }
820 }
821
822 async fn set_user_colors(
823 &self,
824 batch_context: &EngineBatchContext,
825 settings: &ExecutorSettings,
826 source_range: SourceRange,
827 id_generator: &mut IdGenerator,
828 ) -> Result<(), KclError> {
829 let bf = settings
830 .default_backface_color
831 .clone()
832 .unwrap_or(default_backface_color());
833 let backface = csscolorparser::parse(&bf)
834 .map(|color| kcmc::shared::Color::from_rgba(color.r, color.g, color.b, color.a))
835 .unwrap_or(default_backface_color_struct());
836 self.batch_modeling_cmd(
837 batch_context,
838 id_generator.next_uuid(),
839 source_range,
840 &ModelingCmd::from(
841 mcmd::SetDefaultSystemProperties::builder()
842 .backface_color(backface)
843 .build(),
844 ),
845 )
846 .await?;
847 Ok(())
848 }
849
850 async fn modify_grid(
851 &self,
852 batch_context: &EngineBatchContext,
853 hidden: bool,
854 grid_scale_behavior: GridScaleBehavior,
855 source_range: SourceRange,
856 id_generator: &mut IdGenerator,
857 ) -> Result<(), KclError> {
858 self.batch_modeling_cmd(
860 batch_context,
861 id_generator.next_uuid(),
862 source_range,
863 &ModelingCmd::from(
864 mcmd::ObjectVisible::builder()
865 .hidden(hidden)
866 .object_id(*GRID_OBJECT_ID)
867 .build(),
868 ),
869 )
870 .await?;
871
872 self.batch_modeling_cmd(
873 batch_context,
874 id_generator.next_uuid(),
875 source_range,
876 &grid_scale_behavior.into_modeling_cmd(),
877 )
878 .await?;
879
880 self.batch_modeling_cmd(
882 batch_context,
883 id_generator.next_uuid(),
884 source_range,
885 &ModelingCmd::from(
886 mcmd::ObjectVisible::builder()
887 .hidden(hidden)
888 .object_id(*GRID_SCALE_TEXT_OBJECT_ID)
889 .build(),
890 ),
891 )
892 .await?;
893
894 Ok(())
895 }
896
897 pub async fn clear_queues(&self, batch_context: &EngineBatchContext) {
898 batch_context.clear().await;
899 self.ids_of_async_commands().write().await.clear();
900 self.async_tasks().clear().await;
901 }
902
903 fn responses(&self) -> Arc<RwLock<IndexMap<Uuid, WebSocketResponse>>> {
904 self.responses.responses.clone()
905 }
906
907 fn ids_of_async_commands(&self) -> Arc<RwLock<IndexMap<Uuid, SourceRange>>> {
908 self.ids_of_async_commands.clone()
909 }
910
911 fn async_tasks(&self) -> AsyncTasks {
912 self.async_tasks.clone()
913 }
914
915 pub fn stats(&self) -> &EngineStats {
916 &self.stats
917 }
918
919 pub fn get_default_planes(&self) -> Arc<RwLock<Option<DefaultPlanes>>> {
920 self.default_planes.clone()
921 }
922
923 async fn clear_scene_post_hook(
924 &self,
925 batch_context: &EngineBatchContext,
926 id_generator: &mut IdGenerator,
927 source_range: SourceRange,
928 ) -> Result<(), KclError> {
929 let new_planes = self
931 .new_default_planes(batch_context, id_generator, source_range)
932 .await?;
933 *self.default_planes.write().await = Some(new_planes);
934
935 self.transport.start_new_session(source_range).await?;
936
937 Ok(())
938 }
939
940 async fn inner_send_modeling_cmd(
941 &self,
942 id: uuid::Uuid,
943 source_range: SourceRange,
944 cmd: WebSocketRequest,
945 id_to_source_range: HashMap<Uuid, SourceRange>,
946 ) -> Result<WebSocketResponse, KclError> {
947 let response = self
948 .transport
949 .inner_send_modeling_cmd(id, source_range, cmd, id_to_source_range)
950 .await?;
951
952 self.responses.add(id, response.clone()).await;
953 Ok(response)
954 }
955
956 pub async fn get_session_data(&self) -> Option<ModelingSessionData> {
957 self.session_data.read().await.clone()
958 }
959
960 pub async fn close(&self) {
961 let _ = self.transport.close().await;
962 }
963}
964
965#[derive(Debug, PartialEq)]
967pub enum SocketHealth {
968 Active,
969 Inactive,
970}