kcl_lib/engine/
mod.rs

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