Skip to main content

kcl_lib/engine/
mod.rs

1//! Functions for managing engine communications.
2
3pub mod async_tasks;
4#[cfg(target_arch = "wasm32")]
5#[cfg(feature = "engine")]
6pub mod conn_wasm;
7#[cfg(feature = "engine")]
8pub mod engine_manager;
9
10use std::sync::Arc;
11use std::sync::atomic::AtomicUsize;
12use std::sync::atomic::Ordering;
13
14pub use async_tasks::AsyncTasks;
15use indexmap::IndexMap;
16pub use kcl_api::PlaneName;
17use kcl_api::UnitLength;
18use kcmc::ModelingCmd;
19use kcmc::each_cmd as mcmd;
20use kcmc::websocket::WebSocketRequest;
21use kittycad_modeling_cmds::{self as kcmc};
22use tokio::sync::RwLock;
23use uuid::Uuid;
24
25use crate::SourceRange;
26use crate::execution::PlaneInfo;
27use crate::execution::Point3d;
28use crate::unit_conversion::ToKcmc;
29
30lazy_static::lazy_static! {
31    pub static ref GRID_OBJECT_ID: uuid::Uuid = uuid::Uuid::parse_str("cfa78409-653d-4c26-96f1-7c45fb784840").unwrap();
32
33    pub static ref GRID_SCALE_TEXT_OBJECT_ID: uuid::Uuid = uuid::Uuid::parse_str("10782f33-f588-4668-8bcd-040502d26590").unwrap();
34
35    pub static ref DEFAULT_PLANE_INFO: IndexMap<PlaneName, PlaneInfo> = IndexMap::from([
36            (
37                PlaneName::Xy,
38                PlaneInfo {
39                    origin: Point3d::new(0.0, 0.0, 0.0, Some(UnitLength::Millimeters)),
40                    x_axis: Point3d::new(1.0, 0.0, 0.0, None),
41                    y_axis: Point3d::new(0.0, 1.0, 0.0, None),
42                    z_axis: Point3d::new(0.0, 0.0, 1.0, None),
43                },
44            ),
45            (
46                PlaneName::NegXy,
47                PlaneInfo {
48                    origin: Point3d::new( 0.0, 0.0,  0.0, Some(UnitLength::Millimeters)),
49                    x_axis: Point3d::new(-1.0, 0.0,  0.0, None),
50                    y_axis: Point3d::new( 0.0, 1.0,  0.0, None),
51                    z_axis: Point3d::new( 0.0, 0.0, -1.0, None),
52                },
53            ),
54            (
55                PlaneName::Xz,
56                PlaneInfo {
57                    origin: Point3d::new(0.0,  0.0, 0.0, Some(UnitLength::Millimeters)),
58                    x_axis: Point3d::new(1.0,  0.0, 0.0, None),
59                    y_axis: Point3d::new(0.0,  0.0, 1.0, None),
60                    z_axis: Point3d::new(0.0, -1.0, 0.0, None),
61                },
62            ),
63            (
64                PlaneName::NegXz,
65                PlaneInfo {
66                    origin: Point3d::new( 0.0, 0.0, 0.0, Some(UnitLength::Millimeters)),
67                    x_axis: Point3d::new(-1.0, 0.0, 0.0, None),
68                    y_axis: Point3d::new( 0.0, 0.0, 1.0, None),
69                    z_axis: Point3d::new( 0.0, 1.0, 0.0, None),
70                },
71            ),
72            (
73                PlaneName::Yz,
74                PlaneInfo {
75                    origin: Point3d::new(0.0, 0.0, 0.0, Some(UnitLength::Millimeters)),
76                    x_axis: Point3d::new(0.0, 1.0, 0.0, None),
77                    y_axis: Point3d::new(0.0, 0.0, 1.0, None),
78                    z_axis: Point3d::new(1.0, 0.0, 0.0, None),
79                },
80            ),
81            (
82                PlaneName::NegYz,
83                PlaneInfo {
84                    origin: Point3d::new( 0.0,  0.0, 0.0, Some(UnitLength::Millimeters)),
85                    x_axis: Point3d::new( 0.0, -1.0, 0.0, None),
86                    y_axis: Point3d::new( 0.0,  0.0, 1.0, None),
87                    z_axis: Point3d::new(-1.0,  0.0, 0.0, None),
88                },
89            ),
90    ]);
91}
92
93/// Per-execution buffer for modeling commands that must preserve temporal order.
94///
95/// A single execution can enqueue commands whose source ranges come from multiple
96/// modules. The ownership boundary is the execution task carrying this context,
97/// not the module id embedded in a source range.
98#[derive(Debug, Clone)]
99pub struct EngineBatchContext {
100    batch: Arc<RwLock<Vec<(WebSocketRequest, SourceRange)>>>,
101    batch_end: Arc<RwLock<IndexMap<Uuid, (WebSocketRequest, SourceRange)>>>,
102}
103
104impl Default for EngineBatchContext {
105    fn default() -> Self {
106        Self::new()
107    }
108}
109
110impl EngineBatchContext {
111    pub fn new() -> Self {
112        Self {
113            batch: Arc::new(RwLock::new(Vec::new())),
114            batch_end: Arc::new(RwLock::new(IndexMap::new())),
115        }
116    }
117
118    pub async fn is_empty(&self) -> bool {
119        self.batch.read().await.is_empty() && self.batch_end.read().await.is_empty()
120    }
121
122    async fn clear(&self) {
123        self.batch.write().await.clear();
124        self.batch_end.write().await.clear();
125    }
126
127    async fn push(&self, req: WebSocketRequest, source_range: SourceRange) {
128        self.batch.write().await.push((req, source_range));
129    }
130
131    async fn extend(&self, requests: Vec<(WebSocketRequest, SourceRange)>) {
132        self.batch.write().await.extend(requests);
133    }
134
135    async fn insert_end(&self, id: Uuid, req: WebSocketRequest, source_range: SourceRange) {
136        self.batch_end.write().await.insert(id, (req, source_range));
137    }
138
139    pub(crate) async fn move_batch_end_to_batch(&self, ids: Vec<Uuid>) {
140        let mut moved = Vec::new();
141        {
142            let mut batch_end = self.batch_end.write().await;
143            for id in ids {
144                let Some(item) = batch_end.shift_remove(&id) else {
145                    continue;
146                };
147                moved.push(item);
148            }
149        }
150
151        self.extend(moved).await;
152    }
153
154    async fn take_batch(&self) -> Vec<(WebSocketRequest, SourceRange)> {
155        std::mem::take(&mut *self.batch.write().await)
156    }
157
158    async fn take_batch_end(&self) -> IndexMap<Uuid, (WebSocketRequest, SourceRange)> {
159        std::mem::take(&mut *self.batch_end.write().await)
160    }
161}
162
163#[derive(Default, Debug)]
164pub struct EngineStats {
165    pub commands_batched: AtomicUsize,
166    pub batches_sent: AtomicUsize,
167}
168
169impl Clone for EngineStats {
170    fn clone(&self) -> Self {
171        Self {
172            commands_batched: AtomicUsize::new(self.commands_batched.load(Ordering::Relaxed)),
173            batches_sent: AtomicUsize::new(self.batches_sent.load(Ordering::Relaxed)),
174        }
175    }
176}
177
178/// Create a new zoo api client.
179#[cfg(not(target_arch = "wasm32"))]
180pub fn new_zoo_client(token: Option<String>, engine_addr: Option<String>) -> anyhow::Result<kittycad::Client> {
181    let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"),);
182    let http_client = reqwest::Client::builder()
183        .user_agent(user_agent)
184        // For file conversions we need this to be long.
185        .timeout(std::time::Duration::from_secs(600))
186        .connect_timeout(std::time::Duration::from_secs(60));
187    let ws_client = reqwest::Client::builder()
188        .user_agent(user_agent)
189        // For file conversions we need this to be long.
190        .timeout(std::time::Duration::from_secs(600))
191        .connect_timeout(std::time::Duration::from_secs(60))
192        .connection_verbose(true)
193        .tcp_keepalive(std::time::Duration::from_secs(600))
194        .http1_only();
195
196    let zoo_token_env = std::env::var("ZOO_API_TOKEN");
197
198    let token = if let Some(token) = token {
199        token
200    } else if let Ok(token) = std::env::var("KITTYCAD_API_TOKEN") {
201        if let Ok(zoo_token) = zoo_token_env
202            && zoo_token != token
203        {
204            return Err(anyhow::anyhow!(
205                "Both environment variables KITTYCAD_API_TOKEN=`{}` and ZOO_API_TOKEN=`{}` are set. Use only one.",
206                token,
207                zoo_token
208            ));
209        }
210        token
211    } else if let Ok(token) = zoo_token_env {
212        token
213    } else {
214        return Err(anyhow::anyhow!(
215            "No API token found in environment variables. Use ZOO_API_TOKEN"
216        ));
217    };
218
219    // Create the client.
220    let mut client = kittycad::Client::new_from_reqwest(token, http_client, ws_client);
221    // Set an engine address if it's set.
222    let kittycad_host_env = std::env::var("KITTYCAD_HOST");
223    if let Some(addr) = engine_addr {
224        client.set_base_url(addr);
225    } else if let Ok(addr) = std::env::var("ZOO_HOST") {
226        if let Ok(kittycad_host) = kittycad_host_env
227            && kittycad_host != addr
228        {
229            return Err(anyhow::anyhow!(
230                "Both environment variables KITTYCAD_HOST=`{}` and ZOO_HOST=`{}` are set. Use only one.",
231                kittycad_host,
232                addr
233            ));
234        }
235        client.set_base_url(addr);
236    } else if let Ok(addr) = kittycad_host_env {
237        client.set_base_url(addr);
238    }
239
240    Ok(client)
241}
242
243#[derive(Copy, Clone, Debug)]
244pub enum GridScaleBehavior {
245    ScaleWithZoom,
246    Fixed(Option<UnitLength>),
247}
248
249impl GridScaleBehavior {
250    fn into_modeling_cmd(self) -> ModelingCmd {
251        const NUMBER_OF_GRID_COLUMNS: f32 = 10.0;
252        match self {
253            GridScaleBehavior::ScaleWithZoom => ModelingCmd::from(mcmd::SetGridAutoScale::builder().build()),
254            GridScaleBehavior::Fixed(unit_length) => ModelingCmd::from(
255                mcmd::SetGridScale::builder()
256                    .value(NUMBER_OF_GRID_COLUMNS)
257                    .units(unit_length.unwrap_or(UnitLength::Millimeters).to_kcmc())
258                    .build(),
259            ),
260        }
261    }
262}