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 schemars::JsonSchema;
35use serde::{Deserialize, Serialize};
36use tokio::sync::RwLock;
37use uuid::Uuid;
38use web_time::Instant;
39
40use crate::{
41 SourceRange,
42 errors::{KclError, KclErrorDetails},
43 execution::{DefaultPlanes, IdGenerator, PlaneInfo, Point3d, types::UnitLen},
44};
45
46lazy_static::lazy_static! {
47 pub static ref GRID_OBJECT_ID: uuid::Uuid = uuid::Uuid::parse_str("cfa78409-653d-4c26-96f1-7c45fb784840").unwrap();
48
49 pub static ref GRID_SCALE_TEXT_OBJECT_ID: uuid::Uuid = uuid::Uuid::parse_str("10782f33-f588-4668-8bcd-040502d26590").unwrap();
50
51 pub static ref DEFAULT_PLANE_INFO: IndexMap<PlaneName, PlaneInfo> = IndexMap::from([
52 (
53 PlaneName::Xy,
54 PlaneInfo {
55 origin: Point3d::new(0.0, 0.0, 0.0, UnitLen::Mm),
56 x_axis: Point3d::new(1.0, 0.0, 0.0, UnitLen::Unknown),
57 y_axis: Point3d::new(0.0, 1.0, 0.0, UnitLen::Unknown),
58 z_axis: Point3d::new(0.0, 0.0, 1.0, UnitLen::Unknown),
59 },
60 ),
61 (
62 PlaneName::NegXy,
63 PlaneInfo {
64 origin: Point3d::new( 0.0, 0.0, 0.0, UnitLen::Mm),
65 x_axis: Point3d::new(-1.0, 0.0, 0.0, UnitLen::Unknown),
66 y_axis: Point3d::new( 0.0, 1.0, 0.0, UnitLen::Unknown),
67 z_axis: Point3d::new( 0.0, 0.0, -1.0, UnitLen::Unknown),
68 },
69 ),
70 (
71 PlaneName::Xz,
72 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 z_axis: Point3d::new(0.0, -1.0, 0.0, UnitLen::Unknown),
77 },
78 ),
79 (
80 PlaneName::NegXz,
81 PlaneInfo {
82 origin: Point3d::new( 0.0, 0.0, 0.0, UnitLen::Mm),
83 x_axis: Point3d::new(-1.0, 0.0, 0.0, UnitLen::Unknown),
84 y_axis: Point3d::new( 0.0, 0.0, 1.0, UnitLen::Unknown),
85 z_axis: Point3d::new( 0.0, 1.0, 0.0, UnitLen::Unknown),
86 },
87 ),
88 (
89 PlaneName::Yz,
90 PlaneInfo {
91 origin: Point3d::new(0.0, 0.0, 0.0, UnitLen::Mm),
92 x_axis: Point3d::new(0.0, 1.0, 0.0, UnitLen::Unknown),
93 y_axis: Point3d::new(0.0, 0.0, 1.0, UnitLen::Unknown),
94 z_axis: Point3d::new(1.0, 0.0, 0.0, UnitLen::Unknown),
95 },
96 ),
97 (
98 PlaneName::NegYz,
99 PlaneInfo {
100 origin: Point3d::new( 0.0, 0.0, 0.0, UnitLen::Mm),
101 x_axis: Point3d::new( 0.0, -1.0, 0.0, UnitLen::Unknown),
102 y_axis: Point3d::new( 0.0, 0.0, 1.0, UnitLen::Unknown),
103 z_axis: Point3d::new(-1.0, 0.0, 0.0, UnitLen::Unknown),
104 },
105 ),
106 ]);
107}
108
109#[derive(Default, Debug)]
110pub struct EngineStats {
111 pub commands_batched: AtomicUsize,
112 pub batches_sent: AtomicUsize,
113}
114
115impl Clone for EngineStats {
116 fn clone(&self) -> Self {
117 Self {
118 commands_batched: AtomicUsize::new(self.commands_batched.load(Ordering::Relaxed)),
119 batches_sent: AtomicUsize::new(self.batches_sent.load(Ordering::Relaxed)),
120 }
121 }
122}
123
124#[async_trait::async_trait]
125pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
126 fn batch(&self) -> Arc<RwLock<Vec<(WebSocketRequest, SourceRange)>>>;
128
129 fn batch_end(&self) -> Arc<RwLock<IndexMap<uuid::Uuid, (WebSocketRequest, SourceRange)>>>;
131
132 fn responses(&self) -> Arc<RwLock<IndexMap<Uuid, WebSocketResponse>>>;
134
135 fn ids_of_async_commands(&self) -> Arc<RwLock<IndexMap<Uuid, SourceRange>>>;
137
138 fn async_tasks(&self) -> AsyncTasks;
140
141 async fn take_batch(&self) -> Vec<(WebSocketRequest, SourceRange)> {
143 std::mem::take(&mut *self.batch().write().await)
144 }
145
146 async fn take_batch_end(&self) -> IndexMap<Uuid, (WebSocketRequest, SourceRange)> {
148 std::mem::take(&mut *self.batch_end().write().await)
149 }
150
151 async fn take_ids_of_async_commands(&self) -> IndexMap<Uuid, SourceRange> {
153 std::mem::take(&mut *self.ids_of_async_commands().write().await)
154 }
155
156 async fn take_responses(&self) -> IndexMap<Uuid, WebSocketResponse> {
158 std::mem::take(&mut *self.responses().write().await)
159 }
160
161 fn get_default_planes(&self) -> Arc<RwLock<Option<DefaultPlanes>>>;
163
164 fn stats(&self) -> &EngineStats;
165
166 async fn default_planes(
168 &self,
169 id_generator: &mut IdGenerator,
170 source_range: SourceRange,
171 ) -> Result<DefaultPlanes, KclError> {
172 {
173 let opt = self.get_default_planes().read().await.as_ref().cloned();
174 if let Some(planes) = opt {
175 return Ok(planes);
176 }
177 } let new_planes = self.new_default_planes(id_generator, source_range).await?;
180 *self.get_default_planes().write().await = Some(new_planes.clone());
181
182 Ok(new_planes)
183 }
184
185 async fn clear_scene_post_hook(
188 &self,
189 id_generator: &mut IdGenerator,
190 source_range: SourceRange,
191 ) -> Result<(), crate::errors::KclError>;
192
193 async fn clear_queues(&self) {
194 self.batch().write().await.clear();
195 self.batch_end().write().await.clear();
196 self.ids_of_async_commands().write().await.clear();
197 self.async_tasks().clear().await;
198 }
199
200 async fn fetch_debug(&self) -> Result<(), crate::errors::KclError>;
202
203 async fn get_debug(&self) -> Option<OkWebSocketResponseData>;
205
206 async fn inner_fire_modeling_cmd(
208 &self,
209 id: uuid::Uuid,
210 source_range: SourceRange,
211 cmd: WebSocketRequest,
212 id_to_source_range: HashMap<Uuid, SourceRange>,
213 ) -> Result<(), crate::errors::KclError>;
214
215 async fn inner_send_modeling_cmd(
217 &self,
218 id: uuid::Uuid,
219 source_range: SourceRange,
220 cmd: WebSocketRequest,
221 id_to_source_range: HashMap<uuid::Uuid, SourceRange>,
222 ) -> Result<kcmc::websocket::WebSocketResponse, crate::errors::KclError>;
223
224 async fn clear_scene(
225 &self,
226 id_generator: &mut IdGenerator,
227 source_range: SourceRange,
228 ) -> Result<(), crate::errors::KclError> {
229 self.clear_queues().await;
231
232 self.batch_modeling_cmd(
233 id_generator.next_uuid(),
234 source_range,
235 &ModelingCmd::SceneClearAll(mcmd::SceneClearAll::default()),
236 )
237 .await?;
238
239 self.flush_batch(false, source_range).await?;
242
243 self.clear_scene_post_hook(id_generator, source_range).await?;
245
246 Ok(())
247 }
248
249 async fn ensure_async_command_completed(
251 &self,
252 id: uuid::Uuid,
253 source_range: Option<SourceRange>,
254 ) -> Result<OkWebSocketResponseData, KclError> {
255 let source_range = if let Some(source_range) = source_range {
256 source_range
257 } else {
258 self.ids_of_async_commands()
260 .read()
261 .await
262 .get(&id)
263 .cloned()
264 .unwrap_or_default()
265 };
266
267 let current_time = Instant::now();
268 while current_time.elapsed().as_secs() < 60 {
269 let responses = self.responses().read().await.clone();
270 let Some(resp) = responses.get(&id) else {
271 #[cfg(target_arch = "wasm32")]
274 {
275 let duration = web_time::Duration::from_millis(1);
276 wasm_timer::Delay::new(duration).await.map_err(|err| {
277 KclError::new_internal(KclErrorDetails::new(
278 format!("Failed to sleep: {:?}", err),
279 vec![source_range],
280 ))
281 })?;
282 }
283 #[cfg(not(target_arch = "wasm32"))]
284 tokio::task::yield_now().await;
285 continue;
286 };
287
288 let response = self.parse_websocket_response(resp.clone(), source_range)?;
291 return Ok(response);
292 }
293
294 Err(KclError::new_engine(KclErrorDetails::new(
295 "async command timed out".to_string(),
296 vec![source_range],
297 )))
298 }
299
300 async fn ensure_async_commands_completed(&self) -> Result<(), KclError> {
302 let ids = self.take_ids_of_async_commands().await;
304
305 for (id, source_range) in ids {
307 self.ensure_async_command_completed(id, Some(source_range)).await?;
308 }
309
310 if let Err(err) = self.async_tasks().join_all().await {
316 crate::log::logln!(
317 "Error waiting for async tasks (this is typically fine and just means that an edge became something else): {:?}",
318 err
319 );
320 }
321
322 self.flush_batch(true, SourceRange::default()).await?;
324
325 Ok(())
326 }
327
328 async fn set_edge_visibility(
330 &self,
331 visible: bool,
332 source_range: SourceRange,
333 id_generator: &mut IdGenerator,
334 ) -> Result<(), crate::errors::KclError> {
335 self.batch_modeling_cmd(
336 id_generator.next_uuid(),
337 source_range,
338 &ModelingCmd::from(mcmd::EdgeLinesVisible { hidden: !visible }),
339 )
340 .await?;
341
342 Ok(())
343 }
344
345 async fn reapply_settings(
347 &self,
348 settings: &crate::ExecutorSettings,
349 source_range: SourceRange,
350 id_generator: &mut IdGenerator,
351 grid_scale_unit: GridScaleBehavior,
352 ) -> Result<(), crate::errors::KclError> {
353 self.set_edge_visibility(settings.highlight_edges, source_range, id_generator)
355 .await?;
356
357 self.modify_grid(!settings.show_grid, grid_scale_unit, source_range, id_generator)
360 .await?;
361
362 self.flush_batch(false, source_range).await?;
366
367 Ok(())
368 }
369
370 async fn batch_modeling_cmd(
372 &self,
373 id: uuid::Uuid,
374 source_range: SourceRange,
375 cmd: &ModelingCmd,
376 ) -> Result<(), crate::errors::KclError> {
377 let req = WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
378 cmd: cmd.clone(),
379 cmd_id: id.into(),
380 });
381
382 self.batch().write().await.push((req, source_range));
384 self.stats().commands_batched.fetch_add(1, Ordering::Relaxed);
385
386 Ok(())
387 }
388
389 async fn batch_modeling_cmds(
394 &self,
395 source_range: SourceRange,
396 cmds: &[ModelingCmdReq],
397 ) -> Result<(), crate::errors::KclError> {
398 let mut extended_cmds = Vec::with_capacity(cmds.len());
400 for cmd in cmds {
401 extended_cmds.push((WebSocketRequest::ModelingCmdReq(cmd.clone()), source_range));
402 }
403 self.stats()
404 .commands_batched
405 .fetch_add(extended_cmds.len(), Ordering::Relaxed);
406 self.batch().write().await.extend(extended_cmds);
407
408 Ok(())
409 }
410
411 async fn batch_end_cmd(
415 &self,
416 id: uuid::Uuid,
417 source_range: SourceRange,
418 cmd: &ModelingCmd,
419 ) -> Result<(), crate::errors::KclError> {
420 let req = WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
421 cmd: cmd.clone(),
422 cmd_id: id.into(),
423 });
424
425 self.batch_end().write().await.insert(id, (req, source_range));
427 self.stats().commands_batched.fetch_add(1, Ordering::Relaxed);
428 Ok(())
429 }
430
431 async fn send_modeling_cmd(
433 &self,
434 id: uuid::Uuid,
435 source_range: SourceRange,
436 cmd: &ModelingCmd,
437 ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
438 let mut requests = self.take_batch().await.clone();
439
440 requests.push((
442 WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
443 cmd: cmd.clone(),
444 cmd_id: id.into(),
445 }),
446 source_range,
447 ));
448 self.stats().commands_batched.fetch_add(1, Ordering::Relaxed);
449
450 self.run_batch(requests, source_range).await
452 }
453
454 async fn async_modeling_cmd(
457 &self,
458 id: uuid::Uuid,
459 source_range: SourceRange,
460 cmd: &ModelingCmd,
461 ) -> Result<(), crate::errors::KclError> {
462 self.ids_of_async_commands().write().await.insert(id, source_range);
464
465 self.inner_fire_modeling_cmd(
467 id,
468 source_range,
469 WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
470 cmd: cmd.clone(),
471 cmd_id: id.into(),
472 }),
473 HashMap::from([(id, source_range)]),
474 )
475 .await?;
476
477 Ok(())
478 }
479
480 async fn run_batch(
482 &self,
483 orig_requests: Vec<(WebSocketRequest, SourceRange)>,
484 source_range: SourceRange,
485 ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
486 if orig_requests.is_empty() {
488 return Ok(OkWebSocketResponseData::Modeling {
489 modeling_response: OkModelingCmdResponse::Empty {},
490 });
491 }
492
493 let requests: Vec<ModelingCmdReq> = orig_requests
494 .iter()
495 .filter_map(|(val, _)| match val {
496 WebSocketRequest::ModelingCmdReq(ModelingCmdReq { cmd, cmd_id }) => Some(ModelingCmdReq {
497 cmd: cmd.clone(),
498 cmd_id: *cmd_id,
499 }),
500 _ => None,
501 })
502 .collect();
503
504 let batched_requests = WebSocketRequest::ModelingCmdBatchReq(ModelingBatch {
505 requests,
506 batch_id: uuid::Uuid::new_v4().into(),
507 responses: true,
508 });
509
510 let final_req = if orig_requests.len() == 1 {
511 orig_requests.first().unwrap().0.clone()
513 } else {
514 batched_requests
515 };
516
517 let mut id_to_source_range = HashMap::new();
520 for (req, range) in orig_requests.iter() {
521 match req {
522 WebSocketRequest::ModelingCmdReq(ModelingCmdReq { cmd: _, cmd_id }) => {
523 id_to_source_range.insert(Uuid::from(*cmd_id), *range);
524 }
525 _ => {
526 return Err(KclError::new_engine(KclErrorDetails::new(
527 format!("The request is not a modeling command: {req:?}"),
528 vec![*range],
529 )));
530 }
531 }
532 }
533
534 self.stats().batches_sent.fetch_add(1, Ordering::Relaxed);
535
536 match final_req {
538 WebSocketRequest::ModelingCmdBatchReq(ModelingBatch {
539 ref requests,
540 batch_id,
541 responses: _,
542 }) => {
543 let last_id = requests.last().unwrap().cmd_id;
545 let ws_resp = self
546 .inner_send_modeling_cmd(batch_id.into(), source_range, final_req, id_to_source_range.clone())
547 .await?;
548 let response = self.parse_websocket_response(ws_resp, source_range)?;
549
550 if let OkWebSocketResponseData::ModelingBatch { responses } = response {
552 let responses = responses.into_iter().map(|(k, v)| (Uuid::from(k), v)).collect();
553 self.parse_batch_responses(last_id.into(), id_to_source_range, responses)
554 } else {
555 Err(KclError::new_engine(KclErrorDetails::new(
557 format!("Failed to get batch response: {response:?}"),
558 vec![source_range],
559 )))
560 }
561 }
562 WebSocketRequest::ModelingCmdReq(ModelingCmdReq { cmd: _, cmd_id }) => {
563 let source_range = id_to_source_range.get(cmd_id.as_ref()).cloned().ok_or_else(|| {
571 KclError::new_engine(KclErrorDetails::new(
572 format!("Failed to get source range for command ID: {cmd_id:?}"),
573 vec![],
574 ))
575 })?;
576 let ws_resp = self
577 .inner_send_modeling_cmd(cmd_id.into(), source_range, final_req, id_to_source_range)
578 .await?;
579 self.parse_websocket_response(ws_resp, source_range)
580 }
581 _ => Err(KclError::new_engine(KclErrorDetails::new(
582 format!("The final request is not a modeling command: {final_req:?}"),
583 vec![source_range],
584 ))),
585 }
586 }
587
588 async fn flush_batch(
590 &self,
591 batch_end: bool,
594 source_range: SourceRange,
595 ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
596 let all_requests = if batch_end {
597 let mut requests = self.take_batch().await.clone();
598 requests.extend(self.take_batch_end().await.values().cloned());
599 requests
600 } else {
601 self.take_batch().await.clone()
602 };
603
604 self.run_batch(all_requests, source_range).await
605 }
606
607 async fn make_default_plane(
608 &self,
609 plane_id: uuid::Uuid,
610 info: &PlaneInfo,
611 color: Option<Color>,
612 source_range: SourceRange,
613 id_generator: &mut IdGenerator,
614 ) -> Result<uuid::Uuid, KclError> {
615 let default_size = 100.0;
617
618 self.batch_modeling_cmd(
619 plane_id,
620 source_range,
621 &ModelingCmd::from(mcmd::MakePlane {
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: Some(true),
628 }),
629 )
630 .await?;
631
632 if let Some(color) = color {
633 self.batch_modeling_cmd(
635 id_generator.next_uuid(),
636 source_range,
637 &ModelingCmd::from(mcmd::PlaneSetColor { color, plane_id }),
638 )
639 .await?;
640 }
641
642 Ok(plane_id)
643 }
644
645 async fn new_default_planes(
646 &self,
647 id_generator: &mut IdGenerator,
648 source_range: SourceRange,
649 ) -> Result<DefaultPlanes, KclError> {
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: 0.4,
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: 0.4,
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: 0.4,
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, JsonSchema, 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 if zoo_token != token {
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 }
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 KITTYCAD_API_TOKEN or 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 if kittycad_host != addr {
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 }
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}