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::ExecutorSettings;
42use crate::SourceRange;
43use crate::errors::KclError;
44use crate::errors::KclErrorDetails;
45use crate::execution::DefaultPlanes;
46use crate::execution::IdGenerator;
47use crate::execution::PlaneInfo;
48use crate::execution::Point3d;
49use crate::settings::types::default_backface_color;
50use crate::settings::types::default_backface_color_struct;
51
52lazy_static::lazy_static! {
53 pub static ref GRID_OBJECT_ID: uuid::Uuid = uuid::Uuid::parse_str("cfa78409-653d-4c26-96f1-7c45fb784840").unwrap();
54
55 pub static ref GRID_SCALE_TEXT_OBJECT_ID: uuid::Uuid = uuid::Uuid::parse_str("10782f33-f588-4668-8bcd-040502d26590").unwrap();
56
57 pub static ref DEFAULT_PLANE_INFO: IndexMap<PlaneName, PlaneInfo> = IndexMap::from([
58 (
59 PlaneName::Xy,
60 PlaneInfo {
61 origin: Point3d::new(0.0, 0.0, 0.0, Some(UnitLength::Millimeters)),
62 x_axis: Point3d::new(1.0, 0.0, 0.0, None),
63 y_axis: Point3d::new(0.0, 1.0, 0.0, None),
64 z_axis: Point3d::new(0.0, 0.0, 1.0, None),
65 },
66 ),
67 (
68 PlaneName::NegXy,
69 PlaneInfo {
70 origin: Point3d::new( 0.0, 0.0, 0.0, Some(UnitLength::Millimeters)),
71 x_axis: Point3d::new(-1.0, 0.0, 0.0, None),
72 y_axis: Point3d::new( 0.0, 1.0, 0.0, None),
73 z_axis: Point3d::new( 0.0, 0.0, -1.0, None),
74 },
75 ),
76 (
77 PlaneName::Xz,
78 PlaneInfo {
79 origin: Point3d::new(0.0, 0.0, 0.0, Some(UnitLength::Millimeters)),
80 x_axis: Point3d::new(1.0, 0.0, 0.0, None),
81 y_axis: Point3d::new(0.0, 0.0, 1.0, None),
82 z_axis: Point3d::new(0.0, -1.0, 0.0, None),
83 },
84 ),
85 (
86 PlaneName::NegXz,
87 PlaneInfo {
88 origin: Point3d::new( 0.0, 0.0, 0.0, Some(UnitLength::Millimeters)),
89 x_axis: Point3d::new(-1.0, 0.0, 0.0, None),
90 y_axis: Point3d::new( 0.0, 0.0, 1.0, None),
91 z_axis: Point3d::new( 0.0, 1.0, 0.0, None),
92 },
93 ),
94 (
95 PlaneName::Yz,
96 PlaneInfo {
97 origin: Point3d::new(0.0, 0.0, 0.0, Some(UnitLength::Millimeters)),
98 x_axis: Point3d::new(0.0, 1.0, 0.0, None),
99 y_axis: Point3d::new(0.0, 0.0, 1.0, None),
100 z_axis: Point3d::new(1.0, 0.0, 0.0, None),
101 },
102 ),
103 (
104 PlaneName::NegYz,
105 PlaneInfo {
106 origin: Point3d::new( 0.0, 0.0, 0.0, Some(UnitLength::Millimeters)),
107 x_axis: Point3d::new( 0.0, -1.0, 0.0, None),
108 y_axis: Point3d::new( 0.0, 0.0, 1.0, None),
109 z_axis: Point3d::new(-1.0, 0.0, 0.0, None),
110 },
111 ),
112 ]);
113}
114
115#[derive(Debug, Clone)]
121pub struct EngineBatchContext {
122 batch: Arc<RwLock<Vec<(WebSocketRequest, SourceRange)>>>,
123 batch_end: Arc<RwLock<IndexMap<Uuid, (WebSocketRequest, SourceRange)>>>,
124}
125
126impl Default for EngineBatchContext {
127 fn default() -> Self {
128 Self::new()
129 }
130}
131
132impl EngineBatchContext {
133 pub fn new() -> Self {
134 Self {
135 batch: Arc::new(RwLock::new(Vec::new())),
136 batch_end: Arc::new(RwLock::new(IndexMap::new())),
137 }
138 }
139
140 pub async fn is_empty(&self) -> bool {
141 self.batch.read().await.is_empty() && self.batch_end.read().await.is_empty()
142 }
143
144 async fn clear(&self) {
145 self.batch.write().await.clear();
146 self.batch_end.write().await.clear();
147 }
148
149 async fn push(&self, req: WebSocketRequest, source_range: SourceRange) {
150 self.batch.write().await.push((req, source_range));
151 }
152
153 async fn extend(&self, requests: Vec<(WebSocketRequest, SourceRange)>) {
154 self.batch.write().await.extend(requests);
155 }
156
157 async fn insert_end(&self, id: Uuid, req: WebSocketRequest, source_range: SourceRange) {
158 self.batch_end.write().await.insert(id, (req, source_range));
159 }
160
161 pub(crate) async fn move_batch_end_to_batch(&self, ids: Vec<Uuid>) {
162 let mut moved = Vec::new();
163 {
164 let mut batch_end = self.batch_end.write().await;
165 for id in ids {
166 let Some(item) = batch_end.shift_remove(&id) else {
167 continue;
168 };
169 moved.push(item);
170 }
171 }
172
173 self.extend(moved).await;
174 }
175
176 async fn take_batch(&self) -> Vec<(WebSocketRequest, SourceRange)> {
177 std::mem::take(&mut *self.batch.write().await)
178 }
179
180 async fn take_batch_end(&self) -> IndexMap<Uuid, (WebSocketRequest, SourceRange)> {
181 std::mem::take(&mut *self.batch_end.write().await)
182 }
183}
184
185#[derive(Default, Debug)]
186pub struct EngineStats {
187 pub commands_batched: AtomicUsize,
188 pub batches_sent: AtomicUsize,
189}
190
191impl Clone for EngineStats {
192 fn clone(&self) -> Self {
193 Self {
194 commands_batched: AtomicUsize::new(self.commands_batched.load(Ordering::Relaxed)),
195 batches_sent: AtomicUsize::new(self.batches_sent.load(Ordering::Relaxed)),
196 }
197 }
198}
199
200#[async_trait::async_trait]
201pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
202 fn responses(&self) -> Arc<RwLock<IndexMap<Uuid, WebSocketResponse>>>;
204
205 fn ids_of_async_commands(&self) -> Arc<RwLock<IndexMap<Uuid, SourceRange>>>;
207
208 fn async_tasks(&self) -> AsyncTasks;
210
211 async fn take_ids_of_async_commands(&self) -> IndexMap<Uuid, SourceRange> {
213 std::mem::take(&mut *self.ids_of_async_commands().write().await)
214 }
215
216 async fn take_responses(&self) -> IndexMap<Uuid, WebSocketResponse> {
218 std::mem::take(&mut *self.responses().write().await)
219 }
220
221 fn get_default_planes(&self) -> Arc<RwLock<Option<DefaultPlanes>>>;
223
224 fn stats(&self) -> &EngineStats;
225
226 async fn default_planes(
228 &self,
229 batch_context: &EngineBatchContext,
230 id_generator: &mut IdGenerator,
231 source_range: SourceRange,
232 ) -> Result<DefaultPlanes, KclError> {
233 {
234 let opt = self.get_default_planes().read().await.as_ref().cloned();
235 if let Some(planes) = opt {
236 return Ok(planes);
237 }
238 } let new_planes = self
241 .new_default_planes(batch_context, id_generator, source_range)
242 .await?;
243 *self.get_default_planes().write().await = Some(new_planes.clone());
244
245 Ok(new_planes)
246 }
247
248 async fn clear_scene_post_hook(
251 &self,
252 batch_context: &EngineBatchContext,
253 id_generator: &mut IdGenerator,
254 source_range: SourceRange,
255 ) -> Result<(), crate::errors::KclError>;
256
257 async fn clear_queues(&self, batch_context: &EngineBatchContext) {
258 batch_context.clear().await;
259 self.ids_of_async_commands().write().await.clear();
260 self.async_tasks().clear().await;
261 }
262
263 async fn fetch_debug(&self) -> Result<(), crate::errors::KclError>;
265
266 async fn get_debug(&self) -> Option<OkWebSocketResponseData>;
268
269 async fn inner_fire_modeling_cmd(
271 &self,
272 id: uuid::Uuid,
273 source_range: SourceRange,
274 cmd: WebSocketRequest,
275 id_to_source_range: HashMap<Uuid, SourceRange>,
276 ) -> Result<(), crate::errors::KclError>;
277
278 async fn inner_send_modeling_cmd(
280 &self,
281 id: uuid::Uuid,
282 source_range: SourceRange,
283 cmd: WebSocketRequest,
284 id_to_source_range: HashMap<uuid::Uuid, SourceRange>,
285 ) -> Result<kcmc::websocket::WebSocketResponse, crate::errors::KclError>;
286
287 async fn clear_scene(
288 &self,
289 batch_context: &EngineBatchContext,
290 id_generator: &mut IdGenerator,
291 source_range: SourceRange,
292 ) -> Result<(), crate::errors::KclError> {
293 self.clear_queues(batch_context).await;
295
296 self.batch_modeling_cmd(
297 batch_context,
298 id_generator.next_uuid(),
299 source_range,
300 &ModelingCmd::SceneClearAll(mcmd::SceneClearAll::default()),
301 )
302 .await?;
303
304 self.flush_batch(batch_context, false, source_range).await?;
307
308 self.clear_scene_post_hook(batch_context, id_generator, source_range)
310 .await?;
311
312 Ok(())
313 }
314
315 async fn ensure_async_command_completed(
317 &self,
318 id: uuid::Uuid,
319 source_range: Option<SourceRange>,
320 ) -> Result<OkWebSocketResponseData, KclError> {
321 let source_range = if let Some(source_range) = source_range {
322 source_range
323 } else {
324 self.ids_of_async_commands()
326 .read()
327 .await
328 .get(&id)
329 .cloned()
330 .unwrap_or_default()
331 };
332
333 const ASYNC_CMD_TIMEOUT_SECS: u64 = 600;
341 let current_time = Instant::now();
342 while current_time.elapsed().as_secs() < ASYNC_CMD_TIMEOUT_SECS {
343 let responses = self.responses().read().await.clone();
344 let Some(resp) = responses.get(&id) else {
345 #[cfg(target_arch = "wasm32")]
348 {
349 let duration = web_time::Duration::from_millis(1);
350 wasm_timer::Delay::new(duration).await.map_err(|err| {
351 KclError::new_internal(KclErrorDetails::new(
352 format!("Failed to sleep: {:?}", err),
353 vec![source_range],
354 ))
355 })?;
356 }
357 #[cfg(not(target_arch = "wasm32"))]
358 tokio::task::yield_now().await;
359 continue;
360 };
361
362 let response = self.parse_websocket_response(resp.clone(), source_range)?;
365 return Ok(response);
366 }
367
368 Err(KclError::new_engine(KclErrorDetails::new(
369 format!(
370 "async command timed out after {ASYNC_CMD_TIMEOUT_SECS}s (client-side ceiling, not an engine error)"
371 ),
372 vec![source_range],
373 )))
374 }
375
376 async fn ensure_async_commands_completed(&self, batch_context: &EngineBatchContext) -> Result<(), KclError> {
378 let ids = self.take_ids_of_async_commands().await;
380
381 for (id, source_range) in ids {
383 self.ensure_async_command_completed(id, Some(source_range)).await?;
384 }
385
386 if let Err(err) = self.async_tasks().join_all().await {
392 crate::log::logln!(
393 "Error waiting for async tasks (this is typically fine and just means that an edge became something else): {:?}",
394 err
395 );
396 }
397
398 self.flush_batch(batch_context, true, SourceRange::default()).await?;
400
401 Ok(())
402 }
403
404 async fn set_edge_visibility(
406 &self,
407 batch_context: &EngineBatchContext,
408 visible: bool,
409 source_range: SourceRange,
410 id_generator: &mut IdGenerator,
411 ) -> Result<(), crate::errors::KclError> {
412 self.batch_modeling_cmd(
413 batch_context,
414 id_generator.next_uuid(),
415 source_range,
416 &ModelingCmd::from(mcmd::EdgeLinesVisible::builder().hidden(!visible).build()),
417 )
418 .await?;
419
420 Ok(())
421 }
422
423 async fn reapply_settings(
425 &self,
426 batch_context: &EngineBatchContext,
427 settings: &crate::ExecutorSettings,
428 source_range: SourceRange,
429 id_generator: &mut IdGenerator,
430 grid_scale_unit: GridScaleBehavior,
431 ) -> Result<(), crate::errors::KclError> {
432 self.set_edge_visibility(batch_context, settings.highlight_edges, source_range, id_generator)
434 .await?;
435
436 self.modify_grid(
439 batch_context,
440 !settings.show_grid,
441 grid_scale_unit,
442 source_range,
443 id_generator,
444 )
445 .await?;
446
447 self.set_user_colors(batch_context, settings, source_range, id_generator)
449 .await?;
450
451 self.flush_batch(batch_context, false, source_range).await?;
455
456 Ok(())
457 }
458
459 async fn batch_modeling_cmd(
461 &self,
462 batch_context: &EngineBatchContext,
463 id: uuid::Uuid,
464 source_range: SourceRange,
465 cmd: &ModelingCmd,
466 ) -> Result<(), crate::errors::KclError> {
467 let req = WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
468 cmd: cmd.clone(),
469 cmd_id: id.into(),
470 });
471
472 batch_context.push(req, source_range).await;
474 self.stats().commands_batched.fetch_add(1, Ordering::Relaxed);
475
476 Ok(())
477 }
478
479 async fn batch_modeling_cmds(
484 &self,
485 batch_context: &EngineBatchContext,
486 source_range: SourceRange,
487 cmds: &[ModelingCmdReq],
488 ) -> Result<(), crate::errors::KclError> {
489 let mut extended_cmds = Vec::with_capacity(cmds.len());
491 for cmd in cmds {
492 extended_cmds.push((WebSocketRequest::ModelingCmdReq(cmd.clone()), source_range));
493 }
494 self.stats()
495 .commands_batched
496 .fetch_add(extended_cmds.len(), Ordering::Relaxed);
497 batch_context.extend(extended_cmds).await;
498
499 Ok(())
500 }
501
502 async fn batch_end_cmd(
506 &self,
507 batch_context: &EngineBatchContext,
508 id: uuid::Uuid,
509 source_range: SourceRange,
510 cmd: &ModelingCmd,
511 ) -> Result<(), crate::errors::KclError> {
512 let req = WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
513 cmd: cmd.clone(),
514 cmd_id: id.into(),
515 });
516
517 batch_context.insert_end(id, req, source_range).await;
519 self.stats().commands_batched.fetch_add(1, Ordering::Relaxed);
520 Ok(())
521 }
522
523 async fn send_modeling_cmd(
525 &self,
526 batch_context: &EngineBatchContext,
527 id: uuid::Uuid,
528 source_range: SourceRange,
529 cmd: &ModelingCmd,
530 ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
531 let mut requests = batch_context.take_batch().await;
532
533 requests.push((
535 WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
536 cmd: cmd.clone(),
537 cmd_id: id.into(),
538 }),
539 source_range,
540 ));
541 self.stats().commands_batched.fetch_add(1, Ordering::Relaxed);
542
543 self.run_batch(requests, source_range).await
545 }
546
547 async fn async_modeling_cmd(
550 &self,
551 id: uuid::Uuid,
552 source_range: SourceRange,
553 cmd: &ModelingCmd,
554 ) -> Result<(), crate::errors::KclError> {
555 self.ids_of_async_commands().write().await.insert(id, source_range);
557
558 self.inner_fire_modeling_cmd(
560 id,
561 source_range,
562 WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
563 cmd: cmd.clone(),
564 cmd_id: id.into(),
565 }),
566 HashMap::from([(id, source_range)]),
567 )
568 .await?;
569
570 Ok(())
571 }
572
573 async fn run_batch(
575 &self,
576 orig_requests: Vec<(WebSocketRequest, SourceRange)>,
577 source_range: SourceRange,
578 ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
579 if orig_requests.is_empty() {
581 return Ok(OkWebSocketResponseData::Modeling {
582 modeling_response: OkModelingCmdResponse::Empty {},
583 });
584 }
585
586 let requests: Vec<ModelingCmdReq> = orig_requests
587 .iter()
588 .filter_map(|(val, _)| match val {
589 WebSocketRequest::ModelingCmdReq(ModelingCmdReq { cmd, cmd_id }) => Some(ModelingCmdReq {
590 cmd: cmd.clone(),
591 cmd_id: *cmd_id,
592 }),
593 _ => None,
594 })
595 .collect();
596
597 let batched_requests = WebSocketRequest::ModelingCmdBatchReq(ModelingBatch {
598 requests,
599 batch_id: uuid::Uuid::new_v4().into(),
600 responses: true,
601 });
602
603 let final_req = if orig_requests.len() == 1 {
604 orig_requests.first().unwrap().0.clone()
606 } else {
607 batched_requests
608 };
609
610 let mut id_to_source_range = HashMap::new();
613 for (req, range) in orig_requests.iter() {
614 match req {
615 WebSocketRequest::ModelingCmdReq(ModelingCmdReq { cmd: _, cmd_id }) => {
616 id_to_source_range.insert(Uuid::from(*cmd_id), *range);
617 }
618 _ => {
619 return Err(KclError::new_engine(KclErrorDetails::new(
620 format!("The request is not a modeling command: {req:?}"),
621 vec![*range],
622 )));
623 }
624 }
625 }
626
627 self.stats().batches_sent.fetch_add(1, Ordering::Relaxed);
628
629 match final_req {
631 WebSocketRequest::ModelingCmdBatchReq(ModelingBatch {
632 ref requests,
633 batch_id,
634 responses: _,
635 }) => {
636 let last_id = requests.last().unwrap().cmd_id;
638 let ws_resp = self
639 .inner_send_modeling_cmd(batch_id.into(), source_range, final_req, id_to_source_range.clone())
640 .await?;
641 let response = self.parse_websocket_response(ws_resp, source_range)?;
642
643 if let OkWebSocketResponseData::ModelingBatch { responses } = response {
645 let responses = responses.into_iter().map(|(k, v)| (Uuid::from(k), v)).collect();
646 self.parse_batch_responses(last_id.into(), id_to_source_range, responses)
647 } else {
648 Err(KclError::new_engine(KclErrorDetails::new(
650 format!("Failed to get batch response: {response:?}"),
651 vec![source_range],
652 )))
653 }
654 }
655 WebSocketRequest::ModelingCmdReq(ModelingCmdReq { cmd: _, cmd_id }) => {
656 let source_range = id_to_source_range.get(cmd_id.as_ref()).cloned().ok_or_else(|| {
664 KclError::new_engine(KclErrorDetails::new(
665 format!("Failed to get source range for command ID: {cmd_id:?}"),
666 vec![],
667 ))
668 })?;
669 let ws_resp = self
670 .inner_send_modeling_cmd(cmd_id.into(), source_range, final_req, id_to_source_range)
671 .await?;
672 self.parse_websocket_response(ws_resp, source_range)
673 }
674 _ => Err(KclError::new_engine(KclErrorDetails::new(
675 format!("The final request is not a modeling command: {final_req:?}"),
676 vec![source_range],
677 ))),
678 }
679 }
680
681 async fn flush_batch(
683 &self,
684 batch_context: &EngineBatchContext,
685 batch_end: bool,
688 source_range: SourceRange,
689 ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
690 let all_requests = if batch_end {
691 let mut requests = batch_context.take_batch().await;
692 requests.extend(batch_context.take_batch_end().await.values().cloned());
693 requests
694 } else {
695 batch_context.take_batch().await
696 };
697
698 self.run_batch(all_requests, source_range).await
699 }
700
701 async fn make_default_plane(
702 &self,
703 batch_context: &EngineBatchContext,
704 plane_id: uuid::Uuid,
705 info: &PlaneInfo,
706 color: Option<Color>,
707 source_range: SourceRange,
708 id_generator: &mut IdGenerator,
709 ) -> Result<uuid::Uuid, KclError> {
710 let default_size = 100.0;
712
713 self.batch_modeling_cmd(
714 batch_context,
715 plane_id,
716 source_range,
717 &ModelingCmd::from(
718 mcmd::MakePlane::builder()
719 .clobber(false)
720 .origin(info.origin.into())
721 .size(LengthUnit(default_size))
722 .x_axis(info.x_axis.into())
723 .y_axis(info.y_axis.into())
724 .hide(true)
725 .build(),
726 ),
727 )
728 .await?;
729
730 if let Some(color) = color {
731 self.batch_modeling_cmd(
733 batch_context,
734 id_generator.next_uuid(),
735 source_range,
736 &ModelingCmd::from(mcmd::PlaneSetColor::builder().color(color).plane_id(plane_id).build()),
737 )
738 .await?;
739 }
740
741 Ok(plane_id)
742 }
743
744 async fn new_default_planes(
745 &self,
746 batch_context: &EngineBatchContext,
747 id_generator: &mut IdGenerator,
748 source_range: SourceRange,
749 ) -> Result<DefaultPlanes, KclError> {
750 let plane_opacity = 0.1;
751 let plane_settings: Vec<(PlaneName, Uuid, Option<Color>)> = vec![
752 (
753 PlaneName::Xy,
754 id_generator.next_uuid(),
755 Some(Color::from_rgba(0.7, 0.28, 0.28, plane_opacity)),
756 ),
757 (
758 PlaneName::Yz,
759 id_generator.next_uuid(),
760 Some(Color::from_rgba(0.28, 0.7, 0.28, plane_opacity)),
761 ),
762 (
763 PlaneName::Xz,
764 id_generator.next_uuid(),
765 Some(Color::from_rgba(0.28, 0.28, 0.7, plane_opacity)),
766 ),
767 (PlaneName::NegXy, id_generator.next_uuid(), None),
768 (PlaneName::NegYz, id_generator.next_uuid(), None),
769 (PlaneName::NegXz, id_generator.next_uuid(), None),
770 ];
771
772 let mut planes = HashMap::new();
773 for (name, plane_id, color) in plane_settings {
774 let info = DEFAULT_PLANE_INFO.get(&name).ok_or_else(|| {
775 KclError::new_engine(KclErrorDetails::new(
777 format!("Failed to get default plane info for: {name:?}"),
778 vec![source_range],
779 ))
780 })?;
781 planes.insert(
782 name,
783 self.make_default_plane(batch_context, plane_id, info, color, source_range, id_generator)
784 .await?,
785 );
786 }
787
788 self.flush_batch(batch_context, false, source_range).await?;
790
791 Ok(DefaultPlanes {
792 xy: planes[&PlaneName::Xy],
793 neg_xy: planes[&PlaneName::NegXy],
794 xz: planes[&PlaneName::Xz],
795 neg_xz: planes[&PlaneName::NegXz],
796 yz: planes[&PlaneName::Yz],
797 neg_yz: planes[&PlaneName::NegYz],
798 })
799 }
800
801 fn parse_websocket_response(
802 &self,
803 response: WebSocketResponse,
804 source_range: SourceRange,
805 ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
806 match response {
807 WebSocketResponse::Success(success) => Ok(success.resp),
808 WebSocketResponse::Failure(fail) => {
809 let _request_id = fail.request_id;
810 if fail.errors.is_empty() {
811 return Err(KclError::new_engine(KclErrorDetails::new(
812 "Failure response with no error details".to_owned(),
813 vec![source_range],
814 )));
815 }
816 Err(KclError::new_engine(KclErrorDetails::new(
817 fail.errors
818 .iter()
819 .map(|e| e.message.clone())
820 .collect::<Vec<_>>()
821 .join("\n"),
822 vec![source_range],
823 )))
824 }
825 }
826 }
827
828 fn parse_batch_responses(
829 &self,
830 id: uuid::Uuid,
832 id_to_source_range: HashMap<uuid::Uuid, SourceRange>,
834 responses: HashMap<uuid::Uuid, BatchResponse>,
836 ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
837 #[expect(
839 clippy::iter_over_hash_type,
840 reason = "modeling command uses a HashMap and keys are random, so we don't really have a choice"
841 )]
842 for (cmd_id, resp) in responses.iter() {
843 match resp {
844 BatchResponse::Success { response } => {
845 if cmd_id == &id {
846 return Ok(OkWebSocketResponseData::Modeling {
848 modeling_response: response.clone(),
849 });
850 } else {
851 continue;
853 }
854 }
855 BatchResponse::Failure { errors } => {
856 let source_range = id_to_source_range.get(cmd_id).cloned().ok_or_else(|| {
858 KclError::new_engine(KclErrorDetails::new(
859 format!("Failed to get source range for command ID: {cmd_id:?}"),
860 vec![],
861 ))
862 })?;
863 if errors.is_empty() {
864 return Err(KclError::new_engine(KclErrorDetails::new(
865 "Failure response for batch with no error details".to_owned(),
866 vec![source_range],
867 )));
868 }
869 return Err(KclError::new_engine(KclErrorDetails::new(
870 errors.iter().map(|e| e.message.clone()).collect::<Vec<_>>().join("\n"),
871 vec![source_range],
872 )));
873 }
874 }
875 }
876
877 Err(KclError::new_engine(KclErrorDetails::new(
880 format!("Failed to find response for command ID: {id:?}"),
881 vec![],
882 )))
883 }
884
885 async fn set_user_colors(
886 &self,
887 batch_context: &EngineBatchContext,
888 settings: &ExecutorSettings,
889 source_range: SourceRange,
890 id_generator: &mut IdGenerator,
891 ) -> Result<(), KclError> {
892 let bf = settings
893 .default_backface_color
894 .clone()
895 .unwrap_or(default_backface_color());
896 let backface = csscolorparser::parse(&bf)
897 .map(|color| kcmc::shared::Color::from_rgba(color.r, color.g, color.b, color.a))
898 .unwrap_or(default_backface_color_struct());
899 self.batch_modeling_cmd(
900 batch_context,
901 id_generator.next_uuid(),
902 source_range,
903 &ModelingCmd::from(
904 mcmd::SetDefaultSystemProperties::builder()
905 .backface_color(backface)
906 .build(),
907 ),
908 )
909 .await?;
910 Ok(())
911 }
912
913 async fn modify_grid(
914 &self,
915 batch_context: &EngineBatchContext,
916 hidden: bool,
917 grid_scale_behavior: GridScaleBehavior,
918 source_range: SourceRange,
919 id_generator: &mut IdGenerator,
920 ) -> Result<(), KclError> {
921 self.batch_modeling_cmd(
923 batch_context,
924 id_generator.next_uuid(),
925 source_range,
926 &ModelingCmd::from(
927 mcmd::ObjectVisible::builder()
928 .hidden(hidden)
929 .object_id(*GRID_OBJECT_ID)
930 .build(),
931 ),
932 )
933 .await?;
934
935 self.batch_modeling_cmd(
936 batch_context,
937 id_generator.next_uuid(),
938 source_range,
939 &grid_scale_behavior.into_modeling_cmd(),
940 )
941 .await?;
942
943 self.batch_modeling_cmd(
945 batch_context,
946 id_generator.next_uuid(),
947 source_range,
948 &ModelingCmd::from(
949 mcmd::ObjectVisible::builder()
950 .hidden(hidden)
951 .object_id(*GRID_SCALE_TEXT_OBJECT_ID)
952 .build(),
953 ),
954 )
955 .await?;
956
957 Ok(())
958 }
959
960 async fn get_session_data(&self) -> Option<ModelingSessionData> {
963 None
964 }
965
966 async fn close(&self);
968}
969
970#[derive(Debug, Hash, Eq, Copy, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, Display, FromStr)]
971#[ts(export)]
972#[serde(rename_all = "camelCase")]
973pub enum PlaneName {
974 #[display("XY")]
976 Xy,
977 #[display("-XY")]
979 NegXy,
980 #[display("XZ")]
982 Xz,
983 #[display("-XZ")]
985 NegXz,
986 #[display("YZ")]
988 Yz,
989 #[display("-YZ")]
991 NegYz,
992}
993
994#[cfg(not(target_arch = "wasm32"))]
996pub fn new_zoo_client(token: Option<String>, engine_addr: Option<String>) -> anyhow::Result<kittycad::Client> {
997 let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"),);
998 let http_client = reqwest::Client::builder()
999 .user_agent(user_agent)
1000 .timeout(std::time::Duration::from_secs(600))
1002 .connect_timeout(std::time::Duration::from_secs(60));
1003 let ws_client = reqwest::Client::builder()
1004 .user_agent(user_agent)
1005 .timeout(std::time::Duration::from_secs(600))
1007 .connect_timeout(std::time::Duration::from_secs(60))
1008 .connection_verbose(true)
1009 .tcp_keepalive(std::time::Duration::from_secs(600))
1010 .http1_only();
1011
1012 let zoo_token_env = std::env::var("ZOO_API_TOKEN");
1013
1014 let token = if let Some(token) = token {
1015 token
1016 } else if let Ok(token) = std::env::var("KITTYCAD_API_TOKEN") {
1017 if let Ok(zoo_token) = zoo_token_env
1018 && zoo_token != token
1019 {
1020 return Err(anyhow::anyhow!(
1021 "Both environment variables KITTYCAD_API_TOKEN=`{}` and ZOO_API_TOKEN=`{}` are set. Use only one.",
1022 token,
1023 zoo_token
1024 ));
1025 }
1026 token
1027 } else if let Ok(token) = zoo_token_env {
1028 token
1029 } else {
1030 return Err(anyhow::anyhow!(
1031 "No API token found in environment variables. Use ZOO_API_TOKEN"
1032 ));
1033 };
1034
1035 let mut client = kittycad::Client::new_from_reqwest(token, http_client, ws_client);
1037 let kittycad_host_env = std::env::var("KITTYCAD_HOST");
1039 if let Some(addr) = engine_addr {
1040 client.set_base_url(addr);
1041 } else if let Ok(addr) = std::env::var("ZOO_HOST") {
1042 if let Ok(kittycad_host) = kittycad_host_env
1043 && kittycad_host != addr
1044 {
1045 return Err(anyhow::anyhow!(
1046 "Both environment variables KITTYCAD_HOST=`{}` and ZOO_HOST=`{}` are set. Use only one.",
1047 kittycad_host,
1048 addr
1049 ));
1050 }
1051 client.set_base_url(addr);
1052 } else if let Ok(addr) = kittycad_host_env {
1053 client.set_base_url(addr);
1054 }
1055
1056 Ok(client)
1057}
1058
1059#[derive(Copy, Clone, Debug)]
1060pub enum GridScaleBehavior {
1061 ScaleWithZoom,
1062 Fixed(Option<kcmc::units::UnitLength>),
1063}
1064
1065impl GridScaleBehavior {
1066 fn into_modeling_cmd(self) -> ModelingCmd {
1067 const NUMBER_OF_GRID_COLUMNS: f32 = 10.0;
1068 match self {
1069 GridScaleBehavior::ScaleWithZoom => ModelingCmd::from(mcmd::SetGridAutoScale::builder().build()),
1070 GridScaleBehavior::Fixed(unit_length) => ModelingCmd::from(
1071 mcmd::SetGridScale::builder()
1072 .value(NUMBER_OF_GRID_COLUMNS)
1073 .units(unit_length.unwrap_or(kcmc::units::UnitLength::Millimeters))
1074 .build(),
1075 ),
1076 }
1077 }
1078}