1#[cfg(not(target_arch = "wasm32"))]
4#[cfg(feature = "engine")]
5pub mod conn;
6pub mod conn_mock;
7#[cfg(target_arch = "wasm32")]
8#[cfg(feature = "engine")]
9pub mod conn_wasm;
10
11use std::{
12 collections::HashMap,
13 sync::{
14 atomic::{AtomicUsize, Ordering},
15 Arc,
16 },
17};
18
19use indexmap::IndexMap;
20use kcmc::{
21 each_cmd as mcmd,
22 id::ModelingCmdId,
23 length_unit::LengthUnit,
24 ok_response::OkModelingCmdResponse,
25 shared::Color,
26 websocket::{
27 BatchResponse, ModelingBatch, ModelingCmdReq, ModelingSessionData, OkWebSocketResponseData, WebSocketRequest,
28 WebSocketResponse,
29 },
30 ModelingCmd,
31};
32use kittycad_modeling_cmds as kcmc;
33use schemars::JsonSchema;
34use serde::{Deserialize, Serialize};
35use tokio::sync::RwLock;
36use uuid::Uuid;
37
38use crate::{
39 errors::{KclError, KclErrorDetails},
40 execution::{ArtifactCommand, DefaultPlanes, IdGenerator, Point3d},
41 SourceRange,
42};
43
44lazy_static::lazy_static! {
45 pub static ref GRID_OBJECT_ID: uuid::Uuid = uuid::Uuid::parse_str("cfa78409-653d-4c26-96f1-7c45fb784840").unwrap();
46
47 pub static ref GRID_SCALE_TEXT_OBJECT_ID: uuid::Uuid = uuid::Uuid::parse_str("10782f33-f588-4668-8bcd-040502d26590").unwrap();
48}
49
50#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS, JsonSchema)]
53#[ts(export)]
54#[serde(rename_all = "camelCase")]
55pub enum ExecutionKind {
56 #[default]
57 Normal,
58 Isolated,
59}
60
61impl ExecutionKind {
62 pub fn is_isolated(&self) -> bool {
63 matches!(self, ExecutionKind::Isolated)
64 }
65}
66
67#[derive(Default, Debug)]
68pub struct EngineStats {
69 pub commands_batched: AtomicUsize,
70 pub batches_sent: AtomicUsize,
71}
72
73impl Clone for EngineStats {
74 fn clone(&self) -> Self {
75 Self {
76 commands_batched: AtomicUsize::new(self.commands_batched.load(Ordering::Relaxed)),
77 batches_sent: AtomicUsize::new(self.batches_sent.load(Ordering::Relaxed)),
78 }
79 }
80}
81
82#[async_trait::async_trait]
83pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
84 fn batch(&self) -> Arc<RwLock<Vec<(WebSocketRequest, SourceRange)>>>;
86
87 fn batch_end(&self) -> Arc<RwLock<IndexMap<uuid::Uuid, (WebSocketRequest, SourceRange)>>>;
89
90 fn responses(&self) -> Arc<RwLock<IndexMap<Uuid, WebSocketResponse>>>;
92
93 fn artifact_commands(&self) -> Arc<RwLock<Vec<ArtifactCommand>>>;
95
96 async fn clear_artifact_commands(&self) {
98 self.artifact_commands().write().await.clear();
99 }
100
101 async fn take_artifact_commands(&self) -> Vec<ArtifactCommand> {
103 std::mem::take(&mut *self.artifact_commands().write().await)
104 }
105
106 async fn take_responses(&self) -> IndexMap<Uuid, WebSocketResponse> {
108 std::mem::take(&mut *self.responses().write().await)
109 }
110
111 async fn execution_kind(&self) -> ExecutionKind;
113
114 async fn replace_execution_kind(&self, execution_kind: ExecutionKind) -> ExecutionKind;
117
118 fn get_default_planes(&self) -> Arc<RwLock<Option<DefaultPlanes>>>;
120
121 fn stats(&self) -> &EngineStats;
122
123 async fn default_planes(
125 &self,
126 id_generator: &mut IdGenerator,
127 source_range: SourceRange,
128 ) -> Result<DefaultPlanes, KclError> {
129 {
130 let opt = self.get_default_planes().read().await.as_ref().cloned();
131 if let Some(planes) = opt {
132 return Ok(planes);
133 }
134 } let new_planes = self.new_default_planes(id_generator, source_range).await?;
137 *self.get_default_planes().write().await = Some(new_planes.clone());
138
139 Ok(new_planes)
140 }
141
142 async fn clear_scene_post_hook(
145 &self,
146 id_generator: &mut IdGenerator,
147 source_range: SourceRange,
148 ) -> Result<(), crate::errors::KclError>;
149
150 async fn clear_queues(&self) {
151 self.batch().write().await.clear();
152 self.batch_end().write().await.clear();
153 }
154
155 async fn inner_send_modeling_cmd(
157 &self,
158 id: uuid::Uuid,
159 source_range: SourceRange,
160 cmd: WebSocketRequest,
161 id_to_source_range: HashMap<uuid::Uuid, SourceRange>,
162 ) -> Result<kcmc::websocket::WebSocketResponse, crate::errors::KclError>;
163
164 async fn clear_scene(
165 &self,
166 id_generator: &mut IdGenerator,
167 source_range: SourceRange,
168 ) -> Result<(), crate::errors::KclError> {
169 self.clear_queues().await;
171
172 self.batch_modeling_cmd(
173 uuid::Uuid::new_v4(),
174 source_range,
175 &ModelingCmd::SceneClearAll(mcmd::SceneClearAll::default()),
176 )
177 .await?;
178
179 self.flush_batch(false, source_range).await?;
182
183 self.clear_artifact_commands().await;
186
187 self.clear_scene_post_hook(id_generator, source_range).await?;
189
190 Ok(())
191 }
192
193 async fn set_edge_visibility(
195 &self,
196 visible: bool,
197 source_range: SourceRange,
198 ) -> Result<(), crate::errors::KclError> {
199 self.batch_modeling_cmd(
200 uuid::Uuid::new_v4(),
201 source_range,
202 &ModelingCmd::from(mcmd::EdgeLinesVisible { hidden: !visible }),
203 )
204 .await?;
205
206 Ok(())
207 }
208
209 async fn handle_artifact_command(
210 &self,
211 cmd: &ModelingCmd,
212 cmd_id: ModelingCmdId,
213 id_to_source_range: &HashMap<Uuid, SourceRange>,
214 ) -> Result<(), KclError> {
215 let cmd_id = *cmd_id.as_ref();
216 let range = id_to_source_range
217 .get(&cmd_id)
218 .copied()
219 .ok_or_else(|| KclError::internal(format!("Failed to get source range for command ID: {:?}", cmd_id)))?;
220
221 self.artifact_commands().write().await.push(ArtifactCommand {
223 cmd_id,
224 range,
225 command: cmd.clone(),
226 });
227 Ok(())
228 }
229
230 async fn set_units(
231 &self,
232 units: crate::UnitLength,
233 source_range: SourceRange,
234 ) -> Result<(), crate::errors::KclError> {
235 self.batch_modeling_cmd(
237 uuid::Uuid::new_v4(),
238 source_range,
239 &ModelingCmd::from(mcmd::SetSceneUnits { unit: units.into() }),
240 )
241 .await?;
242
243 Ok(())
244 }
245
246 async fn reapply_settings(
248 &self,
249 settings: &crate::ExecutorSettings,
250 source_range: SourceRange,
251 ) -> Result<(), crate::errors::KclError> {
252 self.set_edge_visibility(settings.highlight_edges, source_range).await?;
254
255 self.set_units(settings.units, source_range).await?;
257
258 self.modify_grid(!settings.show_grid, source_range).await?;
260
261 self.flush_batch(false, source_range).await?;
265
266 Ok(())
267 }
268
269 async fn batch_modeling_cmd(
271 &self,
272 id: uuid::Uuid,
273 source_range: SourceRange,
274 cmd: &ModelingCmd,
275 ) -> Result<(), crate::errors::KclError> {
276 if self.execution_kind().await.is_isolated() {
278 return Ok(());
279 }
280
281 let req = WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
282 cmd: cmd.clone(),
283 cmd_id: id.into(),
284 });
285
286 self.batch().write().await.push((req, source_range));
288 self.stats().commands_batched.fetch_add(1, Ordering::Relaxed);
289
290 Ok(())
291 }
292
293 async fn batch_modeling_cmds(
298 &self,
299 source_range: SourceRange,
300 cmds: &[ModelingCmdReq],
301 ) -> Result<(), crate::errors::KclError> {
302 if self.execution_kind().await.is_isolated() {
304 return Ok(());
305 }
306
307 let mut extended_cmds = Vec::with_capacity(cmds.len());
309 for cmd in cmds {
310 extended_cmds.push((WebSocketRequest::ModelingCmdReq(cmd.clone()), source_range));
311 }
312 self.stats()
313 .commands_batched
314 .fetch_add(extended_cmds.len(), Ordering::Relaxed);
315 self.batch().write().await.extend(extended_cmds);
316
317 Ok(())
318 }
319
320 async fn batch_end_cmd(
324 &self,
325 id: uuid::Uuid,
326 source_range: SourceRange,
327 cmd: &ModelingCmd,
328 ) -> Result<(), crate::errors::KclError> {
329 if self.execution_kind().await.is_isolated() {
331 return Ok(());
332 }
333
334 let req = WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
335 cmd: cmd.clone(),
336 cmd_id: id.into(),
337 });
338
339 self.batch_end().write().await.insert(id, (req, source_range));
341 self.stats().commands_batched.fetch_add(1, Ordering::Relaxed);
342 Ok(())
343 }
344
345 async fn send_modeling_cmd(
347 &self,
348 id: uuid::Uuid,
349 source_range: SourceRange,
350 cmd: &ModelingCmd,
351 ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
352 self.batch_modeling_cmd(id, source_range, cmd).await?;
353
354 self.flush_batch(false, source_range).await
356 }
357
358 async fn flush_batch(
360 &self,
361 batch_end: bool,
364 source_range: SourceRange,
365 ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
366 let all_requests = if batch_end {
367 let mut requests = self.batch().read().await.clone();
368 requests.extend(self.batch_end().read().await.values().cloned());
369 requests
370 } else {
371 self.batch().read().await.clone()
372 };
373
374 if all_requests.is_empty() {
376 return Ok(OkWebSocketResponseData::Modeling {
377 modeling_response: OkModelingCmdResponse::Empty {},
378 });
379 }
380
381 let requests: Vec<ModelingCmdReq> = all_requests
382 .iter()
383 .filter_map(|(val, _)| match val {
384 WebSocketRequest::ModelingCmdReq(ModelingCmdReq { cmd, cmd_id }) => Some(ModelingCmdReq {
385 cmd: cmd.clone(),
386 cmd_id: *cmd_id,
387 }),
388 _ => None,
389 })
390 .collect();
391
392 let batched_requests = WebSocketRequest::ModelingCmdBatchReq(ModelingBatch {
393 requests,
394 batch_id: uuid::Uuid::new_v4().into(),
395 responses: true,
396 });
397
398 let final_req = if all_requests.len() == 1 {
399 all_requests.first().unwrap().0.clone()
401 } else {
402 batched_requests
403 };
404
405 let mut id_to_source_range = HashMap::new();
408 for (req, range) in all_requests.iter() {
409 match req {
410 WebSocketRequest::ModelingCmdReq(ModelingCmdReq { cmd: _, cmd_id }) => {
411 id_to_source_range.insert(Uuid::from(*cmd_id), *range);
412 }
413 _ => {
414 return Err(KclError::Engine(KclErrorDetails {
415 message: format!("The request is not a modeling command: {:?}", req),
416 source_ranges: vec![*range],
417 }));
418 }
419 }
420 }
421
422 for (req, _) in all_requests.iter() {
424 match &req {
425 WebSocketRequest::ModelingCmdBatchReq(ModelingBatch { requests, .. }) => {
426 for request in requests {
427 self.handle_artifact_command(&request.cmd, request.cmd_id, &id_to_source_range)
428 .await?;
429 }
430 }
431 WebSocketRequest::ModelingCmdReq(request) => {
432 self.handle_artifact_command(&request.cmd, request.cmd_id, &id_to_source_range)
433 .await?;
434 }
435 _ => {}
436 }
437 }
438
439 self.batch().write().await.clear();
441 if batch_end {
442 self.batch_end().write().await.clear();
443 }
444 self.stats().batches_sent.fetch_add(1, Ordering::Relaxed);
445
446 match final_req {
448 WebSocketRequest::ModelingCmdBatchReq(ModelingBatch {
449 ref requests,
450 batch_id,
451 responses: _,
452 }) => {
453 let last_id = requests.last().unwrap().cmd_id;
455 let ws_resp = self
456 .inner_send_modeling_cmd(batch_id.into(), source_range, final_req, id_to_source_range.clone())
457 .await?;
458 let response = self.parse_websocket_response(ws_resp, source_range)?;
459
460 if let OkWebSocketResponseData::ModelingBatch { responses } = response {
462 let responses = responses.into_iter().map(|(k, v)| (Uuid::from(k), v)).collect();
463 self.parse_batch_responses(last_id.into(), id_to_source_range, responses)
464 } else {
465 Err(KclError::Engine(KclErrorDetails {
467 message: format!("Failed to get batch response: {:?}", response),
468 source_ranges: vec![source_range],
469 }))
470 }
471 }
472 WebSocketRequest::ModelingCmdReq(ModelingCmdReq { cmd: _, cmd_id }) => {
473 let source_range = id_to_source_range.get(cmd_id.as_ref()).cloned().ok_or_else(|| {
481 KclError::Engine(KclErrorDetails {
482 message: format!("Failed to get source range for command ID: {:?}", cmd_id),
483 source_ranges: vec![],
484 })
485 })?;
486 let ws_resp = self
487 .inner_send_modeling_cmd(cmd_id.into(), source_range, final_req, id_to_source_range)
488 .await?;
489 self.parse_websocket_response(ws_resp, source_range)
490 }
491 _ => Err(KclError::Engine(KclErrorDetails {
492 message: format!("The final request is not a modeling command: {:?}", final_req),
493 source_ranges: vec![source_range],
494 })),
495 }
496 }
497
498 async fn make_default_plane(
499 &self,
500 plane_id: uuid::Uuid,
501 x_axis: Point3d,
502 y_axis: Point3d,
503 color: Option<Color>,
504 source_range: SourceRange,
505 ) -> Result<uuid::Uuid, KclError> {
506 let default_size = 100.0;
508 let default_origin = Point3d { x: 0.0, y: 0.0, z: 0.0 }.into();
509
510 self.batch_modeling_cmd(
511 plane_id,
512 source_range,
513 &ModelingCmd::from(mcmd::MakePlane {
514 clobber: false,
515 origin: default_origin,
516 size: LengthUnit(default_size),
517 x_axis: x_axis.into(),
518 y_axis: y_axis.into(),
519 hide: Some(true),
520 }),
521 )
522 .await?;
523
524 if let Some(color) = color {
525 self.batch_modeling_cmd(
527 uuid::Uuid::new_v4(),
528 source_range,
529 &ModelingCmd::from(mcmd::PlaneSetColor { color, plane_id }),
530 )
531 .await?;
532 }
533
534 Ok(plane_id)
535 }
536
537 async fn new_default_planes(
538 &self,
539 id_generator: &mut IdGenerator,
540 source_range: SourceRange,
541 ) -> Result<DefaultPlanes, KclError> {
542 let plane_settings: Vec<(PlaneName, Uuid, Point3d, Point3d, Option<Color>)> = vec![
543 (
544 PlaneName::Xy,
545 id_generator.next_uuid(),
546 Point3d { x: 1.0, y: 0.0, z: 0.0 },
547 Point3d { x: 0.0, y: 1.0, z: 0.0 },
548 Some(Color {
549 r: 0.7,
550 g: 0.28,
551 b: 0.28,
552 a: 0.4,
553 }),
554 ),
555 (
556 PlaneName::Yz,
557 id_generator.next_uuid(),
558 Point3d { x: 0.0, y: 1.0, z: 0.0 },
559 Point3d { x: 0.0, y: 0.0, z: 1.0 },
560 Some(Color {
561 r: 0.28,
562 g: 0.7,
563 b: 0.28,
564 a: 0.4,
565 }),
566 ),
567 (
568 PlaneName::Xz,
569 id_generator.next_uuid(),
570 Point3d { x: 1.0, y: 0.0, z: 0.0 },
571 Point3d { x: 0.0, y: 0.0, z: 1.0 },
572 Some(Color {
573 r: 0.28,
574 g: 0.28,
575 b: 0.7,
576 a: 0.4,
577 }),
578 ),
579 (
580 PlaneName::NegXy,
581 id_generator.next_uuid(),
582 Point3d {
583 x: -1.0,
584 y: 0.0,
585 z: 0.0,
586 },
587 Point3d { x: 0.0, y: 1.0, z: 0.0 },
588 None,
589 ),
590 (
591 PlaneName::NegYz,
592 id_generator.next_uuid(),
593 Point3d {
594 x: 0.0,
595 y: -1.0,
596 z: 0.0,
597 },
598 Point3d { x: 0.0, y: 0.0, z: 1.0 },
599 None,
600 ),
601 (
602 PlaneName::NegXz,
603 id_generator.next_uuid(),
604 Point3d {
605 x: -1.0,
606 y: 0.0,
607 z: 0.0,
608 },
609 Point3d { x: 0.0, y: 0.0, z: 1.0 },
610 None,
611 ),
612 ];
613
614 let mut planes = HashMap::new();
615 for (name, plane_id, x_axis, y_axis, color) in plane_settings {
616 planes.insert(
617 name,
618 self.make_default_plane(plane_id, x_axis, y_axis, color, source_range)
619 .await?,
620 );
621 }
622
623 self.flush_batch(false, source_range).await?;
625
626 Ok(DefaultPlanes {
627 xy: planes[&PlaneName::Xy],
628 neg_xy: planes[&PlaneName::NegXy],
629 xz: planes[&PlaneName::Xz],
630 neg_xz: planes[&PlaneName::NegXz],
631 yz: planes[&PlaneName::Yz],
632 neg_yz: planes[&PlaneName::NegYz],
633 })
634 }
635
636 fn parse_websocket_response(
637 &self,
638 response: WebSocketResponse,
639 source_range: SourceRange,
640 ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
641 match response {
642 WebSocketResponse::Success(success) => Ok(success.resp),
643 WebSocketResponse::Failure(fail) => {
644 let _request_id = fail.request_id;
645 Err(KclError::Engine(KclErrorDetails {
646 message: format!("Modeling command failed: {:?}", fail.errors),
647 source_ranges: vec![source_range],
648 }))
649 }
650 }
651 }
652
653 fn parse_batch_responses(
654 &self,
655 id: uuid::Uuid,
657 id_to_source_range: HashMap<uuid::Uuid, SourceRange>,
659 responses: HashMap<uuid::Uuid, BatchResponse>,
661 ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
662 #[expect(
664 clippy::iter_over_hash_type,
665 reason = "modeling command uses a HashMap and keys are random, so we don't really have a choice"
666 )]
667 for (cmd_id, resp) in responses.iter() {
668 match resp {
669 BatchResponse::Success { response } => {
670 if cmd_id == &id {
671 return Ok(OkWebSocketResponseData::Modeling {
673 modeling_response: response.clone(),
674 });
675 } else {
676 continue;
678 }
679 }
680 BatchResponse::Failure { errors } => {
681 let source_range = id_to_source_range.get(cmd_id).cloned().ok_or_else(|| {
683 KclError::Engine(KclErrorDetails {
684 message: format!("Failed to get source range for command ID: {:?}", cmd_id),
685 source_ranges: vec![],
686 })
687 })?;
688 return Err(KclError::Engine(KclErrorDetails {
689 message: format!("Modeling command failed: {:?}", errors),
690 source_ranges: vec![source_range],
691 }));
692 }
693 }
694 }
695
696 Err(KclError::Engine(KclErrorDetails {
699 message: format!("Failed to find response for command ID: {:?}", id),
700 source_ranges: vec![],
701 }))
702 }
703
704 async fn modify_grid(&self, hidden: bool, source_range: SourceRange) -> Result<(), KclError> {
705 self.batch_modeling_cmd(
707 uuid::Uuid::new_v4(),
708 source_range,
709 &ModelingCmd::from(mcmd::ObjectVisible {
710 hidden,
711 object_id: *GRID_OBJECT_ID,
712 }),
713 )
714 .await?;
715
716 self.batch_modeling_cmd(
718 uuid::Uuid::new_v4(),
719 source_range,
720 &ModelingCmd::from(mcmd::ObjectVisible {
721 hidden,
722 object_id: *GRID_SCALE_TEXT_OBJECT_ID,
723 }),
724 )
725 .await?;
726
727 Ok(())
728 }
729
730 async fn get_session_data(&self) -> Option<ModelingSessionData> {
733 None
734 }
735
736 async fn close(&self);
738}
739
740#[derive(Debug, Hash, Eq, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
741#[ts(export)]
742#[serde(rename_all = "camelCase")]
743pub enum PlaneName {
744 Xy,
746 NegXy,
748 Xz,
750 NegXz,
752 Yz,
754 NegYz,
756}
757
758#[cfg(not(target_arch = "wasm32"))]
760pub fn new_zoo_client(token: Option<String>, engine_addr: Option<String>) -> anyhow::Result<kittycad::Client> {
761 let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"),);
762 let http_client = reqwest::Client::builder()
763 .user_agent(user_agent)
764 .timeout(std::time::Duration::from_secs(600))
766 .connect_timeout(std::time::Duration::from_secs(60));
767 let ws_client = reqwest::Client::builder()
768 .user_agent(user_agent)
769 .timeout(std::time::Duration::from_secs(600))
771 .connect_timeout(std::time::Duration::from_secs(60))
772 .connection_verbose(true)
773 .tcp_keepalive(std::time::Duration::from_secs(600))
774 .http1_only();
775
776 let zoo_token_env = std::env::var("ZOO_API_TOKEN");
777
778 let token = if let Some(token) = token {
779 token
780 } else if let Ok(token) = std::env::var("KITTYCAD_API_TOKEN") {
781 if let Ok(zoo_token) = zoo_token_env {
782 if zoo_token != token {
783 return Err(anyhow::anyhow!(
784 "Both environment variables KITTYCAD_API_TOKEN=`{}` and ZOO_API_TOKEN=`{}` are set. Use only one.",
785 token,
786 zoo_token
787 ));
788 }
789 }
790 token
791 } else if let Ok(token) = zoo_token_env {
792 token
793 } else {
794 return Err(anyhow::anyhow!(
795 "No API token found in environment variables. Use KITTYCAD_API_TOKEN or ZOO_API_TOKEN"
796 ));
797 };
798
799 let mut client = kittycad::Client::new_from_reqwest(token, http_client, ws_client);
801 let kittycad_host_env = std::env::var("KITTYCAD_HOST");
803 if let Some(addr) = engine_addr {
804 client.set_base_url(addr);
805 } else if let Ok(addr) = std::env::var("ZOO_HOST") {
806 if let Ok(kittycad_host) = kittycad_host_env {
807 if kittycad_host != addr {
808 return Err(anyhow::anyhow!(
809 "Both environment variables KITTYCAD_HOST=`{}` and ZOO_HOST=`{}` are set. Use only one.",
810 kittycad_host,
811 addr
812 ));
813 }
814 }
815 client.set_base_url(addr);
816 } else if let Ok(addr) = kittycad_host_env {
817 client.set_base_url(addr);
818 }
819
820 Ok(client)
821}