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 let current_time = Instant::now();
331 while current_time.elapsed().as_secs() < 60 {
332 let responses = self.responses().read().await.clone();
333 let Some(resp) = responses.get(&id) else {
334 #[cfg(target_arch = "wasm32")]
337 {
338 let duration = web_time::Duration::from_millis(1);
339 wasm_timer::Delay::new(duration).await.map_err(|err| {
340 KclError::new_internal(KclErrorDetails::new(
341 format!("Failed to sleep: {:?}", err),
342 vec![source_range],
343 ))
344 })?;
345 }
346 #[cfg(not(target_arch = "wasm32"))]
347 tokio::task::yield_now().await;
348 continue;
349 };
350
351 let response = self.parse_websocket_response(resp.clone(), source_range)?;
354 return Ok(response);
355 }
356
357 Err(KclError::new_engine(KclErrorDetails::new(
358 "async command timed out".to_string(),
359 vec![source_range],
360 )))
361 }
362
363 async fn ensure_async_commands_completed(&self, batch_context: &EngineBatchContext) -> Result<(), KclError> {
365 let ids = self.take_ids_of_async_commands().await;
367
368 for (id, source_range) in ids {
370 self.ensure_async_command_completed(id, Some(source_range)).await?;
371 }
372
373 if let Err(err) = self.async_tasks().join_all().await {
379 crate::log::logln!(
380 "Error waiting for async tasks (this is typically fine and just means that an edge became something else): {:?}",
381 err
382 );
383 }
384
385 self.flush_batch(batch_context, true, SourceRange::default()).await?;
387
388 Ok(())
389 }
390
391 async fn set_edge_visibility(
393 &self,
394 batch_context: &EngineBatchContext,
395 visible: bool,
396 source_range: SourceRange,
397 id_generator: &mut IdGenerator,
398 ) -> Result<(), crate::errors::KclError> {
399 self.batch_modeling_cmd(
400 batch_context,
401 id_generator.next_uuid(),
402 source_range,
403 &ModelingCmd::from(mcmd::EdgeLinesVisible::builder().hidden(!visible).build()),
404 )
405 .await?;
406
407 Ok(())
408 }
409
410 async fn reapply_settings(
412 &self,
413 batch_context: &EngineBatchContext,
414 settings: &crate::ExecutorSettings,
415 source_range: SourceRange,
416 id_generator: &mut IdGenerator,
417 grid_scale_unit: GridScaleBehavior,
418 ) -> Result<(), crate::errors::KclError> {
419 self.set_edge_visibility(batch_context, settings.highlight_edges, source_range, id_generator)
421 .await?;
422
423 self.modify_grid(
426 batch_context,
427 !settings.show_grid,
428 grid_scale_unit,
429 source_range,
430 id_generator,
431 )
432 .await?;
433
434 self.flush_batch(batch_context, false, source_range).await?;
438
439 Ok(())
440 }
441
442 async fn batch_modeling_cmd(
444 &self,
445 batch_context: &EngineBatchContext,
446 id: uuid::Uuid,
447 source_range: SourceRange,
448 cmd: &ModelingCmd,
449 ) -> Result<(), crate::errors::KclError> {
450 let req = WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
451 cmd: cmd.clone(),
452 cmd_id: id.into(),
453 });
454
455 batch_context.push(req, source_range).await;
457 self.stats().commands_batched.fetch_add(1, Ordering::Relaxed);
458
459 Ok(())
460 }
461
462 async fn batch_modeling_cmds(
467 &self,
468 batch_context: &EngineBatchContext,
469 source_range: SourceRange,
470 cmds: &[ModelingCmdReq],
471 ) -> Result<(), crate::errors::KclError> {
472 let mut extended_cmds = Vec::with_capacity(cmds.len());
474 for cmd in cmds {
475 extended_cmds.push((WebSocketRequest::ModelingCmdReq(cmd.clone()), source_range));
476 }
477 self.stats()
478 .commands_batched
479 .fetch_add(extended_cmds.len(), Ordering::Relaxed);
480 batch_context.extend(extended_cmds).await;
481
482 Ok(())
483 }
484
485 async fn batch_end_cmd(
489 &self,
490 batch_context: &EngineBatchContext,
491 id: uuid::Uuid,
492 source_range: SourceRange,
493 cmd: &ModelingCmd,
494 ) -> Result<(), crate::errors::KclError> {
495 let req = WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
496 cmd: cmd.clone(),
497 cmd_id: id.into(),
498 });
499
500 batch_context.insert_end(id, req, source_range).await;
502 self.stats().commands_batched.fetch_add(1, Ordering::Relaxed);
503 Ok(())
504 }
505
506 async fn send_modeling_cmd(
508 &self,
509 batch_context: &EngineBatchContext,
510 id: uuid::Uuid,
511 source_range: SourceRange,
512 cmd: &ModelingCmd,
513 ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
514 let mut requests = batch_context.take_batch().await;
515
516 requests.push((
518 WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
519 cmd: cmd.clone(),
520 cmd_id: id.into(),
521 }),
522 source_range,
523 ));
524 self.stats().commands_batched.fetch_add(1, Ordering::Relaxed);
525
526 self.run_batch(requests, source_range).await
528 }
529
530 async fn async_modeling_cmd(
533 &self,
534 id: uuid::Uuid,
535 source_range: SourceRange,
536 cmd: &ModelingCmd,
537 ) -> Result<(), crate::errors::KclError> {
538 self.ids_of_async_commands().write().await.insert(id, source_range);
540
541 self.inner_fire_modeling_cmd(
543 id,
544 source_range,
545 WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
546 cmd: cmd.clone(),
547 cmd_id: id.into(),
548 }),
549 HashMap::from([(id, source_range)]),
550 )
551 .await?;
552
553 Ok(())
554 }
555
556 async fn run_batch(
558 &self,
559 orig_requests: Vec<(WebSocketRequest, SourceRange)>,
560 source_range: SourceRange,
561 ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
562 if orig_requests.is_empty() {
564 return Ok(OkWebSocketResponseData::Modeling {
565 modeling_response: OkModelingCmdResponse::Empty {},
566 });
567 }
568
569 let requests: Vec<ModelingCmdReq> = orig_requests
570 .iter()
571 .filter_map(|(val, _)| match val {
572 WebSocketRequest::ModelingCmdReq(ModelingCmdReq { cmd, cmd_id }) => Some(ModelingCmdReq {
573 cmd: cmd.clone(),
574 cmd_id: *cmd_id,
575 }),
576 _ => None,
577 })
578 .collect();
579
580 let batched_requests = WebSocketRequest::ModelingCmdBatchReq(ModelingBatch {
581 requests,
582 batch_id: uuid::Uuid::new_v4().into(),
583 responses: true,
584 });
585
586 let final_req = if orig_requests.len() == 1 {
587 orig_requests.first().unwrap().0.clone()
589 } else {
590 batched_requests
591 };
592
593 let mut id_to_source_range = HashMap::new();
596 for (req, range) in orig_requests.iter() {
597 match req {
598 WebSocketRequest::ModelingCmdReq(ModelingCmdReq { cmd: _, cmd_id }) => {
599 id_to_source_range.insert(Uuid::from(*cmd_id), *range);
600 }
601 _ => {
602 return Err(KclError::new_engine(KclErrorDetails::new(
603 format!("The request is not a modeling command: {req:?}"),
604 vec![*range],
605 )));
606 }
607 }
608 }
609
610 self.stats().batches_sent.fetch_add(1, Ordering::Relaxed);
611
612 match final_req {
614 WebSocketRequest::ModelingCmdBatchReq(ModelingBatch {
615 ref requests,
616 batch_id,
617 responses: _,
618 }) => {
619 let last_id = requests.last().unwrap().cmd_id;
621 let ws_resp = self
622 .inner_send_modeling_cmd(batch_id.into(), source_range, final_req, id_to_source_range.clone())
623 .await?;
624 let response = self.parse_websocket_response(ws_resp, source_range)?;
625
626 if let OkWebSocketResponseData::ModelingBatch { responses } = response {
628 let responses = responses.into_iter().map(|(k, v)| (Uuid::from(k), v)).collect();
629 self.parse_batch_responses(last_id.into(), id_to_source_range, responses)
630 } else {
631 Err(KclError::new_engine(KclErrorDetails::new(
633 format!("Failed to get batch response: {response:?}"),
634 vec![source_range],
635 )))
636 }
637 }
638 WebSocketRequest::ModelingCmdReq(ModelingCmdReq { cmd: _, cmd_id }) => {
639 let source_range = id_to_source_range.get(cmd_id.as_ref()).cloned().ok_or_else(|| {
647 KclError::new_engine(KclErrorDetails::new(
648 format!("Failed to get source range for command ID: {cmd_id:?}"),
649 vec![],
650 ))
651 })?;
652 let ws_resp = self
653 .inner_send_modeling_cmd(cmd_id.into(), source_range, final_req, id_to_source_range)
654 .await?;
655 self.parse_websocket_response(ws_resp, source_range)
656 }
657 _ => Err(KclError::new_engine(KclErrorDetails::new(
658 format!("The final request is not a modeling command: {final_req:?}"),
659 vec![source_range],
660 ))),
661 }
662 }
663
664 async fn flush_batch(
666 &self,
667 batch_context: &EngineBatchContext,
668 batch_end: bool,
671 source_range: SourceRange,
672 ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
673 let all_requests = if batch_end {
674 let mut requests = batch_context.take_batch().await;
675 requests.extend(batch_context.take_batch_end().await.values().cloned());
676 requests
677 } else {
678 batch_context.take_batch().await
679 };
680
681 self.run_batch(all_requests, source_range).await
682 }
683
684 async fn make_default_plane(
685 &self,
686 batch_context: &EngineBatchContext,
687 plane_id: uuid::Uuid,
688 info: &PlaneInfo,
689 color: Option<Color>,
690 source_range: SourceRange,
691 id_generator: &mut IdGenerator,
692 ) -> Result<uuid::Uuid, KclError> {
693 let default_size = 100.0;
695
696 self.batch_modeling_cmd(
697 batch_context,
698 plane_id,
699 source_range,
700 &ModelingCmd::from(
701 mcmd::MakePlane::builder()
702 .clobber(false)
703 .origin(info.origin.into())
704 .size(LengthUnit(default_size))
705 .x_axis(info.x_axis.into())
706 .y_axis(info.y_axis.into())
707 .hide(true)
708 .build(),
709 ),
710 )
711 .await?;
712
713 if let Some(color) = color {
714 self.batch_modeling_cmd(
716 batch_context,
717 id_generator.next_uuid(),
718 source_range,
719 &ModelingCmd::from(mcmd::PlaneSetColor::builder().color(color).plane_id(plane_id).build()),
720 )
721 .await?;
722 }
723
724 Ok(plane_id)
725 }
726
727 async fn new_default_planes(
728 &self,
729 batch_context: &EngineBatchContext,
730 id_generator: &mut IdGenerator,
731 source_range: SourceRange,
732 ) -> Result<DefaultPlanes, KclError> {
733 let plane_opacity = 0.1;
734 let plane_settings: Vec<(PlaneName, Uuid, Option<Color>)> = vec![
735 (
736 PlaneName::Xy,
737 id_generator.next_uuid(),
738 Some(Color::from_rgba(0.7, 0.28, 0.28, plane_opacity)),
739 ),
740 (
741 PlaneName::Yz,
742 id_generator.next_uuid(),
743 Some(Color::from_rgba(0.28, 0.7, 0.28, plane_opacity)),
744 ),
745 (
746 PlaneName::Xz,
747 id_generator.next_uuid(),
748 Some(Color::from_rgba(0.28, 0.28, 0.7, plane_opacity)),
749 ),
750 (PlaneName::NegXy, id_generator.next_uuid(), None),
751 (PlaneName::NegYz, id_generator.next_uuid(), None),
752 (PlaneName::NegXz, id_generator.next_uuid(), None),
753 ];
754
755 let mut planes = HashMap::new();
756 for (name, plane_id, color) in plane_settings {
757 let info = DEFAULT_PLANE_INFO.get(&name).ok_or_else(|| {
758 KclError::new_engine(KclErrorDetails::new(
760 format!("Failed to get default plane info for: {name:?}"),
761 vec![source_range],
762 ))
763 })?;
764 planes.insert(
765 name,
766 self.make_default_plane(batch_context, plane_id, info, color, source_range, id_generator)
767 .await?,
768 );
769 }
770
771 self.flush_batch(batch_context, false, source_range).await?;
773
774 Ok(DefaultPlanes {
775 xy: planes[&PlaneName::Xy],
776 neg_xy: planes[&PlaneName::NegXy],
777 xz: planes[&PlaneName::Xz],
778 neg_xz: planes[&PlaneName::NegXz],
779 yz: planes[&PlaneName::Yz],
780 neg_yz: planes[&PlaneName::NegYz],
781 })
782 }
783
784 fn parse_websocket_response(
785 &self,
786 response: WebSocketResponse,
787 source_range: SourceRange,
788 ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
789 match response {
790 WebSocketResponse::Success(success) => Ok(success.resp),
791 WebSocketResponse::Failure(fail) => {
792 let _request_id = fail.request_id;
793 if fail.errors.is_empty() {
794 return Err(KclError::new_engine(KclErrorDetails::new(
795 "Failure response with no error details".to_owned(),
796 vec![source_range],
797 )));
798 }
799 Err(KclError::new_engine(KclErrorDetails::new(
800 fail.errors
801 .iter()
802 .map(|e| e.message.clone())
803 .collect::<Vec<_>>()
804 .join("\n"),
805 vec![source_range],
806 )))
807 }
808 }
809 }
810
811 fn parse_batch_responses(
812 &self,
813 id: uuid::Uuid,
815 id_to_source_range: HashMap<uuid::Uuid, SourceRange>,
817 responses: HashMap<uuid::Uuid, BatchResponse>,
819 ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
820 #[expect(
822 clippy::iter_over_hash_type,
823 reason = "modeling command uses a HashMap and keys are random, so we don't really have a choice"
824 )]
825 for (cmd_id, resp) in responses.iter() {
826 match resp {
827 BatchResponse::Success { response } => {
828 if cmd_id == &id {
829 return Ok(OkWebSocketResponseData::Modeling {
831 modeling_response: response.clone(),
832 });
833 } else {
834 continue;
836 }
837 }
838 BatchResponse::Failure { errors } => {
839 let source_range = id_to_source_range.get(cmd_id).cloned().ok_or_else(|| {
841 KclError::new_engine(KclErrorDetails::new(
842 format!("Failed to get source range for command ID: {cmd_id:?}"),
843 vec![],
844 ))
845 })?;
846 if errors.is_empty() {
847 return Err(KclError::new_engine(KclErrorDetails::new(
848 "Failure response for batch with no error details".to_owned(),
849 vec![source_range],
850 )));
851 }
852 return Err(KclError::new_engine(KclErrorDetails::new(
853 errors.iter().map(|e| e.message.clone()).collect::<Vec<_>>().join("\n"),
854 vec![source_range],
855 )));
856 }
857 }
858 }
859
860 Err(KclError::new_engine(KclErrorDetails::new(
863 format!("Failed to find response for command ID: {id:?}"),
864 vec![],
865 )))
866 }
867
868 async fn modify_grid(
869 &self,
870 batch_context: &EngineBatchContext,
871 hidden: bool,
872 grid_scale_behavior: GridScaleBehavior,
873 source_range: SourceRange,
874 id_generator: &mut IdGenerator,
875 ) -> Result<(), KclError> {
876 self.batch_modeling_cmd(
878 batch_context,
879 id_generator.next_uuid(),
880 source_range,
881 &ModelingCmd::from(
882 mcmd::ObjectVisible::builder()
883 .hidden(hidden)
884 .object_id(*GRID_OBJECT_ID)
885 .build(),
886 ),
887 )
888 .await?;
889
890 self.batch_modeling_cmd(
891 batch_context,
892 id_generator.next_uuid(),
893 source_range,
894 &grid_scale_behavior.into_modeling_cmd(),
895 )
896 .await?;
897
898 self.batch_modeling_cmd(
900 batch_context,
901 id_generator.next_uuid(),
902 source_range,
903 &ModelingCmd::from(
904 mcmd::ObjectVisible::builder()
905 .hidden(hidden)
906 .object_id(*GRID_SCALE_TEXT_OBJECT_ID)
907 .build(),
908 ),
909 )
910 .await?;
911
912 Ok(())
913 }
914
915 async fn get_session_data(&self) -> Option<ModelingSessionData> {
918 None
919 }
920
921 async fn close(&self);
923}
924
925#[derive(Debug, Hash, Eq, Copy, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, Display, FromStr)]
926#[ts(export)]
927#[serde(rename_all = "camelCase")]
928pub enum PlaneName {
929 #[display("XY")]
931 Xy,
932 #[display("-XY")]
934 NegXy,
935 #[display("XZ")]
937 Xz,
938 #[display("-XZ")]
940 NegXz,
941 #[display("YZ")]
943 Yz,
944 #[display("-YZ")]
946 NegYz,
947}
948
949#[cfg(not(target_arch = "wasm32"))]
951pub fn new_zoo_client(token: Option<String>, engine_addr: Option<String>) -> anyhow::Result<kittycad::Client> {
952 let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"),);
953 let http_client = reqwest::Client::builder()
954 .user_agent(user_agent)
955 .timeout(std::time::Duration::from_secs(600))
957 .connect_timeout(std::time::Duration::from_secs(60));
958 let ws_client = reqwest::Client::builder()
959 .user_agent(user_agent)
960 .timeout(std::time::Duration::from_secs(600))
962 .connect_timeout(std::time::Duration::from_secs(60))
963 .connection_verbose(true)
964 .tcp_keepalive(std::time::Duration::from_secs(600))
965 .http1_only();
966
967 let zoo_token_env = std::env::var("ZOO_API_TOKEN");
968
969 let token = if let Some(token) = token {
970 token
971 } else if let Ok(token) = std::env::var("KITTYCAD_API_TOKEN") {
972 if let Ok(zoo_token) = zoo_token_env
973 && zoo_token != token
974 {
975 return Err(anyhow::anyhow!(
976 "Both environment variables KITTYCAD_API_TOKEN=`{}` and ZOO_API_TOKEN=`{}` are set. Use only one.",
977 token,
978 zoo_token
979 ));
980 }
981 token
982 } else if let Ok(token) = zoo_token_env {
983 token
984 } else {
985 return Err(anyhow::anyhow!(
986 "No API token found in environment variables. Use ZOO_API_TOKEN"
987 ));
988 };
989
990 let mut client = kittycad::Client::new_from_reqwest(token, http_client, ws_client);
992 let kittycad_host_env = std::env::var("KITTYCAD_HOST");
994 if let Some(addr) = engine_addr {
995 client.set_base_url(addr);
996 } else if let Ok(addr) = std::env::var("ZOO_HOST") {
997 if let Ok(kittycad_host) = kittycad_host_env
998 && kittycad_host != addr
999 {
1000 return Err(anyhow::anyhow!(
1001 "Both environment variables KITTYCAD_HOST=`{}` and ZOO_HOST=`{}` are set. Use only one.",
1002 kittycad_host,
1003 addr
1004 ));
1005 }
1006 client.set_base_url(addr);
1007 } else if let Ok(addr) = kittycad_host_env {
1008 client.set_base_url(addr);
1009 }
1010
1011 Ok(client)
1012}
1013
1014#[derive(Copy, Clone, Debug)]
1015pub enum GridScaleBehavior {
1016 ScaleWithZoom,
1017 Fixed(Option<kcmc::units::UnitLength>),
1018}
1019
1020impl GridScaleBehavior {
1021 fn into_modeling_cmd(self) -> ModelingCmd {
1022 const NUMBER_OF_GRID_COLUMNS: f32 = 10.0;
1023 match self {
1024 GridScaleBehavior::ScaleWithZoom => ModelingCmd::from(mcmd::SetGridAutoScale::builder().build()),
1025 GridScaleBehavior::Fixed(unit_length) => ModelingCmd::from(
1026 mcmd::SetGridScale::builder()
1027 .value(NUMBER_OF_GRID_COLUMNS)
1028 .units(unit_length.unwrap_or(kcmc::units::UnitLength::Millimeters))
1029 .build(),
1030 ),
1031 }
1032 }
1033}