1#[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#[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 fn batch(&self) -> Arc<RwLock<Vec<(WebSocketRequest, SourceRange)>>>;
86
87 fn batch_end(&self) -> Arc<RwLock<IndexMap<uuid::Uuid, (WebSocketRequest, SourceRange)>>>;
89
90 fn responses(&self) -> Arc<RwLock<IndexMap<Uuid, WebSocketResponse>>>;
92
93 fn artifact_commands(&self) -> Arc<RwLock<Vec<ArtifactCommand>>>;
95
96 async fn take_batch(&self) -> Vec<(WebSocketRequest, SourceRange)> {
98 std::mem::take(&mut *self.batch().write().await)
99 }
100
101 async fn take_batch_end(&self) -> IndexMap<Uuid, (WebSocketRequest, SourceRange)> {
103 std::mem::take(&mut *self.batch_end().write().await)
104 }
105
106 async fn clear_artifact_commands(&self) {
108 self.artifact_commands().write().await.clear();
109 }
110
111 async fn take_artifact_commands(&self) -> Vec<ArtifactCommand> {
113 std::mem::take(&mut *self.artifact_commands().write().await)
114 }
115
116 async fn take_responses(&self) -> IndexMap<Uuid, WebSocketResponse> {
118 std::mem::take(&mut *self.responses().write().await)
119 }
120
121 async fn execution_kind(&self) -> ExecutionKind;
123
124 async fn replace_execution_kind(&self, execution_kind: ExecutionKind) -> ExecutionKind;
127
128 fn get_default_planes(&self) -> Arc<RwLock<Option<DefaultPlanes>>>;
130
131 fn stats(&self) -> &EngineStats;
132
133 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 } 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 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 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 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 self.set_units(Default::default(), source_range, id_generator).await?;
192
193 self.flush_batch(false, source_range).await?;
196
197 self.clear_artifact_commands().await;
200
201 self.clear_scene_post_hook(id_generator, source_range).await?;
203
204 Ok(())
205 }
206
207 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 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 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 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 self.set_edge_visibility(settings.highlight_edges, source_range, id_generator)
271 .await?;
272
273 self.modify_grid(!settings.show_grid, source_range, id_generator)
275 .await?;
276
277 self.flush_batch(false, source_range).await?;
281
282 Ok(())
283 }
284
285 async fn batch_modeling_cmd(
287 &self,
288 id: uuid::Uuid,
289 source_range: SourceRange,
290 cmd: &ModelingCmd,
291 ) -> Result<(), crate::errors::KclError> {
292 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 self.batch().write().await.push((req, source_range));
304 self.stats().commands_batched.fetch_add(1, Ordering::Relaxed);
305
306 Ok(())
307 }
308
309 async fn batch_modeling_cmds(
314 &self,
315 source_range: SourceRange,
316 cmds: &[ModelingCmdReq],
317 ) -> Result<(), crate::errors::KclError> {
318 if self.execution_kind().await.is_isolated() {
320 return Ok(());
321 }
322
323 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 async fn batch_end_cmd(
340 &self,
341 id: uuid::Uuid,
342 source_range: SourceRange,
343 cmd: &ModelingCmd,
344 ) -> Result<(), crate::errors::KclError> {
345 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 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 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 self.flush_batch(false, source_range).await
372 }
373
374 async fn flush_batch(
376 &self,
377 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 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 all_requests.first().unwrap().0.clone()
417 } else {
418 batched_requests
419 };
420
421 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 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 match final_req {
459 WebSocketRequest::ModelingCmdBatchReq(ModelingBatch {
460 ref requests,
461 batch_id,
462 responses: _,
463 }) => {
464 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 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 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 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 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 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 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 id: uuid::Uuid,
723 id_to_source_range: HashMap<uuid::Uuid, SourceRange>,
725 responses: HashMap<uuid::Uuid, BatchResponse>,
727 ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
728 #[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 return Ok(OkWebSocketResponseData::Modeling {
739 modeling_response: response.clone(),
740 });
741 } else {
742 continue;
744 }
745 }
746 BatchResponse::Failure { errors } => {
747 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 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 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 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 async fn get_session_data(&self) -> Option<ModelingSessionData> {
804 None
805 }
806
807 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 Xy,
817 NegXy,
819 Xz,
821 NegXz,
823 Yz,
825 NegYz,
827}
828
829#[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 .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 .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 let mut client = kittycad::Client::new_from_reqwest(token, http_client, ws_client);
872 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}