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