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 { hidden: !visible }),
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(mcmd::MakePlane {
621 clobber: false,
622 origin: info.origin.into(),
623 size: LengthUnit(default_size),
624 x_axis: info.x_axis.into(),
625 y_axis: info.y_axis.into(),
626 hide: Some(true),
627 }),
628 )
629 .await?;
630
631 if let Some(color) = color {
632 self.batch_modeling_cmd(
634 id_generator.next_uuid(),
635 source_range,
636 &ModelingCmd::from(mcmd::PlaneSetColor { color, plane_id }),
637 )
638 .await?;
639 }
640
641 Ok(plane_id)
642 }
643
644 async fn new_default_planes(
645 &self,
646 id_generator: &mut IdGenerator,
647 source_range: SourceRange,
648 ) -> Result<DefaultPlanes, KclError> {
649 let plane_opacity = 0.1;
650 let plane_settings: Vec<(PlaneName, Uuid, Option<Color>)> = vec![
651 (
652 PlaneName::Xy,
653 id_generator.next_uuid(),
654 Some(Color {
655 r: 0.7,
656 g: 0.28,
657 b: 0.28,
658 a: plane_opacity,
659 }),
660 ),
661 (
662 PlaneName::Yz,
663 id_generator.next_uuid(),
664 Some(Color {
665 r: 0.28,
666 g: 0.7,
667 b: 0.28,
668 a: plane_opacity,
669 }),
670 ),
671 (
672 PlaneName::Xz,
673 id_generator.next_uuid(),
674 Some(Color {
675 r: 0.28,
676 g: 0.28,
677 b: 0.7,
678 a: plane_opacity,
679 }),
680 ),
681 (PlaneName::NegXy, id_generator.next_uuid(), None),
682 (PlaneName::NegYz, id_generator.next_uuid(), None),
683 (PlaneName::NegXz, id_generator.next_uuid(), None),
684 ];
685
686 let mut planes = HashMap::new();
687 for (name, plane_id, color) in plane_settings {
688 let info = DEFAULT_PLANE_INFO.get(&name).ok_or_else(|| {
689 KclError::new_engine(KclErrorDetails::new(
691 format!("Failed to get default plane info for: {name:?}"),
692 vec![source_range],
693 ))
694 })?;
695 planes.insert(
696 name,
697 self.make_default_plane(plane_id, info, color, source_range, id_generator)
698 .await?,
699 );
700 }
701
702 self.flush_batch(false, source_range).await?;
704
705 Ok(DefaultPlanes {
706 xy: planes[&PlaneName::Xy],
707 neg_xy: planes[&PlaneName::NegXy],
708 xz: planes[&PlaneName::Xz],
709 neg_xz: planes[&PlaneName::NegXz],
710 yz: planes[&PlaneName::Yz],
711 neg_yz: planes[&PlaneName::NegYz],
712 })
713 }
714
715 fn parse_websocket_response(
716 &self,
717 response: WebSocketResponse,
718 source_range: SourceRange,
719 ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
720 match response {
721 WebSocketResponse::Success(success) => Ok(success.resp),
722 WebSocketResponse::Failure(fail) => {
723 let _request_id = fail.request_id;
724 if fail.errors.is_empty() {
725 return Err(KclError::new_engine(KclErrorDetails::new(
726 "Failure response with no error details".to_owned(),
727 vec![source_range],
728 )));
729 }
730 Err(KclError::new_engine(KclErrorDetails::new(
731 fail.errors
732 .iter()
733 .map(|e| e.message.clone())
734 .collect::<Vec<_>>()
735 .join("\n"),
736 vec![source_range],
737 )))
738 }
739 }
740 }
741
742 fn parse_batch_responses(
743 &self,
744 id: uuid::Uuid,
746 id_to_source_range: HashMap<uuid::Uuid, SourceRange>,
748 responses: HashMap<uuid::Uuid, BatchResponse>,
750 ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
751 #[expect(
753 clippy::iter_over_hash_type,
754 reason = "modeling command uses a HashMap and keys are random, so we don't really have a choice"
755 )]
756 for (cmd_id, resp) in responses.iter() {
757 match resp {
758 BatchResponse::Success { response } => {
759 if cmd_id == &id {
760 return Ok(OkWebSocketResponseData::Modeling {
762 modeling_response: response.clone(),
763 });
764 } else {
765 continue;
767 }
768 }
769 BatchResponse::Failure { errors } => {
770 let source_range = id_to_source_range.get(cmd_id).cloned().ok_or_else(|| {
772 KclError::new_engine(KclErrorDetails::new(
773 format!("Failed to get source range for command ID: {cmd_id:?}"),
774 vec![],
775 ))
776 })?;
777 if errors.is_empty() {
778 return Err(KclError::new_engine(KclErrorDetails::new(
779 "Failure response for batch with no error details".to_owned(),
780 vec![source_range],
781 )));
782 }
783 return Err(KclError::new_engine(KclErrorDetails::new(
784 errors.iter().map(|e| e.message.clone()).collect::<Vec<_>>().join("\n"),
785 vec![source_range],
786 )));
787 }
788 }
789 }
790
791 Err(KclError::new_engine(KclErrorDetails::new(
794 format!("Failed to find response for command ID: {id:?}"),
795 vec![],
796 )))
797 }
798
799 async fn modify_grid(
800 &self,
801 hidden: bool,
802 grid_scale_behavior: GridScaleBehavior,
803 source_range: SourceRange,
804 id_generator: &mut IdGenerator,
805 ) -> Result<(), KclError> {
806 self.batch_modeling_cmd(
808 id_generator.next_uuid(),
809 source_range,
810 &ModelingCmd::from(mcmd::ObjectVisible {
811 hidden,
812 object_id: *GRID_OBJECT_ID,
813 }),
814 )
815 .await?;
816
817 self.batch_modeling_cmd(
818 id_generator.next_uuid(),
819 source_range,
820 &grid_scale_behavior.into_modeling_cmd(),
821 )
822 .await?;
823
824 self.batch_modeling_cmd(
826 id_generator.next_uuid(),
827 source_range,
828 &ModelingCmd::from(mcmd::ObjectVisible {
829 hidden,
830 object_id: *GRID_SCALE_TEXT_OBJECT_ID,
831 }),
832 )
833 .await?;
834
835 Ok(())
836 }
837
838 async fn get_session_data(&self) -> Option<ModelingSessionData> {
841 None
842 }
843
844 async fn close(&self);
846}
847
848#[derive(Debug, Hash, Eq, Copy, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, Display, FromStr)]
849#[ts(export)]
850#[serde(rename_all = "camelCase")]
851pub enum PlaneName {
852 #[display("XY")]
854 Xy,
855 #[display("-XY")]
857 NegXy,
858 #[display("XZ")]
860 Xz,
861 #[display("-XZ")]
863 NegXz,
864 #[display("YZ")]
866 Yz,
867 #[display("-YZ")]
869 NegYz,
870}
871
872#[cfg(not(target_arch = "wasm32"))]
874pub fn new_zoo_client(token: Option<String>, engine_addr: Option<String>) -> anyhow::Result<kittycad::Client> {
875 let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"),);
876 let http_client = reqwest::Client::builder()
877 .user_agent(user_agent)
878 .timeout(std::time::Duration::from_secs(600))
880 .connect_timeout(std::time::Duration::from_secs(60));
881 let ws_client = reqwest::Client::builder()
882 .user_agent(user_agent)
883 .timeout(std::time::Duration::from_secs(600))
885 .connect_timeout(std::time::Duration::from_secs(60))
886 .connection_verbose(true)
887 .tcp_keepalive(std::time::Duration::from_secs(600))
888 .http1_only();
889
890 let zoo_token_env = std::env::var("ZOO_API_TOKEN");
891
892 let token = if let Some(token) = token {
893 token
894 } else if let Ok(token) = std::env::var("KITTYCAD_API_TOKEN") {
895 if let Ok(zoo_token) = zoo_token_env
896 && zoo_token != token
897 {
898 return Err(anyhow::anyhow!(
899 "Both environment variables KITTYCAD_API_TOKEN=`{}` and ZOO_API_TOKEN=`{}` are set. Use only one.",
900 token,
901 zoo_token
902 ));
903 }
904 token
905 } else if let Ok(token) = zoo_token_env {
906 token
907 } else {
908 return Err(anyhow::anyhow!(
909 "No API token found in environment variables. Use ZOO_API_TOKEN"
910 ));
911 };
912
913 let mut client = kittycad::Client::new_from_reqwest(token, http_client, ws_client);
915 let kittycad_host_env = std::env::var("KITTYCAD_HOST");
917 if let Some(addr) = engine_addr {
918 client.set_base_url(addr);
919 } else if let Ok(addr) = std::env::var("ZOO_HOST") {
920 if let Ok(kittycad_host) = kittycad_host_env
921 && kittycad_host != addr
922 {
923 return Err(anyhow::anyhow!(
924 "Both environment variables KITTYCAD_HOST=`{}` and ZOO_HOST=`{}` are set. Use only one.",
925 kittycad_host,
926 addr
927 ));
928 }
929 client.set_base_url(addr);
930 } else if let Ok(addr) = kittycad_host_env {
931 client.set_base_url(addr);
932 }
933
934 Ok(client)
935}
936
937#[derive(Copy, Clone, Debug)]
938pub enum GridScaleBehavior {
939 ScaleWithZoom,
940 Fixed(Option<kcmc::units::UnitLength>),
941}
942
943impl GridScaleBehavior {
944 fn into_modeling_cmd(self) -> ModelingCmd {
945 const NUMBER_OF_GRID_COLUMNS: f32 = 10.0;
946 match self {
947 GridScaleBehavior::ScaleWithZoom => ModelingCmd::from(mcmd::SetGridAutoScale {}),
948 GridScaleBehavior::Fixed(unit_length) => ModelingCmd::from(mcmd::SetGridScale {
949 value: NUMBER_OF_GRID_COLUMNS,
950 units: unit_length.unwrap_or(kcmc::units::UnitLength::Millimeters),
951 }),
952 }
953 }
954}