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