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