brep_app/panels/
plm_launch.rs1use crate::document::{Document, Documents, EMPTY_DOCUMENT};
26use crate::panels::file::FileDialog;
27use crate::plm::PlmFuture;
28use crate::store::{ModelStore, Residency};
29
30pub fn open_key(search: &str) -> Option<String> {
34 let query = search.strip_prefix('?').unwrap_or(search);
35 let raw = query.split('&').find_map(|pair| pair.strip_prefix("open="))?;
36 let key = decode(raw);
37 let parts: Vec<&str> = key.split('/').collect();
38 match parts.as_slice() {
39 ["part", part, "rev", revision] if !part.is_empty() && !revision.is_empty() => Some(key.clone()),
40 _ => None,
41 }
42}
43
44fn decode(text: &str) -> String {
45 let bytes = text.as_bytes();
46 let mut out = Vec::with_capacity(bytes.len());
47 let mut i = 0;
48 while i < bytes.len() {
49 let hex = bytes
50 .get(i + 1..i + 3)
51 .and_then(|h| std::str::from_utf8(h).ok())
52 .and_then(|h| u8::from_str_radix(h, 16).ok());
53 match (bytes[i], hex) {
54 (b'%', Some(byte)) => {
55 out.push(byte);
56 i += 3;
57 }
58 (b'+', _) => {
59 out.push(b' ');
60 i += 1;
61 }
62 (byte, _) => {
63 out.push(byte);
64 i += 1;
65 }
66 }
67 }
68 String::from_utf8_lossy(&out).into_owned()
69}
70
71#[derive(Debug, PartialEq)]
73pub enum Step {
74 Pending,
76 Idle,
78 Opened(String),
80 OpenedEmpty(String),
82 SignIn(String),
85 Refused(String),
87}
88
89impl Step {
90 pub fn settles(&self) -> bool {
92 matches!(self, Step::Opened(_) | Step::OpenedEmpty(_) | Step::Refused(_))
93 }
94}
95
96#[derive(Debug, Default, serde::Deserialize)]
98#[serde(default)]
99struct PartCheck {
100 number: String,
101 document_class: String,
102 revisions: Vec<RevisionCheck>,
103}
104
105#[derive(Debug, Default, serde::Deserialize)]
106#[serde(default)]
107struct RevisionCheck {
108 id: String,
109 label: String,
110}
111
112enum State {
113 Waiting,
114 Checking(PlmFuture<PartCheck>),
115 Done,
116}
117
118pub struct Launch {
120 pub key: String,
121 state: State,
122}
123
124impl Launch {
125 pub fn new(key: impl Into<String>) -> Self {
126 Launch { key: key.into(), state: State::Waiting }
127 }
128
129 #[cfg(target_arch = "wasm32")]
131 pub fn from_page() -> Option<Self> {
132 let search = web_sys::window()?.location().search().ok()?;
133 open_key(&search).map(Launch::new)
134 }
135
136 pub fn is_done(&self) -> bool {
137 matches!(self.state, State::Done)
138 }
139
140 pub fn step(&mut self, store: &dyn ModelStore, docs: &mut Documents, file: &mut FileDialog) -> Step {
143 if self.is_done() {
144 return Step::Idle;
145 }
146 let Some(client) = store.plm_client() else {
147 self.state = State::Done;
148 return match store.plm_session() {
149 Some(_) => Step::SignIn(format!(
150 "Sign in to the PLM to open {} — the page reloads onto it after you sign in.",
151 self.key
152 )),
153 None => Step::Refused(format!(
154 "{} can be opened only in the CAD app the PLM hosts (its Open in CAD button)",
155 self.key
156 )),
157 };
158 };
159 let name = store.canonical_identity(&self.key);
160 match store.residency(&name) {
161 Residency::Resident => {
162 self.state = State::Done;
163 file.open_document(docs, store, &name);
164 return if docs.active().name() == Some(name.as_str()) {
167 Step::Opened(name)
168 } else {
169 Step::Refused(format!("{} could not be opened: its document did not load", self.key))
170 };
171 }
172 Residency::OnDisk => {
173 let _ = store.read(&name);
175 return Step::Pending;
176 }
177 Residency::Absent => {}
178 }
179 if let State::Waiting = self.state {
182 let (part, _) = split(&self.key);
183 let path = crate::plm::identity::part_path(&part);
184 let client = client.clone();
185 self.state = State::Checking(Box::pin(async move {
186 let response = client.call("GET", &path, None).await.map_err(|e| e.to_string())?;
187 serde_json::from_slice(&response.body).map_err(|e| format!("the PLM's answer was not understood: {e}"))
188 }));
189 }
190 let State::Checking(future) = &mut self.state else { return Step::Pending };
191 let mut cx = std::task::Context::from_waker(std::task::Waker::noop());
192 let std::task::Poll::Ready(answer) = future.as_mut().poll(&mut cx) else { return Step::Pending };
193 self.state = State::Done;
194 let (_, revision) = split(&self.key);
195 match answer {
196 Err(sentence) => Step::Refused(format!("{} could not be opened: {sentence}", self.key)),
197 Ok(part) => match part.revisions.iter().find(|r| r.id == revision) {
198 None => Step::Refused(format!("{} could not be opened: part {} has no revision {revision}", self.key, part.number)),
199 Some(found) => {
200 let mut engine = docs.spawn_engine();
201 if let Err(e) = engine.set_history_json(EMPTY_DOCUMENT) {
202 return Step::Refused(format!("{} could not be opened: {e}", self.key));
203 }
204 let class = match part.document_class.as_str() {
205 "family" => crate::document_class::DocumentClass::Family,
206 "template" => crate::document_class::DocumentClass::Template,
207 _ => crate::document_class::DocumentClass::Normal,
208 };
209 crate::document_class::set_document_class(&mut engine, class);
210 let mut doc = Document::new(engine);
211 doc.set_name(Some(name.clone()));
212 docs.open_document(doc);
213 let _ = found;
214 Step::OpenedEmpty(name)
215 }
216 },
217 }
218 }
219}
220
221fn split(key: &str) -> (String, String) {
222 crate::plm::family::key_ids(key).unwrap_or_default()
223}
224
225pub fn forget_query() {
227 #[cfg(target_arch = "wasm32")]
228 {
229 use wasm_bindgen::{JsCast, JsValue};
230 let Some(window) = web_sys::window() else { return };
231 let location = window.location();
232 let (Ok(path), Ok(search), Ok(hash)) = (location.pathname(), location.search(), location.hash()) else { return };
233 let kept: Vec<&str> = search
234 .trim_start_matches('?')
235 .split('&')
236 .filter(|pair| !pair.is_empty() && !pair.starts_with("open="))
237 .collect();
238 let query = if kept.is_empty() { String::new() } else { format!("?{}", kept.join("&")) };
239 let url = JsValue::from_str(&format!("{path}{query}{hash}"));
243 let Ok(history) = js_sys::Reflect::get(&window, &JsValue::from_str("history")) else { return };
244 let Ok(replace) = js_sys::Reflect::get(&history, &JsValue::from_str("replaceState")) else { return };
245 if let Ok(replace) = replace.dyn_into::<js_sys::Function>() {
246 let _ = replace.call3(&history, &JsValue::NULL, &JsValue::from_str(""), &url);
247 }
248 }
249}
250