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