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