1pub mod async_tasks;
4#[cfg(not(target_arch = "wasm32"))]
5#[cfg(feature = "engine")]
6pub mod conn;
7pub mod conn_mock;
8#[cfg(target_arch = "wasm32")]
9#[cfg(feature = "engine")]
10pub mod conn_wasm;
11
12use std::{
13 collections::HashMap,
14 sync::{
15 atomic::{AtomicUsize, Ordering},
16 Arc,
17 },
18};
19
20pub use async_tasks::AsyncTasks;
21use indexmap::IndexMap;
22#[cfg(feature = "artifact-graph")]
23use kcmc::id::ModelingCmdId;
24use kcmc::{
25 each_cmd as mcmd,
26 length_unit::LengthUnit,
27 ok_response::OkModelingCmdResponse,
28 shared::Color,
29 websocket::{
30 BatchResponse, ModelingBatch, ModelingCmdReq, ModelingSessionData, OkWebSocketResponseData, WebSocketRequest,
31 WebSocketResponse,
32 },
33 ModelingCmd,
34};
35use kittycad_modeling_cmds as kcmc;
36use parse_display::{Display, FromStr};
37use schemars::JsonSchema;
38use serde::{Deserialize, Serialize};
39use tokio::sync::RwLock;
40use uuid::Uuid;
41
42#[cfg(feature = "artifact-graph")]
43use crate::execution::ArtifactCommand;
44use crate::{
45 errors::{KclError, KclErrorDetails},
46 execution::{types::UnitLen, DefaultPlanes, IdGenerator, PlaneInfo, Point3d},
47 SourceRange,
48};
49
50lazy_static::lazy_static! {
51 pub static ref GRID_OBJECT_ID: uuid::Uuid = uuid::Uuid::parse_str("cfa78409-653d-4c26-96f1-7c45fb784840").unwrap();
52
53 pub static ref GRID_SCALE_TEXT_OBJECT_ID: uuid::Uuid = uuid::Uuid::parse_str("10782f33-f588-4668-8bcd-040502d26590").unwrap();
54
55 pub static ref DEFAULT_PLANE_INFO: IndexMap<PlaneName, PlaneInfo> = IndexMap::from([
56 (PlaneName::Xy,PlaneInfo{
57 origin: Point3d::new(0.0, 0.0, 0.0, UnitLen::Mm),
58 x_axis: Point3d::new(1.0, 0.0, 0.0, UnitLen::Unknown),
59 y_axis: Point3d::new(0.0, 1.0, 0.0, UnitLen::Unknown),
60 }),
61 (PlaneName::NegXy,
62 PlaneInfo{
63 origin: Point3d::new(0.0, 0.0, 0.0, UnitLen::Mm),
64 x_axis: Point3d::new(-1.0, 0.0, 0.0, UnitLen::Unknown),
65 y_axis: Point3d::new(0.0, 1.0, 0.0, UnitLen::Unknown),
66 }),
67 (PlaneName::Xz, PlaneInfo{
68 origin: Point3d::new(0.0, 0.0, 0.0, UnitLen::Mm),
69 x_axis: Point3d::new(1.0, 0.0, 0.0, UnitLen::Unknown),
70 y_axis: Point3d::new(0.0, 0.0, 1.0, UnitLen::Unknown),
71 }),
72 (PlaneName::NegXz, PlaneInfo{
73 origin: Point3d::new(0.0, 0.0, 0.0, UnitLen::Mm),
74 x_axis: Point3d::new(-1.0, 0.0, 0.0, UnitLen::Unknown),
75 y_axis: Point3d::new(0.0, 0.0, 1.0, UnitLen::Unknown),
76 }),
77 (PlaneName::Yz, PlaneInfo{
78 origin: Point3d::new(0.0, 0.0, 0.0, UnitLen::Mm),
79 x_axis: Point3d::new(0.0, 1.0, 0.0, UnitLen::Unknown),
80 y_axis: Point3d::new(0.0, 0.0, 1.0, UnitLen::Unknown),
81 }),
82 (PlaneName::NegYz, PlaneInfo{
83 origin: Point3d::new(0.0, 0.0, 0.0, UnitLen::Mm),
84 x_axis: Point3d::new(0.0, -1.0, 0.0, UnitLen::Unknown),
85 y_axis: Point3d::new(0.0, 0.0, 1.0, UnitLen::Unknown),
86 }),
87 ]);
88}
89
90#[derive(Default, Debug)]
91pub struct EngineStats {
92 pub commands_batched: AtomicUsize,
93 pub batches_sent: AtomicUsize,
94}
95
96impl Clone for EngineStats {
97 fn clone(&self) -> Self {
98 Self {
99 commands_batched: AtomicUsize::new(self.commands_batched.load(Ordering::Relaxed)),
100 batches_sent: AtomicUsize::new(self.batches_sent.load(Ordering::Relaxed)),
101 }
102 }
103}
104
105#[async_trait::async_trait]
106pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
107 fn batch(&self) -> Arc<RwLock<Vec<(WebSocketRequest, SourceRange)>>>;
109
110 fn batch_end(&self) -> Arc<RwLock<IndexMap<uuid::Uuid, (WebSocketRequest, SourceRange)>>>;
112
113 fn responses(&self) -> Arc<RwLock<IndexMap<Uuid, WebSocketResponse>>>;
115
116 #[cfg(feature = "artifact-graph")]
118 fn artifact_commands(&self) -> Arc<RwLock<Vec<ArtifactCommand>>>;
119
120 fn ids_of_async_commands(&self) -> Arc<RwLock<IndexMap<Uuid, SourceRange>>>;
122
123 fn async_tasks(&self) -> AsyncTasks;
125
126 async fn take_batch(&self) -> Vec<(WebSocketRequest, SourceRange)> {
128 std::mem::take(&mut *self.batch().write().await)
129 }
130
131 async fn take_batch_end(&self) -> IndexMap<Uuid, (WebSocketRequest, SourceRange)> {
133 std::mem::take(&mut *self.batch_end().write().await)
134 }
135
136 #[cfg(feature = "artifact-graph")]
138 async fn clear_artifact_commands(&self) {
139 self.artifact_commands().write().await.clear();
140 }
141
142 #[cfg(feature = "artifact-graph")]
144 async fn take_artifact_commands(&self) -> Vec<ArtifactCommand> {
145 std::mem::take(&mut *self.artifact_commands().write().await)
146 }
147
148 async fn take_ids_of_async_commands(&self) -> IndexMap<Uuid, SourceRange> {
150 std::mem::take(&mut *self.ids_of_async_commands().write().await)
151 }
152
153 async fn take_responses(&self) -> IndexMap<Uuid, WebSocketResponse> {
155 std::mem::take(&mut *self.responses().write().await)
156 }
157
158 fn get_default_planes(&self) -> Arc<RwLock<Option<DefaultPlanes>>>;
160
161 fn stats(&self) -> &EngineStats;
162
163 async fn default_planes(
165 &self,
166 id_generator: &mut IdGenerator,
167 source_range: SourceRange,
168 ) -> Result<DefaultPlanes, KclError> {
169 {
170 let opt = self.get_default_planes().read().await.as_ref().cloned();
171 if let Some(planes) = opt {
172 return Ok(planes);
173 }
174 } let new_planes = self.new_default_planes(id_generator, source_range).await?;
177 *self.get_default_planes().write().await = Some(new_planes.clone());
178
179 Ok(new_planes)
180 }
181
182 async fn clear_scene_post_hook(
185 &self,
186 id_generator: &mut IdGenerator,
187 source_range: SourceRange,
188 ) -> Result<(), crate::errors::KclError>;
189
190 async fn clear_queues(&self) {
191 self.batch().write().await.clear();
192 self.batch_end().write().await.clear();
193 self.ids_of_async_commands().write().await.clear();
194 self.async_tasks().clear().await;
195 }
196
197 async fn fetch_debug(&self) -> Result<(), crate::errors::KclError>;
199
200 async fn get_debug(&self) -> Option<OkWebSocketResponseData>;
202
203 async fn inner_fire_modeling_cmd(
205 &self,
206 id: uuid::Uuid,
207 source_range: SourceRange,
208 cmd: WebSocketRequest,
209 id_to_source_range: HashMap<Uuid, SourceRange>,
210 ) -> Result<(), crate::errors::KclError>;
211
212 async fn inner_send_modeling_cmd(
214 &self,
215 id: uuid::Uuid,
216 source_range: SourceRange,
217 cmd: WebSocketRequest,
218 id_to_source_range: HashMap<uuid::Uuid, SourceRange>,
219 ) -> Result<kcmc::websocket::WebSocketResponse, crate::errors::KclError>;
220
221 async fn clear_scene(
222 &self,
223 id_generator: &mut IdGenerator,
224 source_range: SourceRange,
225 ) -> Result<(), crate::errors::KclError> {
226 self.clear_queues().await;
228
229 self.batch_modeling_cmd(
230 id_generator.next_uuid(),
231 source_range,
232 &ModelingCmd::SceneClearAll(mcmd::SceneClearAll::default()),
233 )
234 .await?;
235
236 self.flush_batch(false, source_range).await?;
239
240 #[cfg(feature = "artifact-graph")]
243 self.clear_artifact_commands().await;
244
245 self.clear_scene_post_hook(id_generator, source_range).await?;
247
248 Ok(())
249 }
250
251 async fn ensure_async_command_completed(
253 &self,
254 id: uuid::Uuid,
255 source_range: Option<SourceRange>,
256 ) -> Result<OkWebSocketResponseData, KclError> {
257 let source_range = if let Some(source_range) = source_range {
258 source_range
259 } else {
260 self.ids_of_async_commands()
262 .read()
263 .await
264 .get(&id)
265 .cloned()
266 .unwrap_or_default()
267 };
268
269 let current_time = instant::Instant::now();
270 while current_time.elapsed().as_secs() < 60 {
271 let responses = self.responses().read().await.clone();
272 let Some(resp) = responses.get(&id) else {
273 #[cfg(target_arch = "wasm32")]
276 {
277 let duration = instant::Duration::from_millis(1);
278 wasm_timer::Delay::new(duration).await.map_err(|err| {
279 KclError::Internal(KclErrorDetails {
280 message: format!("Failed to sleep: {:?}", err),
281 source_ranges: vec![source_range],
282 })
283 })?;
284 }
285 #[cfg(not(target_arch = "wasm32"))]
286 tokio::task::yield_now().await;
287 continue;
288 };
289
290 let response = self.parse_websocket_response(resp.clone(), source_range)?;
293 return Ok(response);
294 }
295
296 Err(KclError::Engine(KclErrorDetails {
297 message: "async command timed out".to_string(),
298 source_ranges: vec![source_range],
299 }))
300 }
301
302 async fn ensure_async_commands_completed(&self) -> Result<(), KclError> {
304 let ids = self.take_ids_of_async_commands().await;
306
307 for (id, source_range) in ids {
309 self.ensure_async_command_completed(id, Some(source_range)).await?;
310 }
311
312 if let Err(err) = self.async_tasks().join_all().await {
318 crate::log::logln!("Error waiting for async tasks (this is typically fine and just means that an edge became something else): {:?}", err);
319 }
320
321 self.flush_batch(true, SourceRange::default()).await?;
323
324 Ok(())
325 }
326
327 async fn set_edge_visibility(
329 &self,
330 visible: bool,
331 source_range: SourceRange,
332 id_generator: &mut IdGenerator,
333 ) -> Result<(), crate::errors::KclError> {
334 self.batch_modeling_cmd(
335 id_generator.next_uuid(),
336 source_range,
337 &ModelingCmd::from(mcmd::EdgeLinesVisible { hidden: !visible }),
338 )
339 .await?;
340
341 Ok(())
342 }
343
344 #[cfg(feature = "artifact-graph")]
345 async fn handle_artifact_command(
346 &self,
347 cmd: &ModelingCmd,
348 cmd_id: ModelingCmdId,
349 id_to_source_range: &HashMap<Uuid, SourceRange>,
350 ) -> Result<(), KclError> {
351 let cmd_id = *cmd_id.as_ref();
352 let range = id_to_source_range
353 .get(&cmd_id)
354 .copied()
355 .ok_or_else(|| KclError::internal(format!("Failed to get source range for command ID: {:?}", cmd_id)))?;
356
357 self.artifact_commands().write().await.push(ArtifactCommand {
359 cmd_id,
360 range,
361 command: cmd.clone(),
362 });
363 Ok(())
364 }
365
366 async fn reapply_settings(
368 &self,
369 settings: &crate::ExecutorSettings,
370 source_range: SourceRange,
371 id_generator: &mut IdGenerator,
372 ) -> Result<(), crate::errors::KclError> {
373 self.set_edge_visibility(settings.highlight_edges, source_range, id_generator)
375 .await?;
376
377 self.modify_grid(!settings.show_grid, source_range, id_generator)
379 .await?;
380
381 self.flush_batch(false, source_range).await?;
385
386 Ok(())
387 }
388
389 async fn batch_modeling_cmd(
391 &self,
392 id: uuid::Uuid,
393 source_range: SourceRange,
394 cmd: &ModelingCmd,
395 ) -> Result<(), crate::errors::KclError> {
396 let req = WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
397 cmd: cmd.clone(),
398 cmd_id: id.into(),
399 });
400
401 self.batch().write().await.push((req, source_range));
403 self.stats().commands_batched.fetch_add(1, Ordering::Relaxed);
404
405 Ok(())
406 }
407
408 async fn batch_modeling_cmds(
413 &self,
414 source_range: SourceRange,
415 cmds: &[ModelingCmdReq],
416 ) -> Result<(), crate::errors::KclError> {
417 let mut extended_cmds = Vec::with_capacity(cmds.len());
419 for cmd in cmds {
420 extended_cmds.push((WebSocketRequest::ModelingCmdReq(cmd.clone()), source_range));
421 }
422 self.stats()
423 .commands_batched
424 .fetch_add(extended_cmds.len(), Ordering::Relaxed);
425 self.batch().write().await.extend(extended_cmds);
426
427 Ok(())
428 }
429
430 async fn batch_end_cmd(
434 &self,
435 id: uuid::Uuid,
436 source_range: SourceRange,
437 cmd: &ModelingCmd,
438 ) -> Result<(), crate::errors::KclError> {
439 let req = WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
440 cmd: cmd.clone(),
441 cmd_id: id.into(),
442 });
443
444 self.batch_end().write().await.insert(id, (req, source_range));
446 self.stats().commands_batched.fetch_add(1, Ordering::Relaxed);
447 Ok(())
448 }
449
450 async fn send_modeling_cmd(
452 &self,
453 id: uuid::Uuid,
454 source_range: SourceRange,
455 cmd: &ModelingCmd,
456 ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
457 let mut requests = self.take_batch().await.clone();
458
459 requests.push((
461 WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
462 cmd: cmd.clone(),
463 cmd_id: id.into(),
464 }),
465 source_range,
466 ));
467 self.stats().commands_batched.fetch_add(1, Ordering::Relaxed);
468
469 self.run_batch(requests, source_range).await
471 }
472
473 async fn async_modeling_cmd(
476 &self,
477 id: uuid::Uuid,
478 source_range: SourceRange,
479 cmd: &ModelingCmd,
480 ) -> Result<(), crate::errors::KclError> {
481 self.ids_of_async_commands().write().await.insert(id, source_range);
483
484 #[cfg(feature = "artifact-graph")]
486 self.handle_artifact_command(cmd, id.into(), &HashMap::from([(id, source_range)]))
487 .await?;
488
489 self.inner_fire_modeling_cmd(
491 id,
492 source_range,
493 WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
494 cmd: cmd.clone(),
495 cmd_id: id.into(),
496 }),
497 HashMap::from([(id, source_range)]),
498 )
499 .await?;
500
501 Ok(())
502 }
503
504 async fn run_batch(
506 &self,
507 orig_requests: Vec<(WebSocketRequest, SourceRange)>,
508 source_range: SourceRange,
509 ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
510 if orig_requests.is_empty() {
512 return Ok(OkWebSocketResponseData::Modeling {
513 modeling_response: OkModelingCmdResponse::Empty {},
514 });
515 }
516
517 let requests: Vec<ModelingCmdReq> = orig_requests
518 .iter()
519 .filter_map(|(val, _)| match val {
520 WebSocketRequest::ModelingCmdReq(ModelingCmdReq { cmd, cmd_id }) => Some(ModelingCmdReq {
521 cmd: cmd.clone(),
522 cmd_id: *cmd_id,
523 }),
524 _ => None,
525 })
526 .collect();
527
528 let batched_requests = WebSocketRequest::ModelingCmdBatchReq(ModelingBatch {
529 requests,
530 batch_id: uuid::Uuid::new_v4().into(),
531 responses: true,
532 });
533
534 let final_req = if orig_requests.len() == 1 {
535 orig_requests.first().unwrap().0.clone()
537 } else {
538 batched_requests
539 };
540
541 let mut id_to_source_range = HashMap::new();
544 for (req, range) in orig_requests.iter() {
545 match req {
546 WebSocketRequest::ModelingCmdReq(ModelingCmdReq { cmd: _, cmd_id }) => {
547 id_to_source_range.insert(Uuid::from(*cmd_id), *range);
548 }
549 _ => {
550 return Err(KclError::Engine(KclErrorDetails {
551 message: format!("The request is not a modeling command: {:?}", req),
552 source_ranges: vec![*range],
553 }));
554 }
555 }
556 }
557
558 #[cfg(feature = "artifact-graph")]
560 for (req, _) in orig_requests.iter() {
561 match &req {
562 WebSocketRequest::ModelingCmdBatchReq(ModelingBatch { requests, .. }) => {
563 for request in requests {
564 self.handle_artifact_command(&request.cmd, request.cmd_id, &id_to_source_range)
565 .await?;
566 }
567 }
568 WebSocketRequest::ModelingCmdReq(request) => {
569 self.handle_artifact_command(&request.cmd, request.cmd_id, &id_to_source_range)
570 .await?;
571 }
572 _ => {}
573 }
574 }
575
576 self.stats().batches_sent.fetch_add(1, Ordering::Relaxed);
577
578 match final_req {
580 WebSocketRequest::ModelingCmdBatchReq(ModelingBatch {
581 ref requests,
582 batch_id,
583 responses: _,
584 }) => {
585 let last_id = requests.last().unwrap().cmd_id;
587 let ws_resp = self
588 .inner_send_modeling_cmd(batch_id.into(), source_range, final_req, id_to_source_range.clone())
589 .await?;
590 let response = self.parse_websocket_response(ws_resp, source_range)?;
591
592 if let OkWebSocketResponseData::ModelingBatch { responses } = response {
594 let responses = responses.into_iter().map(|(k, v)| (Uuid::from(k), v)).collect();
595 self.parse_batch_responses(last_id.into(), id_to_source_range, responses)
596 } else {
597 Err(KclError::Engine(KclErrorDetails {
599 message: format!("Failed to get batch response: {:?}", response),
600 source_ranges: vec![source_range],
601 }))
602 }
603 }
604 WebSocketRequest::ModelingCmdReq(ModelingCmdReq { cmd: _, cmd_id }) => {
605 let source_range = id_to_source_range.get(cmd_id.as_ref()).cloned().ok_or_else(|| {
613 KclError::Engine(KclErrorDetails {
614 message: format!("Failed to get source range for command ID: {:?}", cmd_id),
615 source_ranges: vec![],
616 })
617 })?;
618 let ws_resp = self
619 .inner_send_modeling_cmd(cmd_id.into(), source_range, final_req, id_to_source_range)
620 .await?;
621 self.parse_websocket_response(ws_resp, source_range)
622 }
623 _ => Err(KclError::Engine(KclErrorDetails {
624 message: format!("The final request is not a modeling command: {:?}", final_req),
625 source_ranges: vec![source_range],
626 })),
627 }
628 }
629
630 async fn flush_batch(
632 &self,
633 batch_end: bool,
636 source_range: SourceRange,
637 ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
638 let all_requests = if batch_end {
639 let mut requests = self.take_batch().await.clone();
640 requests.extend(self.take_batch_end().await.values().cloned());
641 requests
642 } else {
643 self.take_batch().await.clone()
644 };
645
646 self.run_batch(all_requests, source_range).await
647 }
648
649 async fn make_default_plane(
650 &self,
651 plane_id: uuid::Uuid,
652 info: &PlaneInfo,
653 color: Option<Color>,
654 source_range: SourceRange,
655 id_generator: &mut IdGenerator,
656 ) -> Result<uuid::Uuid, KclError> {
657 let default_size = 100.0;
659
660 self.batch_modeling_cmd(
661 plane_id,
662 source_range,
663 &ModelingCmd::from(mcmd::MakePlane {
664 clobber: false,
665 origin: info.origin.into(),
666 size: LengthUnit(default_size),
667 x_axis: info.x_axis.into(),
668 y_axis: info.y_axis.into(),
669 hide: Some(true),
670 }),
671 )
672 .await?;
673
674 if let Some(color) = color {
675 self.batch_modeling_cmd(
677 id_generator.next_uuid(),
678 source_range,
679 &ModelingCmd::from(mcmd::PlaneSetColor { color, plane_id }),
680 )
681 .await?;
682 }
683
684 Ok(plane_id)
685 }
686
687 async fn new_default_planes(
688 &self,
689 id_generator: &mut IdGenerator,
690 source_range: SourceRange,
691 ) -> Result<DefaultPlanes, KclError> {
692 let plane_settings: Vec<(PlaneName, Uuid, Option<Color>)> = vec![
693 (
694 PlaneName::Xy,
695 id_generator.next_uuid(),
696 Some(Color {
697 r: 0.7,
698 g: 0.28,
699 b: 0.28,
700 a: 0.4,
701 }),
702 ),
703 (
704 PlaneName::Yz,
705 id_generator.next_uuid(),
706 Some(Color {
707 r: 0.28,
708 g: 0.7,
709 b: 0.28,
710 a: 0.4,
711 }),
712 ),
713 (
714 PlaneName::Xz,
715 id_generator.next_uuid(),
716 Some(Color {
717 r: 0.28,
718 g: 0.28,
719 b: 0.7,
720 a: 0.4,
721 }),
722 ),
723 (PlaneName::NegXy, id_generator.next_uuid(), None),
724 (PlaneName::NegYz, id_generator.next_uuid(), None),
725 (PlaneName::NegXz, id_generator.next_uuid(), None),
726 ];
727
728 let mut planes = HashMap::new();
729 for (name, plane_id, color) in plane_settings {
730 let info = DEFAULT_PLANE_INFO.get(&name).ok_or_else(|| {
731 KclError::Engine(KclErrorDetails {
733 message: format!("Failed to get default plane info for: {:?}", name),
734 source_ranges: vec![source_range],
735 })
736 })?;
737 planes.insert(
738 name,
739 self.make_default_plane(plane_id, info, color, source_range, id_generator)
740 .await?,
741 );
742 }
743
744 self.flush_batch(false, source_range).await?;
746
747 Ok(DefaultPlanes {
748 xy: planes[&PlaneName::Xy],
749 neg_xy: planes[&PlaneName::NegXy],
750 xz: planes[&PlaneName::Xz],
751 neg_xz: planes[&PlaneName::NegXz],
752 yz: planes[&PlaneName::Yz],
753 neg_yz: planes[&PlaneName::NegYz],
754 })
755 }
756
757 fn parse_websocket_response(
758 &self,
759 response: WebSocketResponse,
760 source_range: SourceRange,
761 ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
762 match response {
763 WebSocketResponse::Success(success) => Ok(success.resp),
764 WebSocketResponse::Failure(fail) => {
765 let _request_id = fail.request_id;
766 Err(KclError::Engine(KclErrorDetails {
767 message: fail
768 .errors
769 .iter()
770 .map(|e| e.message.clone())
771 .collect::<Vec<_>>()
772 .join("\n"),
773 source_ranges: vec![source_range],
774 }))
775 }
776 }
777 }
778
779 fn parse_batch_responses(
780 &self,
781 id: uuid::Uuid,
783 id_to_source_range: HashMap<uuid::Uuid, SourceRange>,
785 responses: HashMap<uuid::Uuid, BatchResponse>,
787 ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
788 #[expect(
790 clippy::iter_over_hash_type,
791 reason = "modeling command uses a HashMap and keys are random, so we don't really have a choice"
792 )]
793 for (cmd_id, resp) in responses.iter() {
794 match resp {
795 BatchResponse::Success { response } => {
796 if cmd_id == &id {
797 return Ok(OkWebSocketResponseData::Modeling {
799 modeling_response: response.clone(),
800 });
801 } else {
802 continue;
804 }
805 }
806 BatchResponse::Failure { errors } => {
807 let source_range = id_to_source_range.get(cmd_id).cloned().ok_or_else(|| {
809 KclError::Engine(KclErrorDetails {
810 message: format!("Failed to get source range for command ID: {:?}", cmd_id),
811 source_ranges: vec![],
812 })
813 })?;
814 return Err(KclError::Engine(KclErrorDetails {
815 message: errors.iter().map(|e| e.message.clone()).collect::<Vec<_>>().join("\n"),
816 source_ranges: vec![source_range],
817 }));
818 }
819 }
820 }
821
822 Err(KclError::Engine(KclErrorDetails {
825 message: format!("Failed to find response for command ID: {:?}", id),
826 source_ranges: vec![],
827 }))
828 }
829
830 async fn modify_grid(
831 &self,
832 hidden: bool,
833 source_range: SourceRange,
834 id_generator: &mut IdGenerator,
835 ) -> Result<(), KclError> {
836 self.batch_modeling_cmd(
838 id_generator.next_uuid(),
839 source_range,
840 &ModelingCmd::from(mcmd::ObjectVisible {
841 hidden,
842 object_id: *GRID_OBJECT_ID,
843 }),
844 )
845 .await?;
846
847 self.batch_modeling_cmd(
849 id_generator.next_uuid(),
850 source_range,
851 &ModelingCmd::from(mcmd::ObjectVisible {
852 hidden,
853 object_id: *GRID_SCALE_TEXT_OBJECT_ID,
854 }),
855 )
856 .await?;
857
858 Ok(())
859 }
860
861 async fn get_session_data(&self) -> Option<ModelingSessionData> {
864 None
865 }
866
867 async fn close(&self);
869}
870
871#[derive(Debug, Hash, Eq, Copy, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Display, FromStr)]
872#[ts(export)]
873#[serde(rename_all = "camelCase")]
874pub enum PlaneName {
875 #[display("XY")]
877 Xy,
878 #[display("-XY")]
880 NegXy,
881 #[display("XZ")]
883 Xz,
884 #[display("-XZ")]
886 NegXz,
887 #[display("YZ")]
889 Yz,
890 #[display("-YZ")]
892 NegYz,
893}
894
895#[cfg(not(target_arch = "wasm32"))]
897pub fn new_zoo_client(token: Option<String>, engine_addr: Option<String>) -> anyhow::Result<kittycad::Client> {
898 let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"),);
899 let http_client = reqwest::Client::builder()
900 .user_agent(user_agent)
901 .timeout(std::time::Duration::from_secs(600))
903 .connect_timeout(std::time::Duration::from_secs(60));
904 let ws_client = reqwest::Client::builder()
905 .user_agent(user_agent)
906 .timeout(std::time::Duration::from_secs(600))
908 .connect_timeout(std::time::Duration::from_secs(60))
909 .connection_verbose(true)
910 .tcp_keepalive(std::time::Duration::from_secs(600))
911 .http1_only();
912
913 let zoo_token_env = std::env::var("ZOO_API_TOKEN");
914
915 let token = if let Some(token) = token {
916 token
917 } else if let Ok(token) = std::env::var("KITTYCAD_API_TOKEN") {
918 if let Ok(zoo_token) = zoo_token_env {
919 if zoo_token != token {
920 return Err(anyhow::anyhow!(
921 "Both environment variables KITTYCAD_API_TOKEN=`{}` and ZOO_API_TOKEN=`{}` are set. Use only one.",
922 token,
923 zoo_token
924 ));
925 }
926 }
927 token
928 } else if let Ok(token) = zoo_token_env {
929 token
930 } else {
931 return Err(anyhow::anyhow!(
932 "No API token found in environment variables. Use KITTYCAD_API_TOKEN or ZOO_API_TOKEN"
933 ));
934 };
935
936 let mut client = kittycad::Client::new_from_reqwest(token, http_client, ws_client);
938 let kittycad_host_env = std::env::var("KITTYCAD_HOST");
940 if let Some(addr) = engine_addr {
941 client.set_base_url(addr);
942 } else if let Ok(addr) = std::env::var("ZOO_HOST") {
943 if let Ok(kittycad_host) = kittycad_host_env {
944 if kittycad_host != addr {
945 return Err(anyhow::anyhow!(
946 "Both environment variables KITTYCAD_HOST=`{}` and ZOO_HOST=`{}` are set. Use only one.",
947 kittycad_host,
948 addr
949 ));
950 }
951 }
952 client.set_base_url(addr);
953 } else if let Ok(addr) = kittycad_host_env {
954 client.set_base_url(addr);
955 }
956
957 Ok(client)
958}