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