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::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/// Information about the responses from the engine.
53#[derive(Clone, Debug)]
54pub struct ResponseInformation {
55    /// The responses from the engine.
56    responses: Arc<RwLock<IndexMap<uuid::Uuid, WebSocketResponse>>>,
57}
58
59impl ResponseInformation {
60    /// Basic constructor.
61    pub fn new(responses: Arc<RwLock<IndexMap<uuid::Uuid, WebSocketResponse>>>) -> Self {
62        Self { responses }
63    }
64
65    /// Add a new response from the engine.
66    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    // Replaces `engine_req_tx: mpsc::Sender<ToEngineReq>`
74    // from the original native connection type.
75    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    /// The default planes for the scene.
82    #[builder(default)]
83    default_planes: Arc<RwLock<Option<DefaultPlanes>>>,
84    /// If the server sends session data, it'll be copied to here.
85    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    /// Mock connection that doesn't actually connect to anything.
170    /// Used for testing.
171    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    /// Take the ids of async commands that have accumulated so far and clear them.
193    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    /// Take the responses that have accumulated so far and clear them.
198    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        // Clear any batched commands leftover from previous scenes.
209        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        // Flush the batch queue, so clear is run right away.
220        // Otherwise the hooks below won't work.
221        self.flush_batch(batch_context, false, source_range).await?;
222
223        // Do the after clear scene hook.
224        self.clear_scene_post_hook(batch_context, id_generator, source_range)
225            .await?;
226
227        Ok(())
228    }
229
230    /// Ensure a specific async command has been completed.
231    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            // Look it up if we don't have it.
240            self.ids_of_async_commands()
241                .read()
242                .await
243                .get(&id)
244                .cloned()
245                .unwrap_or_default()
246        };
247
248        // The previous 60s ceiling here was too aggressive for long-running
249        // engine commands - notably large STEP / B-rep imports, which the
250        // engine itself routinely takes several minutes to process. When the
251        // ceiling fired first the user got a generic "async command timed
252        // out" message and the eventual engine response (success OR error)
253        // was discarded, masking the real outcome. 600s (10 min) gives the
254        // engine room to finish or to surface its own error.
255        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                // Yield to the event loop so that we don’t block the UI thread.
261                // No seriously WE DO NOT WANT TO PAUSE THE WHOLE APP ON THE JS SIDE.
262                #[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            // If the response is an error, return it.
278            // Parsing will do that and we can ignore the result, we don't care.
279            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    /// Ensure ALL async commands have been completed.
292    pub async fn ensure_async_commands_completed(&self, batch_context: &EngineBatchContext) -> Result<(), KclError> {
293        // Check if all async commands have been completed.
294        let ids = self.take_ids_of_async_commands().await;
295
296        // Try to get them from the responses.
297        for (id, source_range) in ids {
298            self.ensure_async_command_completed(id, Some(source_range)).await?;
299        }
300
301        // Make sure we check for all async tasks as well.
302        // The reason why we ignore the error here is that, if a model fillets an edge
303        // we previously called something on, it might no longer exist. In which case,
304        // the artifact graph won't care either if its gone since you can't select it
305        // anymore anyways.
306        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        // Flush the batch to make sure nothing remains.
314        self.flush_batch(batch_context, true, SourceRange::default()).await?;
315
316        Ok(())
317    }
318
319    /// Set the visibility of edges.
320    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    /// Re-run the command to apply the settings.
339    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        // Set the edge visibility.
348        self.set_edge_visibility(batch_context, settings.highlight_edges, source_range, id_generator)
349            .await?;
350
351        // Send the command to show the grid.
352
353        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        // Set up user's color choices.
363        self.set_user_colors(batch_context, settings, source_range, id_generator)
364            .await?;
365
366        // We do not have commands for changing ssao on the fly.
367
368        // Flush the batch queue, so the settings are applied right away.
369        self.flush_batch(batch_context, false, source_range).await?;
370
371        Ok(())
372    }
373
374    // Add a modeling command to the batch but don't fire it right away.
375    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        // Add cmd to the batch.
388        batch_context.push(req, source_range).await;
389        self.stats().commands_batched.fetch_add(1, Relaxed);
390
391        Ok(())
392    }
393
394    // Add a vector of modeling commands to the batch but don't fire it right away.
395    // This allows you to force them all to be added together in the same order.
396    // When we are running things in parallel this prevents race conditions that might come
397    // if specific commands are run before others.
398    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        // Add cmds to the batch.
405        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    /// Add a command to the batch that needs to be executed at the very end.
416    /// This for stuff like fillets or chamfers where if we execute too soon the
417    /// engine will eat the ID and we can't reference it for other commands.
418    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        // Add cmd to the batch end.
431        batch_context.insert_end(id, req, source_range).await;
432        self.stats().commands_batched.fetch_add(1, Relaxed);
433        Ok(())
434    }
435
436    /// Send the modeling cmd and wait for the response.
437    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        // Add the command to the batch.
447        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        // Flush the batch queue.
457        self.run_batch(requests, source_range).await
458    }
459
460    /// Send the modeling cmd async and don't wait for the response.
461    /// Add it to our list of async commands.
462    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        // Add the command ID to the list of async commands.
469        self.ids_of_async_commands().write().await.insert(id, source_range);
470
471        // Fire off the command now, but don't wait for the response, we don't care about it.
472        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    /// Run the batch for the specific commands.
488    async fn run_batch(
489        &self,
490        orig_requests: Vec<(WebSocketRequest, SourceRange)>,
491        source_range: SourceRange,
492    ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
493        // Return early if we have no commands to send.
494        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            // We can unwrap here because we know the batch has only one element.
519            orig_requests.first().unwrap().0.clone()
520        } else {
521            batched_requests
522        };
523
524        // Create the map of original command IDs to source range.
525        // This is for the wasm side, kurt needs it for selections.
526        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        // We pop off the responses to cleanup our mappings.
544        match final_req {
545            WebSocketRequest::ModelingCmdBatchReq(ModelingBatch {
546                ref requests,
547                batch_id,
548                responses: _,
549            }) => {
550                // Get the last command ID.
551                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 we have a batch response, we want to return the specific id we care about.
558                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                    // We should never get here.
563                    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                // You are probably wondering why we can't just return the source range we were
571                // passed with the function. Well this is actually really important.
572                // If this is the last command in the batch and there is only one and we've reached
573                // the end of the file, this will trigger a flush batch function, but it will just
574                // send default or the end of the file as it's source range not the origin of the
575                // request so we need the original request source range in case the engine returns
576                // an error.
577                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    /// Force flush the batch queue.
596    pub async fn flush_batch(
597        &self,
598        batch_context: &EngineBatchContext,
599        // Whether or not to flush the end commands as well.
600        // We only do this at the very end of the file.
601        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        // Create new default planes.
625        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            // Set the color.
646            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                // We should never get here.
690                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        // Flush the batch queue, so these planes are created right away.
703        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        // The last response we are looking for.
745        id: uuid::Uuid,
746        // The mapping of source ranges to command IDs.
747        id_to_source_range: HashMap<uuid::Uuid, SourceRange>,
748        // The response from the engine.
749        responses: HashMap<uuid::Uuid, BatchResponse>,
750    ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
751        // Iterate over the responses and check for errors.
752        #[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                        // This is the response we care about.
761                        return Ok(OkWebSocketResponseData::Modeling {
762                            modeling_response: response.clone(),
763                        });
764                    } else {
765                        // Continue the loop if this is not the response we care about.
766                        continue;
767                    }
768                }
769                BatchResponse::Failure { errors } => {
770                    // Get the source range for the command.
771                    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        // Return an error that we did not get an error or the response we wanted.
792        // This should never happen but who knows.
793        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        // Hide/show the grid.
836        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        // Hide/show the grid scale text.
858        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        // Remake the default planes, since they would have been removed after the scene was cleared.
907        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/// State of the connection to the engine.
943#[derive(Debug, PartialEq)]
944pub enum SocketHealth {
945    Active,
946    Inactive,
947}