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 as kcmc;
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, types::UnitLen},
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, UnitLen::Mm),
55 x_axis: Point3d::new(1.0, 0.0, 0.0, UnitLen::Unknown),
56 y_axis: Point3d::new(0.0, 1.0, 0.0, UnitLen::Unknown),
57 z_axis: Point3d::new(0.0, 0.0, 1.0, UnitLen::Unknown),
58 },
59 ),
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 z_axis: Point3d::new( 0.0, 0.0, -1.0, UnitLen::Unknown),
67 },
68 ),
69 (
70 PlaneName::Xz,
71 PlaneInfo {
72 origin: Point3d::new(0.0, 0.0, 0.0, UnitLen::Mm),
73 x_axis: Point3d::new(1.0, 0.0, 0.0, UnitLen::Unknown),
74 y_axis: Point3d::new(0.0, 0.0, 1.0, UnitLen::Unknown),
75 z_axis: Point3d::new(0.0, -1.0, 0.0, UnitLen::Unknown),
76 },
77 ),
78 (
79 PlaneName::NegXz,
80 PlaneInfo {
81 origin: Point3d::new( 0.0, 0.0, 0.0, UnitLen::Mm),
82 x_axis: Point3d::new(-1.0, 0.0, 0.0, UnitLen::Unknown),
83 y_axis: Point3d::new( 0.0, 0.0, 1.0, UnitLen::Unknown),
84 z_axis: Point3d::new( 0.0, 1.0, 0.0, UnitLen::Unknown),
85 },
86 ),
87 (
88 PlaneName::Yz,
89 PlaneInfo {
90 origin: Point3d::new(0.0, 0.0, 0.0, UnitLen::Mm),
91 x_axis: Point3d::new(0.0, 1.0, 0.0, UnitLen::Unknown),
92 y_axis: Point3d::new(0.0, 0.0, 1.0, UnitLen::Unknown),
93 z_axis: Point3d::new(1.0, 0.0, 0.0, UnitLen::Unknown),
94 },
95 ),
96 (
97 PlaneName::NegYz,
98 PlaneInfo {
99 origin: Point3d::new( 0.0, 0.0, 0.0, UnitLen::Mm),
100 x_axis: Point3d::new( 0.0, -1.0, 0.0, UnitLen::Unknown),
101 y_axis: Point3d::new( 0.0, 0.0, 1.0, UnitLen::Unknown),
102 z_axis: Point3d::new(-1.0, 0.0, 0.0, UnitLen::Unknown),
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.clone()
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_settings: Vec<(PlaneName, Uuid, Option<Color>)> = vec![
650 (
651 PlaneName::Xy,
652 id_generator.next_uuid(),
653 Some(Color {
654 r: 0.7,
655 g: 0.28,
656 b: 0.28,
657 a: 0.4,
658 }),
659 ),
660 (
661 PlaneName::Yz,
662 id_generator.next_uuid(),
663 Some(Color {
664 r: 0.28,
665 g: 0.7,
666 b: 0.28,
667 a: 0.4,
668 }),
669 ),
670 (
671 PlaneName::Xz,
672 id_generator.next_uuid(),
673 Some(Color {
674 r: 0.28,
675 g: 0.28,
676 b: 0.7,
677 a: 0.4,
678 }),
679 ),
680 (PlaneName::NegXy, id_generator.next_uuid(), None),
681 (PlaneName::NegYz, id_generator.next_uuid(), None),
682 (PlaneName::NegXz, id_generator.next_uuid(), None),
683 ];
684
685 let mut planes = HashMap::new();
686 for (name, plane_id, color) in plane_settings {
687 let info = DEFAULT_PLANE_INFO.get(&name).ok_or_else(|| {
688 KclError::new_engine(KclErrorDetails::new(
690 format!("Failed to get default plane info for: {name:?}"),
691 vec![source_range],
692 ))
693 })?;
694 planes.insert(
695 name,
696 self.make_default_plane(plane_id, info, color, source_range, id_generator)
697 .await?,
698 );
699 }
700
701 self.flush_batch(false, source_range).await?;
703
704 Ok(DefaultPlanes {
705 xy: planes[&PlaneName::Xy],
706 neg_xy: planes[&PlaneName::NegXy],
707 xz: planes[&PlaneName::Xz],
708 neg_xz: planes[&PlaneName::NegXz],
709 yz: planes[&PlaneName::Yz],
710 neg_yz: planes[&PlaneName::NegYz],
711 })
712 }
713
714 fn parse_websocket_response(
715 &self,
716 response: WebSocketResponse,
717 source_range: SourceRange,
718 ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
719 match response {
720 WebSocketResponse::Success(success) => Ok(success.resp),
721 WebSocketResponse::Failure(fail) => {
722 let _request_id = fail.request_id;
723 if fail.errors.is_empty() {
724 return Err(KclError::new_engine(KclErrorDetails::new(
725 "Failure response with no error details".to_owned(),
726 vec![source_range],
727 )));
728 }
729 Err(KclError::new_engine(KclErrorDetails::new(
730 fail.errors
731 .iter()
732 .map(|e| e.message.clone())
733 .collect::<Vec<_>>()
734 .join("\n"),
735 vec![source_range],
736 )))
737 }
738 }
739 }
740
741 fn parse_batch_responses(
742 &self,
743 id: uuid::Uuid,
745 id_to_source_range: HashMap<uuid::Uuid, SourceRange>,
747 responses: HashMap<uuid::Uuid, BatchResponse>,
749 ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
750 #[expect(
752 clippy::iter_over_hash_type,
753 reason = "modeling command uses a HashMap and keys are random, so we don't really have a choice"
754 )]
755 for (cmd_id, resp) in responses.iter() {
756 match resp {
757 BatchResponse::Success { response } => {
758 if cmd_id == &id {
759 return Ok(OkWebSocketResponseData::Modeling {
761 modeling_response: response.clone(),
762 });
763 } else {
764 continue;
766 }
767 }
768 BatchResponse::Failure { errors } => {
769 let source_range = id_to_source_range.get(cmd_id).cloned().ok_or_else(|| {
771 KclError::new_engine(KclErrorDetails::new(
772 format!("Failed to get source range for command ID: {cmd_id:?}"),
773 vec![],
774 ))
775 })?;
776 if errors.is_empty() {
777 return Err(KclError::new_engine(KclErrorDetails::new(
778 "Failure response for batch with no error details".to_owned(),
779 vec![source_range],
780 )));
781 }
782 return Err(KclError::new_engine(KclErrorDetails::new(
783 errors.iter().map(|e| e.message.clone()).collect::<Vec<_>>().join("\n"),
784 vec![source_range],
785 )));
786 }
787 }
788 }
789
790 Err(KclError::new_engine(KclErrorDetails::new(
793 format!("Failed to find response for command ID: {id:?}"),
794 vec![],
795 )))
796 }
797
798 async fn modify_grid(
799 &self,
800 hidden: bool,
801 grid_scale_behavior: GridScaleBehavior,
802 source_range: SourceRange,
803 id_generator: &mut IdGenerator,
804 ) -> Result<(), KclError> {
805 self.batch_modeling_cmd(
807 id_generator.next_uuid(),
808 source_range,
809 &ModelingCmd::from(mcmd::ObjectVisible {
810 hidden,
811 object_id: *GRID_OBJECT_ID,
812 }),
813 )
814 .await?;
815
816 self.batch_modeling_cmd(
817 id_generator.next_uuid(),
818 source_range,
819 &grid_scale_behavior.into_modeling_cmd(),
820 )
821 .await?;
822
823 self.batch_modeling_cmd(
825 id_generator.next_uuid(),
826 source_range,
827 &ModelingCmd::from(mcmd::ObjectVisible {
828 hidden,
829 object_id: *GRID_SCALE_TEXT_OBJECT_ID,
830 }),
831 )
832 .await?;
833
834 Ok(())
835 }
836
837 async fn get_session_data(&self) -> Option<ModelingSessionData> {
840 None
841 }
842
843 async fn close(&self);
845}
846
847#[derive(Debug, Hash, Eq, Copy, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, Display, FromStr)]
848#[ts(export)]
849#[serde(rename_all = "camelCase")]
850pub enum PlaneName {
851 #[display("XY")]
853 Xy,
854 #[display("-XY")]
856 NegXy,
857 #[display("XZ")]
859 Xz,
860 #[display("-XZ")]
862 NegXz,
863 #[display("YZ")]
865 Yz,
866 #[display("-YZ")]
868 NegYz,
869}
870
871#[cfg(not(target_arch = "wasm32"))]
873pub fn new_zoo_client(token: Option<String>, engine_addr: Option<String>) -> anyhow::Result<kittycad::Client> {
874 let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"),);
875 let http_client = reqwest::Client::builder()
876 .user_agent(user_agent)
877 .timeout(std::time::Duration::from_secs(600))
879 .connect_timeout(std::time::Duration::from_secs(60));
880 let ws_client = reqwest::Client::builder()
881 .user_agent(user_agent)
882 .timeout(std::time::Duration::from_secs(600))
884 .connect_timeout(std::time::Duration::from_secs(60))
885 .connection_verbose(true)
886 .tcp_keepalive(std::time::Duration::from_secs(600))
887 .http1_only();
888
889 let zoo_token_env = std::env::var("ZOO_API_TOKEN");
890
891 let token = if let Some(token) = token {
892 token
893 } else if let Ok(token) = std::env::var("KITTYCAD_API_TOKEN") {
894 if let Ok(zoo_token) = zoo_token_env
895 && zoo_token != token
896 {
897 return Err(anyhow::anyhow!(
898 "Both environment variables KITTYCAD_API_TOKEN=`{}` and ZOO_API_TOKEN=`{}` are set. Use only one.",
899 token,
900 zoo_token
901 ));
902 }
903 token
904 } else if let Ok(token) = zoo_token_env {
905 token
906 } else {
907 return Err(anyhow::anyhow!(
908 "No API token found in environment variables. Use KITTYCAD_API_TOKEN or ZOO_API_TOKEN"
909 ));
910 };
911
912 let mut client = kittycad::Client::new_from_reqwest(token, http_client, ws_client);
914 let kittycad_host_env = std::env::var("KITTYCAD_HOST");
916 if let Some(addr) = engine_addr {
917 client.set_base_url(addr);
918 } else if let Ok(addr) = std::env::var("ZOO_HOST") {
919 if let Ok(kittycad_host) = kittycad_host_env
920 && kittycad_host != addr
921 {
922 return Err(anyhow::anyhow!(
923 "Both environment variables KITTYCAD_HOST=`{}` and ZOO_HOST=`{}` are set. Use only one.",
924 kittycad_host,
925 addr
926 ));
927 }
928 client.set_base_url(addr);
929 } else if let Ok(addr) = kittycad_host_env {
930 client.set_base_url(addr);
931 }
932
933 Ok(client)
934}
935
936#[derive(Copy, Clone, Debug)]
937pub enum GridScaleBehavior {
938 ScaleWithZoom,
939 Fixed(Option<kcmc::units::UnitLength>),
940}
941
942impl GridScaleBehavior {
943 fn into_modeling_cmd(self) -> ModelingCmd {
944 const NUMBER_OF_GRID_COLUMNS: f32 = 10.0;
945 match self {
946 GridScaleBehavior::ScaleWithZoom => ModelingCmd::from(mcmd::SetGridAutoScale {}),
947 GridScaleBehavior::Fixed(unit_length) => ModelingCmd::from(mcmd::SetGridScale {
948 value: NUMBER_OF_GRID_COLUMNS,
949 units: unit_length.unwrap_or(kcmc::units::UnitLength::Millimeters),
950 }),
951 }
952 }
953}