Skip to main content

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