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