1pub 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#[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 #[display("XY")]
186 Xy,
187 #[display("-XY")]
189 NegXy,
190 #[display("XZ")]
192 Xz,
193 #[display("-XZ")]
195 NegXz,
196 #[display("YZ")]
198 Yz,
199 #[display("-YZ")]
201 NegYz,
202}
203
204#[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 .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 .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 let mut client = kittycad::Client::new_from_reqwest(token, http_client, ws_client);
247 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}