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::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/// Information about the responses from the engine.
55#[derive(Clone, Debug)]
56pub struct ResponseInformation {
57    /// The responses from the engine.
58    responses: Arc<RwLock<IndexMap<uuid::Uuid, WebSocketResponse>>>,
59}
60
61impl ResponseInformation {
62    /// Basic constructor.
63    pub fn new(responses: Arc<RwLock<IndexMap<uuid::Uuid, WebSocketResponse>>>) -> Self {
64        Self { responses }
65    }
66
67    /// Add a new response from the engine.
68    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    // Replaces `engine_req_tx: mpsc::Sender<ToEngineReq>`
76    // from the original native connection type.
77    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    /// The default planes for the scene.
84    #[builder(default)]
85    default_planes: Arc<RwLock<Option<DefaultPlanes>>>,
86    /// If the server sends session data, it'll be copied to here.
87    session_data: Arc<RwLock<Option<ModelingSessionData>>>,
88
89    /// Request ID returned by the HTTP request that upgraded to this WebSocket.
90    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    /// Mock connection that doesn't actually connect to anything.
188    /// Used for testing.
189    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    /// Take the ids of async commands that have accumulated so far and clear them.
212    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    /// Take the responses that have accumulated so far and clear them.
217    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        // Clear any batched commands leftover from previous scenes.
230        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        // Flush the batch queue, so clear is run right away.
251        // Otherwise the hooks below won't work.
252        self.flush_batch(batch_context, false, source_range).await?;
253
254        // Do the after clear scene hook.
255        self.clear_scene_post_hook(batch_context, id_generator, source_range, geometry_only)
256            .await?;
257
258        Ok(())
259    }
260
261    /// Ensure a specific async command has been completed.
262    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            // Look it up if we don't have it.
271            self.ids_of_async_commands()
272                .read()
273                .await
274                .get(&id)
275                .cloned()
276                .unwrap_or_default()
277        };
278
279        // The previous 60s ceiling here was too aggressive for long-running
280        // engine commands - notably large STEP / B-rep imports, which the
281        // engine itself routinely takes several minutes to process. When the
282        // ceiling fired first the user got a generic "async command timed
283        // out" message and the eventual engine response (success OR error)
284        // was discarded, masking the real outcome. 600s (10 min) gives the
285        // engine room to finish or to surface its own error.
286        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                // Yield to the event loop so that we don’t block the UI thread.
292                // No seriously WE DO NOT WANT TO PAUSE THE WHOLE APP ON THE JS SIDE.
293                #[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            // If the response is an error, return it.
309            // Parsing will do that and we can ignore the result, we don't care.
310            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    /// Ensure ALL async commands have been completed.
323    pub async fn ensure_async_commands_completed(&self, batch_context: &EngineBatchContext) -> Result<(), KclError> {
324        // Check if all async commands have been completed.
325        let ids = self.take_ids_of_async_commands().await;
326
327        // Try to get them from the responses.
328        for (id, source_range) in ids {
329            self.ensure_async_command_completed(id, Some(source_range)).await?;
330        }
331
332        // Make sure we check for all async tasks as well.
333        // The reason why we ignore the error here is that, if a model fillets an edge
334        // we previously called something on, it might no longer exist. In which case,
335        // the artifact graph won't care either if its gone since you can't select it
336        // anymore anyways.
337        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        // Flush the batch to make sure nothing remains.
345        self.flush_batch(batch_context, true, SourceRange::default()).await?;
346
347        Ok(())
348    }
349
350    /// Set the visibility of edges.
351    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    /// Apply graphics settings, if the engine is not CPU-only (geometry-only) mode.
370    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    /// Unconditionally apply graphics settings.
386    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        // Set the edge visibility.
395        self.set_edge_visibility(batch_context, settings.highlight_edges, source_range, id_generator)
396            .await?;
397
398        // Send the command to show the grid.
399
400        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        // Set up user's color choices.
410        self.set_user_colors(batch_context, settings, source_range, id_generator)
411            .await?;
412
413        // We do not have commands for changing ssao on the fly.
414
415        // Flush the batch queue, so the settings are applied right away.
416        self.flush_batch(batch_context, false, source_range).await?;
417
418        Ok(())
419    }
420
421    // Add a modeling command to the batch but don't fire it right away.
422    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        // Add cmd to the batch.
435        batch_context.push(req, source_range).await;
436        self.stats().commands_batched.fetch_add(1, Relaxed);
437
438        Ok(())
439    }
440
441    // Add a vector of modeling commands to the batch but don't fire it right away.
442    // This allows you to force them all to be added together in the same order.
443    // When we are running things in parallel this prevents race conditions that might come
444    // if specific commands are run before others.
445    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        // Add cmds to the batch.
452        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    /// Add a command to the batch that needs to be executed at the very end.
463    /// This for stuff like fillets or chamfers where if we execute too soon the
464    /// engine will eat the ID and we can't reference it for other commands.
465    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        // Add cmd to the batch end.
478        batch_context.insert_end(id, req, source_range).await;
479        self.stats().commands_batched.fetch_add(1, Relaxed);
480        Ok(())
481    }
482
483    /// Send the modeling cmd and wait for the response.
484    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        // Add the command to the batch.
494        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        // Flush the batch queue.
504        self.run_batch(requests, source_range).await
505    }
506
507    /// Send the modeling cmd async and don't wait for the response.
508    /// Add it to our list of async commands.
509    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        // Add the command ID to the list of async commands.
516        self.ids_of_async_commands().write().await.insert(id, source_range);
517
518        // Fire off the command now, but don't wait for the response, we don't care about it.
519        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    /// Run the batch for the specific commands.
535    async fn run_batch(
536        &self,
537        orig_requests: Vec<(WebSocketRequest, SourceRange)>,
538        source_range: SourceRange,
539    ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
540        // Return early if we have no commands to send.
541        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            // We can unwrap here because we know the batch has only one element.
566            orig_requests.first().unwrap().0.clone()
567        } else {
568            batched_requests
569        };
570
571        // Create the map of original command IDs to source range.
572        // This is for the wasm side, kurt needs it for selections.
573        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        // We pop off the responses to cleanup our mappings.
595        match final_req {
596            WebSocketRequest::ModelingCmdBatchReq(ModelingBatch {
597                ref requests,
598                batch_id,
599                responses: _,
600            }) => {
601                // Get the last command ID.
602                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 we have a batch response, we want to return the specific id we care about.
609                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                    // We should never get here.
613                    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                // You are probably wondering why we can't just return the source range we were
621                // passed with the function. Well this is actually really important.
622                // If this is the last command in the batch and there is only one and we've reached
623                // the end of the file, this will trigger a flush batch function, but it will just
624                // send default or the end of the file as it's source range not the origin of the
625                // request so we need the original request source range in case the engine returns
626                // an error.
627                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    /// Force flush the batch queue.
646    pub async fn flush_batch(
647        &self,
648        batch_context: &EngineBatchContext,
649        // Whether or not to flush the end commands as well.
650        // We only do this at the very end of the file.
651        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        // Create new default planes.
675        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            // Set the color.
696            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                // We should never get here.
731                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        // Flush the batch queue, so these planes are created right away.
744        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        // The last response we are looking for.
786        id: uuid::Uuid,
787        // The mapping of source ranges to command IDs.
788        id_to_source_range: HashMap<uuid::Uuid, SourceRange>,
789        // Allows us to print which command failed
790        id_to_command: HashMap<uuid::Uuid, ModelingCmdEndpoint>,
791        // The response from the engine.
792        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        // Iterate over the responses and check for errors.
797        // Any error takes precedent over any Ok.
798        #[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                    // This is the response we care about.
807                    // Keep looking for errors after locating it.
808                    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                    // Get the source range for the command.
819                    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                // Return an error that we did not get an error or the response we wanted.
847                // This should never happen but who knows.
848                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        // Hide/show the grid.
893        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        // Hide/show the grid scale text.
915        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        // Remake the default planes, since they would have been removed after the scene was cleared.
965        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    /// Request ID returned by the HTTP request that upgraded to this WebSocket.
996    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/// State of the connection to the engine.
1006#[derive(Debug, PartialEq)]
1007pub enum SocketHealth {
1008    Active,
1009    Inactive,
1010}