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