kcl_lib/engine/
mod.rs

1//! Functions for managing engine communications.
2
3pub mod async_tasks;
4#[cfg(not(target_arch = "wasm32"))]
5#[cfg(feature = "engine")]
6pub mod conn;
7pub mod conn_mock;
8#[cfg(target_arch = "wasm32")]
9#[cfg(feature = "engine")]
10pub mod conn_wasm;
11
12use std::{
13    collections::HashMap,
14    sync::{
15        atomic::{AtomicUsize, Ordering},
16        Arc,
17    },
18};
19
20pub use async_tasks::AsyncTasks;
21use indexmap::IndexMap;
22#[cfg(feature = "artifact-graph")]
23use kcmc::id::ModelingCmdId;
24use kcmc::{
25    each_cmd as mcmd,
26    length_unit::LengthUnit,
27    ok_response::OkModelingCmdResponse,
28    shared::Color,
29    websocket::{
30        BatchResponse, ModelingBatch, ModelingCmdReq, ModelingSessionData, OkWebSocketResponseData, WebSocketRequest,
31        WebSocketResponse,
32    },
33    ModelingCmd,
34};
35use kittycad_modeling_cmds as kcmc;
36use parse_display::{Display, FromStr};
37use schemars::JsonSchema;
38use serde::{Deserialize, Serialize};
39use tokio::sync::RwLock;
40use uuid::Uuid;
41
42#[cfg(feature = "artifact-graph")]
43use crate::execution::ArtifactCommand;
44use crate::{
45    errors::{KclError, KclErrorDetails},
46    execution::{types::UnitLen, DefaultPlanes, IdGenerator, PlaneInfo, Point3d},
47    SourceRange,
48};
49
50lazy_static::lazy_static! {
51    pub static ref GRID_OBJECT_ID: uuid::Uuid = uuid::Uuid::parse_str("cfa78409-653d-4c26-96f1-7c45fb784840").unwrap();
52
53    pub static ref GRID_SCALE_TEXT_OBJECT_ID: uuid::Uuid = uuid::Uuid::parse_str("10782f33-f588-4668-8bcd-040502d26590").unwrap();
54
55    pub static ref DEFAULT_PLANE_INFO: IndexMap<PlaneName, PlaneInfo> = IndexMap::from([
56            (PlaneName::Xy,PlaneInfo{
57                origin: Point3d::new(0.0, 0.0, 0.0, UnitLen::Mm),
58                x_axis: Point3d::new(1.0, 0.0, 0.0, UnitLen::Unknown),
59                y_axis: Point3d::new(0.0, 1.0, 0.0, UnitLen::Unknown),
60            }),
61            (PlaneName::NegXy,
62           PlaneInfo{
63                origin: Point3d::new(0.0, 0.0, 0.0, UnitLen::Mm),
64                x_axis: Point3d::new(-1.0, 0.0, 0.0, UnitLen::Unknown),
65                y_axis: Point3d::new(0.0, 1.0, 0.0, UnitLen::Unknown),
66            }),
67            (PlaneName::Xz, PlaneInfo{
68                origin: Point3d::new(0.0, 0.0, 0.0, UnitLen::Mm),
69                x_axis: Point3d::new(1.0, 0.0, 0.0, UnitLen::Unknown),
70                y_axis: Point3d::new(0.0, 0.0, 1.0, UnitLen::Unknown),
71            }),
72            (PlaneName::NegXz, PlaneInfo{
73                origin: Point3d::new(0.0, 0.0, 0.0, UnitLen::Mm),
74                x_axis: Point3d::new(-1.0, 0.0, 0.0, UnitLen::Unknown),
75                y_axis: Point3d::new(0.0, 0.0, 1.0, UnitLen::Unknown),
76            }),
77            (PlaneName::Yz, PlaneInfo{
78                origin: Point3d::new(0.0, 0.0, 0.0, UnitLen::Mm),
79                x_axis: Point3d::new(0.0, 1.0, 0.0, UnitLen::Unknown),
80                y_axis: Point3d::new(0.0, 0.0, 1.0, UnitLen::Unknown),
81            }),
82            (PlaneName::NegYz, PlaneInfo{
83                origin: Point3d::new(0.0, 0.0, 0.0, UnitLen::Mm),
84                x_axis: Point3d::new(0.0, -1.0, 0.0, UnitLen::Unknown),
85                y_axis: Point3d::new(0.0, 0.0, 1.0, UnitLen::Unknown),
86            }),
87            ]);
88}
89
90#[derive(Default, Debug)]
91pub struct EngineStats {
92    pub commands_batched: AtomicUsize,
93    pub batches_sent: AtomicUsize,
94}
95
96impl Clone for EngineStats {
97    fn clone(&self) -> Self {
98        Self {
99            commands_batched: AtomicUsize::new(self.commands_batched.load(Ordering::Relaxed)),
100            batches_sent: AtomicUsize::new(self.batches_sent.load(Ordering::Relaxed)),
101        }
102    }
103}
104
105#[async_trait::async_trait]
106pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
107    /// Get the batch of commands to be sent to the engine.
108    fn batch(&self) -> Arc<RwLock<Vec<(WebSocketRequest, SourceRange)>>>;
109
110    /// Get the batch of end commands to be sent to the engine.
111    fn batch_end(&self) -> Arc<RwLock<IndexMap<uuid::Uuid, (WebSocketRequest, SourceRange)>>>;
112
113    /// Get the command responses from the engine.
114    fn responses(&self) -> Arc<RwLock<IndexMap<Uuid, WebSocketResponse>>>;
115
116    /// Get the artifact commands that have accumulated so far.
117    #[cfg(feature = "artifact-graph")]
118    fn artifact_commands(&self) -> Arc<RwLock<Vec<ArtifactCommand>>>;
119
120    /// Get the ids of the async commands we are waiting for.
121    fn ids_of_async_commands(&self) -> Arc<RwLock<IndexMap<Uuid, SourceRange>>>;
122
123    /// Get the async tasks we are waiting for.
124    fn async_tasks(&self) -> AsyncTasks;
125
126    /// Take the batch of commands that have accumulated so far and clear them.
127    async fn take_batch(&self) -> Vec<(WebSocketRequest, SourceRange)> {
128        std::mem::take(&mut *self.batch().write().await)
129    }
130
131    /// Take the batch of end commands that have accumulated so far and clear them.
132    async fn take_batch_end(&self) -> IndexMap<Uuid, (WebSocketRequest, SourceRange)> {
133        std::mem::take(&mut *self.batch_end().write().await)
134    }
135
136    /// Clear all artifact commands that have accumulated so far.
137    #[cfg(feature = "artifact-graph")]
138    async fn clear_artifact_commands(&self) {
139        self.artifact_commands().write().await.clear();
140    }
141
142    /// Take the artifact commands that have accumulated so far and clear them.
143    #[cfg(feature = "artifact-graph")]
144    async fn take_artifact_commands(&self) -> Vec<ArtifactCommand> {
145        std::mem::take(&mut *self.artifact_commands().write().await)
146    }
147
148    /// Take the ids of async commands that have accumulated so far and clear them.
149    async fn take_ids_of_async_commands(&self) -> IndexMap<Uuid, SourceRange> {
150        std::mem::take(&mut *self.ids_of_async_commands().write().await)
151    }
152
153    /// Take the responses that have accumulated so far and clear them.
154    async fn take_responses(&self) -> IndexMap<Uuid, WebSocketResponse> {
155        std::mem::take(&mut *self.responses().write().await)
156    }
157
158    /// Get the default planes.
159    fn get_default_planes(&self) -> Arc<RwLock<Option<DefaultPlanes>>>;
160
161    fn stats(&self) -> &EngineStats;
162
163    /// Get the default planes, creating them if they don't exist.
164    async fn default_planes(
165        &self,
166        id_generator: &mut IdGenerator,
167        source_range: SourceRange,
168    ) -> Result<DefaultPlanes, KclError> {
169        {
170            let opt = self.get_default_planes().read().await.as_ref().cloned();
171            if let Some(planes) = opt {
172                return Ok(planes);
173            }
174        } // drop the read lock
175
176        let new_planes = self.new_default_planes(id_generator, source_range).await?;
177        *self.get_default_planes().write().await = Some(new_planes.clone());
178
179        Ok(new_planes)
180    }
181
182    /// Helpers to be called after clearing a scene.
183    /// (These really only apply to wasm for now).
184    async fn clear_scene_post_hook(
185        &self,
186        id_generator: &mut IdGenerator,
187        source_range: SourceRange,
188    ) -> Result<(), crate::errors::KclError>;
189
190    async fn clear_queues(&self) {
191        self.batch().write().await.clear();
192        self.batch_end().write().await.clear();
193        self.ids_of_async_commands().write().await.clear();
194        self.async_tasks().clear().await;
195    }
196
197    /// Fetch debug information from the peer.
198    async fn fetch_debug(&self) -> Result<(), crate::errors::KclError>;
199
200    /// Get any debug information (if requested)
201    async fn get_debug(&self) -> Option<OkWebSocketResponseData>;
202
203    /// Send a modeling command and do not wait for the response message.
204    async fn inner_fire_modeling_cmd(
205        &self,
206        id: uuid::Uuid,
207        source_range: SourceRange,
208        cmd: WebSocketRequest,
209        id_to_source_range: HashMap<Uuid, SourceRange>,
210    ) -> Result<(), crate::errors::KclError>;
211
212    /// Send a modeling command and wait for the response message.
213    async fn inner_send_modeling_cmd(
214        &self,
215        id: uuid::Uuid,
216        source_range: SourceRange,
217        cmd: WebSocketRequest,
218        id_to_source_range: HashMap<uuid::Uuid, SourceRange>,
219    ) -> Result<kcmc::websocket::WebSocketResponse, crate::errors::KclError>;
220
221    async fn clear_scene(
222        &self,
223        id_generator: &mut IdGenerator,
224        source_range: SourceRange,
225    ) -> Result<(), crate::errors::KclError> {
226        // Clear any batched commands leftover from previous scenes.
227        self.clear_queues().await;
228
229        self.batch_modeling_cmd(
230            id_generator.next_uuid(),
231            source_range,
232            &ModelingCmd::SceneClearAll(mcmd::SceneClearAll::default()),
233        )
234        .await?;
235
236        // Flush the batch queue, so clear is run right away.
237        // Otherwise the hooks below won't work.
238        self.flush_batch(false, source_range).await?;
239
240        // Ensure artifact commands are cleared so that we don't accumulate them
241        // across runs.
242        #[cfg(feature = "artifact-graph")]
243        self.clear_artifact_commands().await;
244
245        // Do the after clear scene hook.
246        self.clear_scene_post_hook(id_generator, source_range).await?;
247
248        Ok(())
249    }
250
251    /// Ensure a specific async command has been completed.
252    async fn ensure_async_command_completed(
253        &self,
254        id: uuid::Uuid,
255        source_range: Option<SourceRange>,
256    ) -> Result<OkWebSocketResponseData, KclError> {
257        let source_range = if let Some(source_range) = source_range {
258            source_range
259        } else {
260            // Look it up if we don't have it.
261            self.ids_of_async_commands()
262                .read()
263                .await
264                .get(&id)
265                .cloned()
266                .unwrap_or_default()
267        };
268
269        let current_time = instant::Instant::now();
270        while current_time.elapsed().as_secs() < 60 {
271            let responses = self.responses().read().await.clone();
272            let Some(resp) = responses.get(&id) else {
273                // Yield to the event loop so that we don’t block the UI thread.
274                // No seriously WE DO NOT WANT TO PAUSE THE WHOLE APP ON THE JS SIDE.
275                #[cfg(target_arch = "wasm32")]
276                {
277                    let duration = instant::Duration::from_millis(1);
278                    wasm_timer::Delay::new(duration).await.map_err(|err| {
279                        KclError::Internal(KclErrorDetails::new(
280                            format!("Failed to sleep: {:?}", err),
281                            vec![source_range],
282                        ))
283                    })?;
284                }
285                #[cfg(not(target_arch = "wasm32"))]
286                tokio::task::yield_now().await;
287                continue;
288            };
289
290            // If the response is an error, return it.
291            // Parsing will do that and we can ignore the result, we don't care.
292            let response = self.parse_websocket_response(resp.clone(), source_range)?;
293            return Ok(response);
294        }
295
296        Err(KclError::Engine(KclErrorDetails::new(
297            "async command timed out".to_string(),
298            vec![source_range],
299        )))
300    }
301
302    /// Ensure ALL async commands have been completed.
303    async fn ensure_async_commands_completed(&self) -> Result<(), KclError> {
304        // Check if all async commands have been completed.
305        let ids = self.take_ids_of_async_commands().await;
306
307        // Try to get them from the responses.
308        for (id, source_range) in ids {
309            self.ensure_async_command_completed(id, Some(source_range)).await?;
310        }
311
312        // Make sure we check for all async tasks as well.
313        // The reason why we ignore the error here is that, if a model fillets an edge
314        // we previously called something on, it might no longer exist. In which case,
315        // the artifact graph won't care either if its gone since you can't select it
316        // anymore anyways.
317        if let Err(err) = self.async_tasks().join_all().await {
318            crate::log::logln!("Error waiting for async tasks (this is typically fine and just means that an edge became something else): {:?}", err);
319        }
320
321        // Flush the batch to make sure nothing remains.
322        self.flush_batch(true, SourceRange::default()).await?;
323
324        Ok(())
325    }
326
327    /// Set the visibility of edges.
328    async fn set_edge_visibility(
329        &self,
330        visible: bool,
331        source_range: SourceRange,
332        id_generator: &mut IdGenerator,
333    ) -> Result<(), crate::errors::KclError> {
334        self.batch_modeling_cmd(
335            id_generator.next_uuid(),
336            source_range,
337            &ModelingCmd::from(mcmd::EdgeLinesVisible { hidden: !visible }),
338        )
339        .await?;
340
341        Ok(())
342    }
343
344    #[cfg(feature = "artifact-graph")]
345    async fn handle_artifact_command(
346        &self,
347        cmd: &ModelingCmd,
348        cmd_id: ModelingCmdId,
349        id_to_source_range: &HashMap<Uuid, SourceRange>,
350    ) -> Result<(), KclError> {
351        let cmd_id = *cmd_id.as_ref();
352        let range = id_to_source_range
353            .get(&cmd_id)
354            .copied()
355            .ok_or_else(|| KclError::internal(format!("Failed to get source range for command ID: {:?}", cmd_id)))?;
356
357        // Add artifact command.
358        self.artifact_commands().write().await.push(ArtifactCommand {
359            cmd_id,
360            range,
361            command: cmd.clone(),
362        });
363        Ok(())
364    }
365
366    /// Re-run the command to apply the settings.
367    async fn reapply_settings(
368        &self,
369        settings: &crate::ExecutorSettings,
370        source_range: SourceRange,
371        id_generator: &mut IdGenerator,
372    ) -> Result<(), crate::errors::KclError> {
373        // Set the edge visibility.
374        self.set_edge_visibility(settings.highlight_edges, source_range, id_generator)
375            .await?;
376
377        // Send the command to show the grid.
378        self.modify_grid(!settings.show_grid, source_range, id_generator)
379            .await?;
380
381        // We do not have commands for changing ssao on the fly.
382
383        // Flush the batch queue, so the settings are applied right away.
384        self.flush_batch(false, source_range).await?;
385
386        Ok(())
387    }
388
389    // Add a modeling command to the batch but don't fire it right away.
390    async fn batch_modeling_cmd(
391        &self,
392        id: uuid::Uuid,
393        source_range: SourceRange,
394        cmd: &ModelingCmd,
395    ) -> Result<(), crate::errors::KclError> {
396        let req = WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
397            cmd: cmd.clone(),
398            cmd_id: id.into(),
399        });
400
401        // Add cmd to the batch.
402        self.batch().write().await.push((req, source_range));
403        self.stats().commands_batched.fetch_add(1, Ordering::Relaxed);
404
405        Ok(())
406    }
407
408    // Add a vector of modeling commands to the batch but don't fire it right away.
409    // This allows you to force them all to be added together in the same order.
410    // When we are running things in parallel this prevents race conditions that might come
411    // if specific commands are run before others.
412    async fn batch_modeling_cmds(
413        &self,
414        source_range: SourceRange,
415        cmds: &[ModelingCmdReq],
416    ) -> Result<(), crate::errors::KclError> {
417        // Add cmds to the batch.
418        let mut extended_cmds = Vec::with_capacity(cmds.len());
419        for cmd in cmds {
420            extended_cmds.push((WebSocketRequest::ModelingCmdReq(cmd.clone()), source_range));
421        }
422        self.stats()
423            .commands_batched
424            .fetch_add(extended_cmds.len(), Ordering::Relaxed);
425        self.batch().write().await.extend(extended_cmds);
426
427        Ok(())
428    }
429
430    /// Add a command to the batch that needs to be executed at the very end.
431    /// This for stuff like fillets or chamfers where if we execute too soon the
432    /// engine will eat the ID and we can't reference it for other commands.
433    async fn batch_end_cmd(
434        &self,
435        id: uuid::Uuid,
436        source_range: SourceRange,
437        cmd: &ModelingCmd,
438    ) -> Result<(), crate::errors::KclError> {
439        let req = WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
440            cmd: cmd.clone(),
441            cmd_id: id.into(),
442        });
443
444        // Add cmd to the batch end.
445        self.batch_end().write().await.insert(id, (req, source_range));
446        self.stats().commands_batched.fetch_add(1, Ordering::Relaxed);
447        Ok(())
448    }
449
450    /// Send the modeling cmd and wait for the response.
451    async fn send_modeling_cmd(
452        &self,
453        id: uuid::Uuid,
454        source_range: SourceRange,
455        cmd: &ModelingCmd,
456    ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
457        let mut requests = self.take_batch().await.clone();
458
459        // Add the command to the batch.
460        requests.push((
461            WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
462                cmd: cmd.clone(),
463                cmd_id: id.into(),
464            }),
465            source_range,
466        ));
467        self.stats().commands_batched.fetch_add(1, Ordering::Relaxed);
468
469        // Flush the batch queue.
470        self.run_batch(requests, source_range).await
471    }
472
473    /// Send the modeling cmd async and don't wait for the response.
474    /// Add it to our list of async commands.
475    async fn async_modeling_cmd(
476        &self,
477        id: uuid::Uuid,
478        source_range: SourceRange,
479        cmd: &ModelingCmd,
480    ) -> Result<(), crate::errors::KclError> {
481        // Add the command ID to the list of async commands.
482        self.ids_of_async_commands().write().await.insert(id, source_range);
483
484        // Add to artifact commands.
485        #[cfg(feature = "artifact-graph")]
486        self.handle_artifact_command(cmd, id.into(), &HashMap::from([(id, source_range)]))
487            .await?;
488
489        // Fire off the command now, but don't wait for the response, we don't care about it.
490        self.inner_fire_modeling_cmd(
491            id,
492            source_range,
493            WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
494                cmd: cmd.clone(),
495                cmd_id: id.into(),
496            }),
497            HashMap::from([(id, source_range)]),
498        )
499        .await?;
500
501        Ok(())
502    }
503
504    /// Run the batch for the specific commands.
505    async fn run_batch(
506        &self,
507        orig_requests: Vec<(WebSocketRequest, SourceRange)>,
508        source_range: SourceRange,
509    ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
510        // Return early if we have no commands to send.
511        if orig_requests.is_empty() {
512            return Ok(OkWebSocketResponseData::Modeling {
513                modeling_response: OkModelingCmdResponse::Empty {},
514            });
515        }
516
517        let requests: Vec<ModelingCmdReq> = orig_requests
518            .iter()
519            .filter_map(|(val, _)| match val {
520                WebSocketRequest::ModelingCmdReq(ModelingCmdReq { cmd, cmd_id }) => Some(ModelingCmdReq {
521                    cmd: cmd.clone(),
522                    cmd_id: *cmd_id,
523                }),
524                _ => None,
525            })
526            .collect();
527
528        let batched_requests = WebSocketRequest::ModelingCmdBatchReq(ModelingBatch {
529            requests,
530            batch_id: uuid::Uuid::new_v4().into(),
531            responses: true,
532        });
533
534        let final_req = if orig_requests.len() == 1 {
535            // We can unwrap here because we know the batch has only one element.
536            orig_requests.first().unwrap().0.clone()
537        } else {
538            batched_requests
539        };
540
541        // Create the map of original command IDs to source range.
542        // This is for the wasm side, kurt needs it for selections.
543        let mut id_to_source_range = HashMap::new();
544        for (req, range) in orig_requests.iter() {
545            match req {
546                WebSocketRequest::ModelingCmdReq(ModelingCmdReq { cmd: _, cmd_id }) => {
547                    id_to_source_range.insert(Uuid::from(*cmd_id), *range);
548                }
549                _ => {
550                    return Err(KclError::Engine(KclErrorDetails::new(
551                        format!("The request is not a modeling command: {:?}", req),
552                        vec![*range],
553                    )));
554                }
555            }
556        }
557
558        // Do the artifact commands.
559        #[cfg(feature = "artifact-graph")]
560        for (req, _) in orig_requests.iter() {
561            match &req {
562                WebSocketRequest::ModelingCmdBatchReq(ModelingBatch { requests, .. }) => {
563                    for request in requests {
564                        self.handle_artifact_command(&request.cmd, request.cmd_id, &id_to_source_range)
565                            .await?;
566                    }
567                }
568                WebSocketRequest::ModelingCmdReq(request) => {
569                    self.handle_artifact_command(&request.cmd, request.cmd_id, &id_to_source_range)
570                        .await?;
571                }
572                _ => {}
573            }
574        }
575
576        self.stats().batches_sent.fetch_add(1, Ordering::Relaxed);
577
578        // We pop off the responses to cleanup our mappings.
579        match final_req {
580            WebSocketRequest::ModelingCmdBatchReq(ModelingBatch {
581                ref requests,
582                batch_id,
583                responses: _,
584            }) => {
585                // Get the last command ID.
586                let last_id = requests.last().unwrap().cmd_id;
587                let ws_resp = self
588                    .inner_send_modeling_cmd(batch_id.into(), source_range, final_req, id_to_source_range.clone())
589                    .await?;
590                let response = self.parse_websocket_response(ws_resp, source_range)?;
591
592                // If we have a batch response, we want to return the specific id we care about.
593                if let OkWebSocketResponseData::ModelingBatch { responses } = response {
594                    let responses = responses.into_iter().map(|(k, v)| (Uuid::from(k), v)).collect();
595                    self.parse_batch_responses(last_id.into(), id_to_source_range, responses)
596                } else {
597                    // We should never get here.
598                    Err(KclError::Engine(KclErrorDetails::new(
599                        format!("Failed to get batch response: {:?}", response),
600                        vec![source_range],
601                    )))
602                }
603            }
604            WebSocketRequest::ModelingCmdReq(ModelingCmdReq { cmd: _, cmd_id }) => {
605                // You are probably wondering why we can't just return the source range we were
606                // passed with the function. Well this is actually really important.
607                // If this is the last command in the batch and there is only one and we've reached
608                // the end of the file, this will trigger a flush batch function, but it will just
609                // send default or the end of the file as it's source range not the origin of the
610                // request so we need the original request source range in case the engine returns
611                // an error.
612                let source_range = id_to_source_range.get(cmd_id.as_ref()).cloned().ok_or_else(|| {
613                    KclError::Engine(KclErrorDetails::new(
614                        format!("Failed to get source range for command ID: {:?}", cmd_id),
615                        vec![],
616                    ))
617                })?;
618                let ws_resp = self
619                    .inner_send_modeling_cmd(cmd_id.into(), source_range, final_req, id_to_source_range)
620                    .await?;
621                self.parse_websocket_response(ws_resp, source_range)
622            }
623            _ => Err(KclError::Engine(KclErrorDetails::new(
624                format!("The final request is not a modeling command: {:?}", final_req),
625                vec![source_range],
626            ))),
627        }
628    }
629
630    /// Force flush the batch queue.
631    async fn flush_batch(
632        &self,
633        // Whether or not to flush the end commands as well.
634        // We only do this at the very end of the file.
635        batch_end: bool,
636        source_range: SourceRange,
637    ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
638        let all_requests = if batch_end {
639            let mut requests = self.take_batch().await.clone();
640            requests.extend(self.take_batch_end().await.values().cloned());
641            requests
642        } else {
643            self.take_batch().await.clone()
644        };
645
646        self.run_batch(all_requests, source_range).await
647    }
648
649    async fn make_default_plane(
650        &self,
651        plane_id: uuid::Uuid,
652        info: &PlaneInfo,
653        color: Option<Color>,
654        source_range: SourceRange,
655        id_generator: &mut IdGenerator,
656    ) -> Result<uuid::Uuid, KclError> {
657        // Create new default planes.
658        let default_size = 100.0;
659
660        self.batch_modeling_cmd(
661            plane_id,
662            source_range,
663            &ModelingCmd::from(mcmd::MakePlane {
664                clobber: false,
665                origin: info.origin.into(),
666                size: LengthUnit(default_size),
667                x_axis: info.x_axis.into(),
668                y_axis: info.y_axis.into(),
669                hide: Some(true),
670            }),
671        )
672        .await?;
673
674        if let Some(color) = color {
675            // Set the color.
676            self.batch_modeling_cmd(
677                id_generator.next_uuid(),
678                source_range,
679                &ModelingCmd::from(mcmd::PlaneSetColor { color, plane_id }),
680            )
681            .await?;
682        }
683
684        Ok(plane_id)
685    }
686
687    async fn new_default_planes(
688        &self,
689        id_generator: &mut IdGenerator,
690        source_range: SourceRange,
691    ) -> Result<DefaultPlanes, KclError> {
692        let plane_settings: Vec<(PlaneName, Uuid, Option<Color>)> = vec![
693            (
694                PlaneName::Xy,
695                id_generator.next_uuid(),
696                Some(Color {
697                    r: 0.7,
698                    g: 0.28,
699                    b: 0.28,
700                    a: 0.4,
701                }),
702            ),
703            (
704                PlaneName::Yz,
705                id_generator.next_uuid(),
706                Some(Color {
707                    r: 0.28,
708                    g: 0.7,
709                    b: 0.28,
710                    a: 0.4,
711                }),
712            ),
713            (
714                PlaneName::Xz,
715                id_generator.next_uuid(),
716                Some(Color {
717                    r: 0.28,
718                    g: 0.28,
719                    b: 0.7,
720                    a: 0.4,
721                }),
722            ),
723            (PlaneName::NegXy, id_generator.next_uuid(), None),
724            (PlaneName::NegYz, id_generator.next_uuid(), None),
725            (PlaneName::NegXz, id_generator.next_uuid(), None),
726        ];
727
728        let mut planes = HashMap::new();
729        for (name, plane_id, color) in plane_settings {
730            let info = DEFAULT_PLANE_INFO.get(&name).ok_or_else(|| {
731                // We should never get here.
732                KclError::Engine(KclErrorDetails::new(
733                    format!("Failed to get default plane info for: {:?}", name),
734                    vec![source_range],
735                ))
736            })?;
737            planes.insert(
738                name,
739                self.make_default_plane(plane_id, info, color, source_range, id_generator)
740                    .await?,
741            );
742        }
743
744        // Flush the batch queue, so these planes are created right away.
745        self.flush_batch(false, source_range).await?;
746
747        Ok(DefaultPlanes {
748            xy: planes[&PlaneName::Xy],
749            neg_xy: planes[&PlaneName::NegXy],
750            xz: planes[&PlaneName::Xz],
751            neg_xz: planes[&PlaneName::NegXz],
752            yz: planes[&PlaneName::Yz],
753            neg_yz: planes[&PlaneName::NegYz],
754        })
755    }
756
757    fn parse_websocket_response(
758        &self,
759        response: WebSocketResponse,
760        source_range: SourceRange,
761    ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
762        match response {
763            WebSocketResponse::Success(success) => Ok(success.resp),
764            WebSocketResponse::Failure(fail) => {
765                let _request_id = fail.request_id;
766                Err(KclError::Engine(KclErrorDetails::new(
767                    fail.errors
768                        .iter()
769                        .map(|e| e.message.clone())
770                        .collect::<Vec<_>>()
771                        .join("\n"),
772                    vec![source_range],
773                )))
774            }
775        }
776    }
777
778    fn parse_batch_responses(
779        &self,
780        // The last response we are looking for.
781        id: uuid::Uuid,
782        // The mapping of source ranges to command IDs.
783        id_to_source_range: HashMap<uuid::Uuid, SourceRange>,
784        // The response from the engine.
785        responses: HashMap<uuid::Uuid, BatchResponse>,
786    ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
787        // Iterate over the responses and check for errors.
788        #[expect(
789            clippy::iter_over_hash_type,
790            reason = "modeling command uses a HashMap and keys are random, so we don't really have a choice"
791        )]
792        for (cmd_id, resp) in responses.iter() {
793            match resp {
794                BatchResponse::Success { response } => {
795                    if cmd_id == &id {
796                        // This is the response we care about.
797                        return Ok(OkWebSocketResponseData::Modeling {
798                            modeling_response: response.clone(),
799                        });
800                    } else {
801                        // Continue the loop if this is not the response we care about.
802                        continue;
803                    }
804                }
805                BatchResponse::Failure { errors } => {
806                    // Get the source range for the command.
807                    let source_range = id_to_source_range.get(cmd_id).cloned().ok_or_else(|| {
808                        KclError::Engine(KclErrorDetails::new(
809                            format!("Failed to get source range for command ID: {:?}", cmd_id),
810                            vec![],
811                        ))
812                    })?;
813                    return Err(KclError::Engine(KclErrorDetails::new(
814                        errors.iter().map(|e| e.message.clone()).collect::<Vec<_>>().join("\n"),
815                        vec![source_range],
816                    )));
817                }
818            }
819        }
820
821        // Return an error that we did not get an error or the response we wanted.
822        // This should never happen but who knows.
823        Err(KclError::Engine(KclErrorDetails::new(
824            format!("Failed to find response for command ID: {:?}", id),
825            vec![],
826        )))
827    }
828
829    async fn modify_grid(
830        &self,
831        hidden: bool,
832        source_range: SourceRange,
833        id_generator: &mut IdGenerator,
834    ) -> Result<(), KclError> {
835        // Hide/show the grid.
836        self.batch_modeling_cmd(
837            id_generator.next_uuid(),
838            source_range,
839            &ModelingCmd::from(mcmd::ObjectVisible {
840                hidden,
841                object_id: *GRID_OBJECT_ID,
842            }),
843        )
844        .await?;
845
846        // Hide/show the grid scale text.
847        self.batch_modeling_cmd(
848            id_generator.next_uuid(),
849            source_range,
850            &ModelingCmd::from(mcmd::ObjectVisible {
851                hidden,
852                object_id: *GRID_SCALE_TEXT_OBJECT_ID,
853            }),
854        )
855        .await?;
856
857        Ok(())
858    }
859
860    /// Get session data, if it has been received.
861    /// Returns None if the server never sent it.
862    async fn get_session_data(&self) -> Option<ModelingSessionData> {
863        None
864    }
865
866    /// Close the engine connection and wait for it to finish.
867    async fn close(&self);
868}
869
870#[derive(Debug, Hash, Eq, Copy, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Display, FromStr)]
871#[ts(export)]
872#[serde(rename_all = "camelCase")]
873pub enum PlaneName {
874    /// The XY plane.
875    #[display("XY")]
876    Xy,
877    /// The opposite side of the XY plane.
878    #[display("-XY")]
879    NegXy,
880    /// The XZ plane.
881    #[display("XZ")]
882    Xz,
883    /// The opposite side of the XZ plane.
884    #[display("-XZ")]
885    NegXz,
886    /// The YZ plane.
887    #[display("YZ")]
888    Yz,
889    /// The opposite side of the YZ plane.
890    #[display("-YZ")]
891    NegYz,
892}
893
894/// Create a new zoo api client.
895#[cfg(not(target_arch = "wasm32"))]
896pub fn new_zoo_client(token: Option<String>, engine_addr: Option<String>) -> anyhow::Result<kittycad::Client> {
897    let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"),);
898    let http_client = reqwest::Client::builder()
899        .user_agent(user_agent)
900        // For file conversions we need this to be long.
901        .timeout(std::time::Duration::from_secs(600))
902        .connect_timeout(std::time::Duration::from_secs(60));
903    let ws_client = reqwest::Client::builder()
904        .user_agent(user_agent)
905        // For file conversions we need this to be long.
906        .timeout(std::time::Duration::from_secs(600))
907        .connect_timeout(std::time::Duration::from_secs(60))
908        .connection_verbose(true)
909        .tcp_keepalive(std::time::Duration::from_secs(600))
910        .http1_only();
911
912    let zoo_token_env = std::env::var("ZOO_API_TOKEN");
913
914    let token = if let Some(token) = token {
915        token
916    } else if let Ok(token) = std::env::var("KITTYCAD_API_TOKEN") {
917        if let Ok(zoo_token) = zoo_token_env {
918            if zoo_token != token {
919                return Err(anyhow::anyhow!(
920                    "Both environment variables KITTYCAD_API_TOKEN=`{}` and ZOO_API_TOKEN=`{}` are set. Use only one.",
921                    token,
922                    zoo_token
923                ));
924            }
925        }
926        token
927    } else if let Ok(token) = zoo_token_env {
928        token
929    } else {
930        return Err(anyhow::anyhow!(
931            "No API token found in environment variables. Use KITTYCAD_API_TOKEN or ZOO_API_TOKEN"
932        ));
933    };
934
935    // Create the client.
936    let mut client = kittycad::Client::new_from_reqwest(token, http_client, ws_client);
937    // Set an engine address if it's set.
938    let kittycad_host_env = std::env::var("KITTYCAD_HOST");
939    if let Some(addr) = engine_addr {
940        client.set_base_url(addr);
941    } else if let Ok(addr) = std::env::var("ZOO_HOST") {
942        if let Ok(kittycad_host) = kittycad_host_env {
943            if kittycad_host != addr {
944                return Err(anyhow::anyhow!(
945                    "Both environment variables KITTYCAD_HOST=`{}` and ZOO_HOST=`{}` are set. Use only one.",
946                    kittycad_host,
947                    addr
948                ));
949            }
950        }
951        client.set_base_url(addr);
952    } else if let Ok(addr) = kittycad_host_env {
953        client.set_base_url(addr);
954    }
955
956    Ok(client)
957}