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(Debug, Clone)]
118pub struct EngineBatchContext {
119 batch: Arc<RwLock<Vec<(WebSocketRequest, SourceRange)>>>,
120 batch_end: Arc<RwLock<IndexMap<Uuid, (WebSocketRequest, SourceRange)>>>,
121}
122
123impl Default for EngineBatchContext {
124 fn default() -> Self {
125 Self::new()
126 }
127}
128
129impl EngineBatchContext {
130 pub fn new() -> Self {
131 Self {
132 batch: Arc::new(RwLock::new(Vec::new())),
133 batch_end: Arc::new(RwLock::new(IndexMap::new())),
134 }
135 }
136
137 pub async fn is_empty(&self) -> bool {
138 self.batch.read().await.is_empty() && self.batch_end.read().await.is_empty()
139 }
140
141 async fn clear(&self) {
142 self.batch.write().await.clear();
143 self.batch_end.write().await.clear();
144 }
145
146 async fn push(&self, req: WebSocketRequest, source_range: SourceRange) {
147 self.batch.write().await.push((req, source_range));
148 }
149
150 async fn extend(&self, requests: Vec<(WebSocketRequest, SourceRange)>) {
151 self.batch.write().await.extend(requests);
152 }
153
154 async fn insert_end(&self, id: Uuid, req: WebSocketRequest, source_range: SourceRange) {
155 self.batch_end.write().await.insert(id, (req, source_range));
156 }
157
158 pub(crate) async fn move_batch_end_to_batch(&self, ids: Vec<Uuid>) {
159 let mut moved = Vec::new();
160 {
161 let mut batch_end = self.batch_end.write().await;
162 for id in ids {
163 let Some(item) = batch_end.shift_remove(&id) else {
164 continue;
165 };
166 moved.push(item);
167 }
168 }
169
170 self.extend(moved).await;
171 }
172
173 async fn take_batch(&self) -> Vec<(WebSocketRequest, SourceRange)> {
174 std::mem::take(&mut *self.batch.write().await)
175 }
176
177 async fn take_batch_end(&self) -> IndexMap<Uuid, (WebSocketRequest, SourceRange)> {
178 std::mem::take(&mut *self.batch_end.write().await)
179 }
180}
181
182#[derive(Default, Debug)]
183pub struct EngineStats {
184 pub commands_batched: AtomicUsize,
185 pub batches_sent: AtomicUsize,
186}
187
188impl Clone for EngineStats {
189 fn clone(&self) -> Self {
190 Self {
191 commands_batched: AtomicUsize::new(self.commands_batched.load(Ordering::Relaxed)),
192 batches_sent: AtomicUsize::new(self.batches_sent.load(Ordering::Relaxed)),
193 }
194 }
195}
196
197#[async_trait::async_trait]
198pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
199 fn responses(&self) -> Arc<RwLock<IndexMap<Uuid, WebSocketResponse>>>;
201
202 fn ids_of_async_commands(&self) -> Arc<RwLock<IndexMap<Uuid, SourceRange>>>;
204
205 fn async_tasks(&self) -> AsyncTasks;
207
208 async fn take_ids_of_async_commands(&self) -> IndexMap<Uuid, SourceRange> {
210 std::mem::take(&mut *self.ids_of_async_commands().write().await)
211 }
212
213 async fn take_responses(&self) -> IndexMap<Uuid, WebSocketResponse> {
215 std::mem::take(&mut *self.responses().write().await)
216 }
217
218 fn get_default_planes(&self) -> Arc<RwLock<Option<DefaultPlanes>>>;
220
221 fn stats(&self) -> &EngineStats;
222
223 async fn default_planes(
225 &self,
226 batch_context: &EngineBatchContext,
227 id_generator: &mut IdGenerator,
228 source_range: SourceRange,
229 ) -> Result<DefaultPlanes, KclError> {
230 {
231 let opt = self.get_default_planes().read().await.as_ref().cloned();
232 if let Some(planes) = opt {
233 return Ok(planes);
234 }
235 } let new_planes = self
238 .new_default_planes(batch_context, id_generator, source_range)
239 .await?;
240 *self.get_default_planes().write().await = Some(new_planes.clone());
241
242 Ok(new_planes)
243 }
244
245 async fn clear_scene_post_hook(
248 &self,
249 batch_context: &EngineBatchContext,
250 id_generator: &mut IdGenerator,
251 source_range: SourceRange,
252 ) -> Result<(), crate::errors::KclError>;
253
254 async fn clear_queues(&self, batch_context: &EngineBatchContext) {
255 batch_context.clear().await;
256 self.ids_of_async_commands().write().await.clear();
257 self.async_tasks().clear().await;
258 }
259
260 async fn fetch_debug(&self) -> Result<(), crate::errors::KclError>;
262
263 async fn get_debug(&self) -> Option<OkWebSocketResponseData>;
265
266 async fn inner_fire_modeling_cmd(
268 &self,
269 id: uuid::Uuid,
270 source_range: SourceRange,
271 cmd: WebSocketRequest,
272 id_to_source_range: HashMap<Uuid, SourceRange>,
273 ) -> Result<(), crate::errors::KclError>;
274
275 async fn inner_send_modeling_cmd(
277 &self,
278 id: uuid::Uuid,
279 source_range: SourceRange,
280 cmd: WebSocketRequest,
281 id_to_source_range: HashMap<uuid::Uuid, SourceRange>,
282 ) -> Result<kcmc::websocket::WebSocketResponse, crate::errors::KclError>;
283
284 async fn clear_scene(
285 &self,
286 batch_context: &EngineBatchContext,
287 id_generator: &mut IdGenerator,
288 source_range: SourceRange,
289 ) -> Result<(), crate::errors::KclError> {
290 self.clear_queues(batch_context).await;
292
293 self.batch_modeling_cmd(
294 batch_context,
295 id_generator.next_uuid(),
296 source_range,
297 &ModelingCmd::SceneClearAll(mcmd::SceneClearAll::default()),
298 )
299 .await?;
300
301 self.flush_batch(batch_context, false, source_range).await?;
304
305 self.clear_scene_post_hook(batch_context, id_generator, source_range)
307 .await?;
308
309 Ok(())
310 }
311
312 async fn ensure_async_command_completed(
314 &self,
315 id: uuid::Uuid,
316 source_range: Option<SourceRange>,
317 ) -> Result<OkWebSocketResponseData, KclError> {
318 let source_range = if let Some(source_range) = source_range {
319 source_range
320 } else {
321 self.ids_of_async_commands()
323 .read()
324 .await
325 .get(&id)
326 .cloned()
327 .unwrap_or_default()
328 };
329
330 const ASYNC_CMD_TIMEOUT_SECS: u64 = 600;
338 let current_time = Instant::now();
339 while current_time.elapsed().as_secs() < ASYNC_CMD_TIMEOUT_SECS {
340 let responses = self.responses().read().await.clone();
341 let Some(resp) = responses.get(&id) else {
342 #[cfg(target_arch = "wasm32")]
345 {
346 let duration = web_time::Duration::from_millis(1);
347 wasm_timer::Delay::new(duration).await.map_err(|err| {
348 KclError::new_internal(KclErrorDetails::new(
349 format!("Failed to sleep: {:?}", err),
350 vec![source_range],
351 ))
352 })?;
353 }
354 #[cfg(not(target_arch = "wasm32"))]
355 tokio::task::yield_now().await;
356 continue;
357 };
358
359 let response = self.parse_websocket_response(resp.clone(), source_range)?;
362 return Ok(response);
363 }
364
365 Err(KclError::new_engine(KclErrorDetails::new(
366 format!(
367 "async command timed out after {ASYNC_CMD_TIMEOUT_SECS}s (client-side ceiling, not an engine error)"
368 ),
369 vec![source_range],
370 )))
371 }
372
373 async fn ensure_async_commands_completed(&self, batch_context: &EngineBatchContext) -> Result<(), KclError> {
375 let ids = self.take_ids_of_async_commands().await;
377
378 for (id, source_range) in ids {
380 self.ensure_async_command_completed(id, Some(source_range)).await?;
381 }
382
383 if let Err(err) = self.async_tasks().join_all().await {
389 crate::log::logln!(
390 "Error waiting for async tasks (this is typically fine and just means that an edge became something else): {:?}",
391 err
392 );
393 }
394
395 self.flush_batch(batch_context, true, SourceRange::default()).await?;
397
398 Ok(())
399 }
400
401 async fn set_edge_visibility(
403 &self,
404 batch_context: &EngineBatchContext,
405 visible: bool,
406 source_range: SourceRange,
407 id_generator: &mut IdGenerator,
408 ) -> Result<(), crate::errors::KclError> {
409 self.batch_modeling_cmd(
410 batch_context,
411 id_generator.next_uuid(),
412 source_range,
413 &ModelingCmd::from(mcmd::EdgeLinesVisible::builder().hidden(!visible).build()),
414 )
415 .await?;
416
417 Ok(())
418 }
419
420 async fn reapply_settings(
422 &self,
423 batch_context: &EngineBatchContext,
424 settings: &crate::ExecutorSettings,
425 source_range: SourceRange,
426 id_generator: &mut IdGenerator,
427 grid_scale_unit: GridScaleBehavior,
428 ) -> Result<(), crate::errors::KclError> {
429 self.set_edge_visibility(batch_context, settings.highlight_edges, source_range, id_generator)
431 .await?;
432
433 self.modify_grid(
436 batch_context,
437 !settings.show_grid,
438 grid_scale_unit,
439 source_range,
440 id_generator,
441 )
442 .await?;
443
444 self.flush_batch(batch_context, false, source_range).await?;
448
449 Ok(())
450 }
451
452 async fn batch_modeling_cmd(
454 &self,
455 batch_context: &EngineBatchContext,
456 id: uuid::Uuid,
457 source_range: SourceRange,
458 cmd: &ModelingCmd,
459 ) -> Result<(), crate::errors::KclError> {
460 let req = WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
461 cmd: cmd.clone(),
462 cmd_id: id.into(),
463 });
464
465 batch_context.push(req, source_range).await;
467 self.stats().commands_batched.fetch_add(1, Ordering::Relaxed);
468
469 Ok(())
470 }
471
472 async fn batch_modeling_cmds(
477 &self,
478 batch_context: &EngineBatchContext,
479 source_range: SourceRange,
480 cmds: &[ModelingCmdReq],
481 ) -> Result<(), crate::errors::KclError> {
482 let mut extended_cmds = Vec::with_capacity(cmds.len());
484 for cmd in cmds {
485 extended_cmds.push((WebSocketRequest::ModelingCmdReq(cmd.clone()), source_range));
486 }
487 self.stats()
488 .commands_batched
489 .fetch_add(extended_cmds.len(), Ordering::Relaxed);
490 batch_context.extend(extended_cmds).await;
491
492 Ok(())
493 }
494
495 async fn batch_end_cmd(
499 &self,
500 batch_context: &EngineBatchContext,
501 id: uuid::Uuid,
502 source_range: SourceRange,
503 cmd: &ModelingCmd,
504 ) -> Result<(), crate::errors::KclError> {
505 let req = WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
506 cmd: cmd.clone(),
507 cmd_id: id.into(),
508 });
509
510 batch_context.insert_end(id, req, source_range).await;
512 self.stats().commands_batched.fetch_add(1, Ordering::Relaxed);
513 Ok(())
514 }
515
516 async fn send_modeling_cmd(
518 &self,
519 batch_context: &EngineBatchContext,
520 id: uuid::Uuid,
521 source_range: SourceRange,
522 cmd: &ModelingCmd,
523 ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
524 let mut requests = batch_context.take_batch().await;
525
526 requests.push((
528 WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
529 cmd: cmd.clone(),
530 cmd_id: id.into(),
531 }),
532 source_range,
533 ));
534 self.stats().commands_batched.fetch_add(1, Ordering::Relaxed);
535
536 self.run_batch(requests, source_range).await
538 }
539
540 async fn async_modeling_cmd(
543 &self,
544 id: uuid::Uuid,
545 source_range: SourceRange,
546 cmd: &ModelingCmd,
547 ) -> Result<(), crate::errors::KclError> {
548 self.ids_of_async_commands().write().await.insert(id, source_range);
550
551 self.inner_fire_modeling_cmd(
553 id,
554 source_range,
555 WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
556 cmd: cmd.clone(),
557 cmd_id: id.into(),
558 }),
559 HashMap::from([(id, source_range)]),
560 )
561 .await?;
562
563 Ok(())
564 }
565
566 async fn run_batch(
568 &self,
569 orig_requests: Vec<(WebSocketRequest, SourceRange)>,
570 source_range: SourceRange,
571 ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
572 if orig_requests.is_empty() {
574 return Ok(OkWebSocketResponseData::Modeling {
575 modeling_response: OkModelingCmdResponse::Empty {},
576 });
577 }
578
579 let requests: Vec<ModelingCmdReq> = orig_requests
580 .iter()
581 .filter_map(|(val, _)| match val {
582 WebSocketRequest::ModelingCmdReq(ModelingCmdReq { cmd, cmd_id }) => Some(ModelingCmdReq {
583 cmd: cmd.clone(),
584 cmd_id: *cmd_id,
585 }),
586 _ => None,
587 })
588 .collect();
589
590 let batched_requests = WebSocketRequest::ModelingCmdBatchReq(ModelingBatch {
591 requests,
592 batch_id: uuid::Uuid::new_v4().into(),
593 responses: true,
594 });
595
596 let final_req = if orig_requests.len() == 1 {
597 orig_requests.first().unwrap().0.clone()
599 } else {
600 batched_requests
601 };
602
603 let mut id_to_source_range = HashMap::new();
606 for (req, range) in orig_requests.iter() {
607 match req {
608 WebSocketRequest::ModelingCmdReq(ModelingCmdReq { cmd: _, cmd_id }) => {
609 id_to_source_range.insert(Uuid::from(*cmd_id), *range);
610 }
611 _ => {
612 return Err(KclError::new_engine(KclErrorDetails::new(
613 format!("The request is not a modeling command: {req:?}"),
614 vec![*range],
615 )));
616 }
617 }
618 }
619
620 self.stats().batches_sent.fetch_add(1, Ordering::Relaxed);
621
622 match final_req {
624 WebSocketRequest::ModelingCmdBatchReq(ModelingBatch {
625 ref requests,
626 batch_id,
627 responses: _,
628 }) => {
629 let last_id = requests.last().unwrap().cmd_id;
631 let ws_resp = self
632 .inner_send_modeling_cmd(batch_id.into(), source_range, final_req, id_to_source_range.clone())
633 .await?;
634 let response = self.parse_websocket_response(ws_resp, source_range)?;
635
636 if let OkWebSocketResponseData::ModelingBatch { responses } = response {
638 let responses = responses.into_iter().map(|(k, v)| (Uuid::from(k), v)).collect();
639 self.parse_batch_responses(last_id.into(), id_to_source_range, responses)
640 } else {
641 Err(KclError::new_engine(KclErrorDetails::new(
643 format!("Failed to get batch response: {response:?}"),
644 vec![source_range],
645 )))
646 }
647 }
648 WebSocketRequest::ModelingCmdReq(ModelingCmdReq { cmd: _, cmd_id }) => {
649 let source_range = id_to_source_range.get(cmd_id.as_ref()).cloned().ok_or_else(|| {
657 KclError::new_engine(KclErrorDetails::new(
658 format!("Failed to get source range for command ID: {cmd_id:?}"),
659 vec![],
660 ))
661 })?;
662 let ws_resp = self
663 .inner_send_modeling_cmd(cmd_id.into(), source_range, final_req, id_to_source_range)
664 .await?;
665 self.parse_websocket_response(ws_resp, source_range)
666 }
667 _ => Err(KclError::new_engine(KclErrorDetails::new(
668 format!("The final request is not a modeling command: {final_req:?}"),
669 vec![source_range],
670 ))),
671 }
672 }
673
674 async fn flush_batch(
676 &self,
677 batch_context: &EngineBatchContext,
678 batch_end: bool,
681 source_range: SourceRange,
682 ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
683 let all_requests = if batch_end {
684 let mut requests = batch_context.take_batch().await;
685 requests.extend(batch_context.take_batch_end().await.values().cloned());
686 requests
687 } else {
688 batch_context.take_batch().await
689 };
690
691 self.run_batch(all_requests, source_range).await
692 }
693
694 async fn make_default_plane(
695 &self,
696 batch_context: &EngineBatchContext,
697 plane_id: uuid::Uuid,
698 info: &PlaneInfo,
699 color: Option<Color>,
700 source_range: SourceRange,
701 id_generator: &mut IdGenerator,
702 ) -> Result<uuid::Uuid, KclError> {
703 let default_size = 100.0;
705
706 self.batch_modeling_cmd(
707 batch_context,
708 plane_id,
709 source_range,
710 &ModelingCmd::from(
711 mcmd::MakePlane::builder()
712 .clobber(false)
713 .origin(info.origin.into())
714 .size(LengthUnit(default_size))
715 .x_axis(info.x_axis.into())
716 .y_axis(info.y_axis.into())
717 .hide(true)
718 .build(),
719 ),
720 )
721 .await?;
722
723 if let Some(color) = color {
724 self.batch_modeling_cmd(
726 batch_context,
727 id_generator.next_uuid(),
728 source_range,
729 &ModelingCmd::from(mcmd::PlaneSetColor::builder().color(color).plane_id(plane_id).build()),
730 )
731 .await?;
732 }
733
734 Ok(plane_id)
735 }
736
737 async fn new_default_planes(
738 &self,
739 batch_context: &EngineBatchContext,
740 id_generator: &mut IdGenerator,
741 source_range: SourceRange,
742 ) -> Result<DefaultPlanes, KclError> {
743 let plane_opacity = 0.1;
744 let plane_settings: Vec<(PlaneName, Uuid, Option<Color>)> = vec![
745 (
746 PlaneName::Xy,
747 id_generator.next_uuid(),
748 Some(Color::from_rgba(0.7, 0.28, 0.28, plane_opacity)),
749 ),
750 (
751 PlaneName::Yz,
752 id_generator.next_uuid(),
753 Some(Color::from_rgba(0.28, 0.7, 0.28, plane_opacity)),
754 ),
755 (
756 PlaneName::Xz,
757 id_generator.next_uuid(),
758 Some(Color::from_rgba(0.28, 0.28, 0.7, plane_opacity)),
759 ),
760 (PlaneName::NegXy, id_generator.next_uuid(), None),
761 (PlaneName::NegYz, id_generator.next_uuid(), None),
762 (PlaneName::NegXz, id_generator.next_uuid(), None),
763 ];
764
765 let mut planes = HashMap::new();
766 for (name, plane_id, color) in plane_settings {
767 let info = DEFAULT_PLANE_INFO.get(&name).ok_or_else(|| {
768 KclError::new_engine(KclErrorDetails::new(
770 format!("Failed to get default plane info for: {name:?}"),
771 vec![source_range],
772 ))
773 })?;
774 planes.insert(
775 name,
776 self.make_default_plane(batch_context, plane_id, info, color, source_range, id_generator)
777 .await?,
778 );
779 }
780
781 self.flush_batch(batch_context, false, source_range).await?;
783
784 Ok(DefaultPlanes {
785 xy: planes[&PlaneName::Xy],
786 neg_xy: planes[&PlaneName::NegXy],
787 xz: planes[&PlaneName::Xz],
788 neg_xz: planes[&PlaneName::NegXz],
789 yz: planes[&PlaneName::Yz],
790 neg_yz: planes[&PlaneName::NegYz],
791 })
792 }
793
794 fn parse_websocket_response(
795 &self,
796 response: WebSocketResponse,
797 source_range: SourceRange,
798 ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
799 match response {
800 WebSocketResponse::Success(success) => Ok(success.resp),
801 WebSocketResponse::Failure(fail) => {
802 let _request_id = fail.request_id;
803 if fail.errors.is_empty() {
804 return Err(KclError::new_engine(KclErrorDetails::new(
805 "Failure response with no error details".to_owned(),
806 vec![source_range],
807 )));
808 }
809 Err(KclError::new_engine(KclErrorDetails::new(
810 fail.errors
811 .iter()
812 .map(|e| e.message.clone())
813 .collect::<Vec<_>>()
814 .join("\n"),
815 vec![source_range],
816 )))
817 }
818 }
819 }
820
821 fn parse_batch_responses(
822 &self,
823 id: uuid::Uuid,
825 id_to_source_range: HashMap<uuid::Uuid, SourceRange>,
827 responses: HashMap<uuid::Uuid, BatchResponse>,
829 ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
830 #[expect(
832 clippy::iter_over_hash_type,
833 reason = "modeling command uses a HashMap and keys are random, so we don't really have a choice"
834 )]
835 for (cmd_id, resp) in responses.iter() {
836 match resp {
837 BatchResponse::Success { response } => {
838 if cmd_id == &id {
839 return Ok(OkWebSocketResponseData::Modeling {
841 modeling_response: response.clone(),
842 });
843 } else {
844 continue;
846 }
847 }
848 BatchResponse::Failure { errors } => {
849 let source_range = id_to_source_range.get(cmd_id).cloned().ok_or_else(|| {
851 KclError::new_engine(KclErrorDetails::new(
852 format!("Failed to get source range for command ID: {cmd_id:?}"),
853 vec![],
854 ))
855 })?;
856 if errors.is_empty() {
857 return Err(KclError::new_engine(KclErrorDetails::new(
858 "Failure response for batch with no error details".to_owned(),
859 vec![source_range],
860 )));
861 }
862 return Err(KclError::new_engine(KclErrorDetails::new(
863 errors.iter().map(|e| e.message.clone()).collect::<Vec<_>>().join("\n"),
864 vec![source_range],
865 )));
866 }
867 }
868 }
869
870 Err(KclError::new_engine(KclErrorDetails::new(
873 format!("Failed to find response for command ID: {id:?}"),
874 vec![],
875 )))
876 }
877
878 async fn modify_grid(
879 &self,
880 batch_context: &EngineBatchContext,
881 hidden: bool,
882 grid_scale_behavior: GridScaleBehavior,
883 source_range: SourceRange,
884 id_generator: &mut IdGenerator,
885 ) -> Result<(), KclError> {
886 self.batch_modeling_cmd(
888 batch_context,
889 id_generator.next_uuid(),
890 source_range,
891 &ModelingCmd::from(
892 mcmd::ObjectVisible::builder()
893 .hidden(hidden)
894 .object_id(*GRID_OBJECT_ID)
895 .build(),
896 ),
897 )
898 .await?;
899
900 self.batch_modeling_cmd(
901 batch_context,
902 id_generator.next_uuid(),
903 source_range,
904 &grid_scale_behavior.into_modeling_cmd(),
905 )
906 .await?;
907
908 self.batch_modeling_cmd(
910 batch_context,
911 id_generator.next_uuid(),
912 source_range,
913 &ModelingCmd::from(
914 mcmd::ObjectVisible::builder()
915 .hidden(hidden)
916 .object_id(*GRID_SCALE_TEXT_OBJECT_ID)
917 .build(),
918 ),
919 )
920 .await?;
921
922 Ok(())
923 }
924
925 async fn get_session_data(&self) -> Option<ModelingSessionData> {
928 None
929 }
930
931 async fn close(&self);
933}
934
935#[derive(Debug, Hash, Eq, Copy, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, Display, FromStr)]
936#[ts(export)]
937#[serde(rename_all = "camelCase")]
938pub enum PlaneName {
939 #[display("XY")]
941 Xy,
942 #[display("-XY")]
944 NegXy,
945 #[display("XZ")]
947 Xz,
948 #[display("-XZ")]
950 NegXz,
951 #[display("YZ")]
953 Yz,
954 #[display("-YZ")]
956 NegYz,
957}
958
959#[cfg(not(target_arch = "wasm32"))]
961pub fn new_zoo_client(token: Option<String>, engine_addr: Option<String>) -> anyhow::Result<kittycad::Client> {
962 let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"),);
963 let http_client = reqwest::Client::builder()
964 .user_agent(user_agent)
965 .timeout(std::time::Duration::from_secs(600))
967 .connect_timeout(std::time::Duration::from_secs(60));
968 let ws_client = reqwest::Client::builder()
969 .user_agent(user_agent)
970 .timeout(std::time::Duration::from_secs(600))
972 .connect_timeout(std::time::Duration::from_secs(60))
973 .connection_verbose(true)
974 .tcp_keepalive(std::time::Duration::from_secs(600))
975 .http1_only();
976
977 let zoo_token_env = std::env::var("ZOO_API_TOKEN");
978
979 let token = if let Some(token) = token {
980 token
981 } else if let Ok(token) = std::env::var("KITTYCAD_API_TOKEN") {
982 if let Ok(zoo_token) = zoo_token_env
983 && zoo_token != token
984 {
985 return Err(anyhow::anyhow!(
986 "Both environment variables KITTYCAD_API_TOKEN=`{}` and ZOO_API_TOKEN=`{}` are set. Use only one.",
987 token,
988 zoo_token
989 ));
990 }
991 token
992 } else if let Ok(token) = zoo_token_env {
993 token
994 } else {
995 return Err(anyhow::anyhow!(
996 "No API token found in environment variables. Use ZOO_API_TOKEN"
997 ));
998 };
999
1000 let mut client = kittycad::Client::new_from_reqwest(token, http_client, ws_client);
1002 let kittycad_host_env = std::env::var("KITTYCAD_HOST");
1004 if let Some(addr) = engine_addr {
1005 client.set_base_url(addr);
1006 } else if let Ok(addr) = std::env::var("ZOO_HOST") {
1007 if let Ok(kittycad_host) = kittycad_host_env
1008 && kittycad_host != addr
1009 {
1010 return Err(anyhow::anyhow!(
1011 "Both environment variables KITTYCAD_HOST=`{}` and ZOO_HOST=`{}` are set. Use only one.",
1012 kittycad_host,
1013 addr
1014 ));
1015 }
1016 client.set_base_url(addr);
1017 } else if let Ok(addr) = kittycad_host_env {
1018 client.set_base_url(addr);
1019 }
1020
1021 Ok(client)
1022}
1023
1024#[derive(Copy, Clone, Debug)]
1025pub enum GridScaleBehavior {
1026 ScaleWithZoom,
1027 Fixed(Option<kcmc::units::UnitLength>),
1028}
1029
1030impl GridScaleBehavior {
1031 fn into_modeling_cmd(self) -> ModelingCmd {
1032 const NUMBER_OF_GRID_COLUMNS: f32 = 10.0;
1033 match self {
1034 GridScaleBehavior::ScaleWithZoom => ModelingCmd::from(mcmd::SetGridAutoScale::builder().build()),
1035 GridScaleBehavior::Fixed(unit_length) => ModelingCmd::from(
1036 mcmd::SetGridScale::builder()
1037 .value(NUMBER_OF_GRID_COLUMNS)
1038 .units(unit_length.unwrap_or(kcmc::units::UnitLength::Millimeters))
1039 .build(),
1040 ),
1041 }
1042 }
1043}