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