Skip to main content

kcl_lib/engine/
engine_manager.rs

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/// Information about the responses from the engine.
54#[derive(Clone, Debug)]
55pub struct ResponseInformation {
56    /// The responses from the engine.
57    responses: Arc<RwLock<IndexMap<uuid::Uuid, WebSocketResponse>>>,
58}
59
60impl ResponseInformation {
61    /// Basic constructor.
62    pub fn new(responses: Arc<RwLock<IndexMap<uuid::Uuid, WebSocketResponse>>>) -> Self {
63        Self { responses }
64    }
65
66    /// Add a new response from the engine.
67    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    // Replaces `engine_req_tx: mpsc::Sender<ToEngineReq>`
75    // from the original native connection type.
76    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    /// The default planes for the scene.
83    #[builder(default)]
84    default_planes: Arc<RwLock<Option<DefaultPlanes>>>,
85    /// If the server sends session data, it'll be copied to here.
86    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    /// Mock connection that doesn't actually connect to anything.
171    /// Used for testing.
172    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    /// Take the ids of async commands that have accumulated so far and clear them.
194    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    /// Take the responses that have accumulated so far and clear them.
199    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        // Clear any batched commands leftover from previous scenes.
210        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        // Flush the batch queue, so clear is run right away.
221        // Otherwise the hooks below won't work.
222        self.flush_batch(batch_context, false, source_range).await?;
223
224        // Do the after clear scene hook.
225        self.clear_scene_post_hook(batch_context, id_generator, source_range)
226            .await?;
227
228        Ok(())
229    }
230
231    /// Ensure a specific async command has been completed.
232    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            // Look it up if we don't have it.
241            self.ids_of_async_commands()
242                .read()
243                .await
244                .get(&id)
245                .cloned()
246                .unwrap_or_default()
247        };
248
249        // The previous 60s ceiling here was too aggressive for long-running
250        // engine commands - notably large STEP / B-rep imports, which the
251        // engine itself routinely takes several minutes to process. When the
252        // ceiling fired first the user got a generic "async command timed
253        // out" message and the eventual engine response (success OR error)
254        // was discarded, masking the real outcome. 600s (10 min) gives the
255        // engine room to finish or to surface its own error.
256        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                // Yield to the event loop so that we don’t block the UI thread.
262                // No seriously WE DO NOT WANT TO PAUSE THE WHOLE APP ON THE JS SIDE.
263                #[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            // If the response is an error, return it.
279            // Parsing will do that and we can ignore the result, we don't care.
280            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    /// Ensure ALL async commands have been completed.
293    pub async fn ensure_async_commands_completed(&self, batch_context: &EngineBatchContext) -> Result<(), KclError> {
294        // Check if all async commands have been completed.
295        let ids = self.take_ids_of_async_commands().await;
296
297        // Try to get them from the responses.
298        for (id, source_range) in ids {
299            self.ensure_async_command_completed(id, Some(source_range)).await?;
300        }
301
302        // Make sure we check for all async tasks as well.
303        // The reason why we ignore the error here is that, if a model fillets an edge
304        // we previously called something on, it might no longer exist. In which case,
305        // the artifact graph won't care either if its gone since you can't select it
306        // anymore anyways.
307        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        // Flush the batch to make sure nothing remains.
315        self.flush_batch(batch_context, true, SourceRange::default()).await?;
316
317        Ok(())
318    }
319
320    /// Set the visibility of edges.
321    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    /// Re-run the command to apply the settings.
340    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        // Set the edge visibility.
352        self.set_edge_visibility(batch_context, settings.highlight_edges, source_range, id_generator)
353            .await?;
354
355        // Send the command to show the grid.
356
357        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        // Set up user's color choices.
367        self.set_user_colors(batch_context, settings, source_range, id_generator)
368            .await?;
369
370        // We do not have commands for changing ssao on the fly.
371
372        // Flush the batch queue, so the settings are applied right away.
373        self.flush_batch(batch_context, false, source_range).await?;
374
375        Ok(())
376    }
377
378    // Add a modeling command to the batch but don't fire it right away.
379    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        // Add cmd to the batch.
392        batch_context.push(req, source_range).await;
393        self.stats().commands_batched.fetch_add(1, Relaxed);
394
395        Ok(())
396    }
397
398    // Add a vector of modeling commands to the batch but don't fire it right away.
399    // This allows you to force them all to be added together in the same order.
400    // When we are running things in parallel this prevents race conditions that might come
401    // if specific commands are run before others.
402    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        // Add cmds to the batch.
409        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    /// Add a command to the batch that needs to be executed at the very end.
420    /// This for stuff like fillets or chamfers where if we execute too soon the
421    /// engine will eat the ID and we can't reference it for other commands.
422    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        // Add cmd to the batch end.
435        batch_context.insert_end(id, req, source_range).await;
436        self.stats().commands_batched.fetch_add(1, Relaxed);
437        Ok(())
438    }
439
440    /// Send the modeling cmd and wait for the response.
441    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        // Add the command to the batch.
451        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        // Flush the batch queue.
461        self.run_batch(requests, source_range).await
462    }
463
464    /// Send the modeling cmd async and don't wait for the response.
465    /// Add it to our list of async commands.
466    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        // Add the command ID to the list of async commands.
473        self.ids_of_async_commands().write().await.insert(id, source_range);
474
475        // Fire off the command now, but don't wait for the response, we don't care about it.
476        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    /// Run the batch for the specific commands.
492    async fn run_batch(
493        &self,
494        orig_requests: Vec<(WebSocketRequest, SourceRange)>,
495        source_range: SourceRange,
496    ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
497        // Return early if we have no commands to send.
498        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            // We can unwrap here because we know the batch has only one element.
523            orig_requests.first().unwrap().0.clone()
524        } else {
525            batched_requests
526        };
527
528        // Create the map of original command IDs to source range.
529        // This is for the wasm side, kurt needs it for selections.
530        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        // We pop off the responses to cleanup our mappings.
552        match final_req {
553            WebSocketRequest::ModelingCmdBatchReq(ModelingBatch {
554                ref requests,
555                batch_id,
556                responses: _,
557            }) => {
558                // Get the last command ID.
559                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 we have a batch response, we want to return the specific id we care about.
566                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                    // We should never get here.
570                    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                // You are probably wondering why we can't just return the source range we were
578                // passed with the function. Well this is actually really important.
579                // If this is the last command in the batch and there is only one and we've reached
580                // the end of the file, this will trigger a flush batch function, but it will just
581                // send default or the end of the file as it's source range not the origin of the
582                // request so we need the original request source range in case the engine returns
583                // an error.
584                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    /// Force flush the batch queue.
603    pub async fn flush_batch(
604        &self,
605        batch_context: &EngineBatchContext,
606        // Whether or not to flush the end commands as well.
607        // We only do this at the very end of the file.
608        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        // Create new default planes.
632        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            // Set the color.
653            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                // We should never get here.
697                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        // Flush the batch queue, so these planes are created right away.
710        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        // The last response we are looking for.
752        id: uuid::Uuid,
753        // The mapping of source ranges to command IDs.
754        id_to_source_range: HashMap<uuid::Uuid, SourceRange>,
755        // Allows us to print which command failed
756        id_to_command: HashMap<uuid::Uuid, ModelingCmdEndpoint>,
757        // The response from the engine.
758        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        // Iterate over the responses and check for errors.
763        // Any error takes precedent over any Ok.
764        #[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                    // This is the response we care about.
773                    // Keep looking for errors after locating it.
774                    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                    // Get the source range for the command.
785                    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                // Return an error that we did not get an error or the response we wanted.
813                // This should never happen but who knows.
814                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        // Hide/show the grid.
859        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        // Hide/show the grid scale text.
881        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        // Remake the default planes, since they would have been removed after the scene was cleared.
930        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/// State of the connection to the engine.
966#[derive(Debug, PartialEq)]
967pub enum SocketHealth {
968    Active,
969    Inactive,
970}