1use std::cell::RefCell;
16use std::collections::BTreeMap;
17
18use wasm_bindgen::prelude::*;
19
20use super::lifecycle::{self, LifecycleOutcome};
21use super::{constraints, mapping, AssemblyState, ConstraintEntry};
22use crate::feature_pipeline::{Env, SceneMap};
23
24struct Session {
25 state: AssemblyState,
26 scene: SceneMap,
27 expressions: String,
28 configurator: serde_json::Value,
29 pose_updates: BTreeMap<String, serde_json::Value>,
33 fixed_updates: BTreeMap<String, bool>,
35 report: serde_json::Value,
37}
38
39thread_local! {
40 static SESSION: RefCell<Option<Session>> = const { RefCell::new(None) };
41}
42
43pub(super) fn install_session(
47 state: AssemblyState,
48 scene: SceneMap,
49 expressions: String,
50 configurator: serde_json::Value,
51 outcome: LifecycleOutcome,
52) {
53 SESSION.with(|session| {
54 *session.borrow_mut() = Some(Session {
55 state,
56 scene,
57 expressions,
58 configurator,
59 pose_updates: outcome.pose_updates,
60 fixed_updates: outcome.fixed_updates,
61 report: outcome.report,
62 });
63 });
64}
65
66fn with_session<T>(f: impl FnOnce(&mut Session) -> Result<T, String>) -> Result<T, String> {
67 SESSION.with(|session| {
68 let mut session = session.borrow_mut();
69 let session = session
70 .as_mut()
71 .ok_or_else(|| "no assembly session (run a history first)".to_string())?;
72 f(session)
73 })
74}
75
76fn solve_session(session: &mut Session) -> serde_json::Value {
81 let env = Env::build(&session.expressions, &session.configurator)
82 .unwrap_or_else(Env::poisoned);
83 let outcome = lifecycle::run_constraints(&mut session.state, &mut session.scene, &env);
84 session.pose_updates.extend(outcome.pose_updates);
85 session.fixed_updates.extend(outcome.fixed_updates);
86 session.report = outcome.report.clone();
87 outcome.report
88}
89
90#[wasm_bindgen]
97pub fn assembly_state_json() -> String {
98 SESSION.with(|session| {
99 session
100 .borrow()
101 .as_ref()
102 .map(|session| serde_json::to_value(&session.state).unwrap_or_default())
103 .unwrap_or_else(|| serde_json::to_value(AssemblyState::default()).unwrap_or_default())
104 .to_string()
105 })
106}
107
108#[wasm_bindgen]
111pub fn assembly_statuses_json() -> String {
112 SESSION.with(|session| {
113 let rows: Vec<serde_json::Value> = session
114 .borrow()
115 .as_ref()
116 .map(|session| {
117 session
118 .state
119 .constraints
120 .iter()
121 .map(|entry| {
122 serde_json::json!({
123 "id": entry.id(),
124 "type": entry.constraint_type,
125 "enabled": entry.enabled,
126 "open": entry.open,
127 "status": entry.status(),
128 "message": entry
129 .persistent("message")
130 .and_then(|value| value.as_str())
131 .unwrap_or(""),
132 "satisfied": entry
133 .persistent("satisfied")
134 .and_then(|value| value.as_bool())
135 .unwrap_or(false),
136 "error": entry.persistent("error").cloned(),
137 })
138 })
139 .collect()
140 })
141 .unwrap_or_default();
142 serde_json::Value::Array(rows).to_string()
143 })
144}
145
146#[wasm_bindgen]
149pub fn assembly_dof_json() -> String {
150 SESSION.with(|session| {
151 session
152 .borrow()
153 .as_ref()
154 .map(|session| session.report.to_string())
155 .unwrap_or_else(|| serde_json::json!({ "ok": true, "mates": 0 }).to_string())
156 })
157}
158
159#[wasm_bindgen]
164pub fn assembly_pose_updates_json() -> String {
165 SESSION.with(|session| {
166 session
167 .borrow()
168 .as_ref()
169 .map(|session| {
170 serde_json::json!({
171 "poses": session.pose_updates,
172 "isFixed": session.fixed_updates,
173 })
174 .to_string()
175 })
176 .unwrap_or_else(|| serde_json::json!({ "poses": {}, "isFixed": {} }).to_string())
177 })
178}
179
180#[wasm_bindgen]
187pub fn assembly_apply_document_json(document_json: &str) -> Result<String, JsValue> {
188 assembly_apply_document_impl(document_json).map_err(|error| JsValue::from_str(&error))
189}
190
191pub fn assembly_apply_document_impl(document_json: &str) -> Result<String, String> {
192 let mut document: serde_json::Value = serde_json::from_str(document_json)
193 .map_err(|error| format!("assembly document fold: bad document: {error}"))?;
194 with_session(|session| {
195 let object = document
196 .as_object_mut()
197 .ok_or_else(|| "assembly document fold: document must be an object".to_string())?;
198 object.insert(
199 "assembly".into(),
200 serde_json::to_value(&session.state)
201 .map_err(|error| format!("assembly state serialize: {error}"))?,
202 );
203 if let Some(features) = object.get_mut("features").and_then(|v| v.as_array_mut()) {
204 for feature in features {
205 let Some(params) = feature
206 .get_mut("inputParams")
207 .and_then(|value| value.as_object_mut())
208 else {
209 continue;
210 };
211 let Some(id) = params.get("id").and_then(|value| value.as_str()) else {
212 continue;
213 };
214 let id = id.to_string();
215 if let Some(pose) = session.pose_updates.get(&id) {
216 fn has_expression(value: &serde_json::Value) -> bool {
219 match value {
220 serde_json::Value::String(_) => true,
221 serde_json::Value::Array(a) => a.iter().any(has_expression),
222 serde_json::Value::Object(o) => o.values().any(has_expression),
223 _ => false,
224 }
225 }
226 if !params.get("transform").is_some_and(has_expression) {
227 params.insert("transform".into(), pose.clone());
228 }
229 }
230 if let Some(&fixed) = session.fixed_updates.get(&id) {
231 params.insert("isFixed".into(), serde_json::Value::Bool(fixed));
232 }
233 }
234 }
235 Ok(())
236 })?;
237 Ok(document.to_string())
238}
239
240#[wasm_bindgen]
247pub fn assembly_overlay_json() -> String {
248 SESSION.with(|session| {
249 let session = session.borrow();
250 let Some(session) = session.as_ref() else {
251 return serde_json::Value::Array(Vec::new()).to_string();
252 };
253 let env = Env::build(&session.expressions, &session.configurator)
254 .unwrap_or_else(Env::poisoned);
255 let rows: Vec<serde_json::Value> = session
256 .state
257 .constraints
258 .iter()
259 .filter(|entry| entry.enabled)
260 .map(|entry| overlay_row(entry, &session.scene, &env))
261 .collect();
262 serde_json::Value::Array(rows).to_string()
263 })
264}
265
266fn overlay_row(entry: &ConstraintEntry, scene: &SceneMap, env: &Env) -> serde_json::Value {
267 let mut row = serde_json::json!({
268 "id": entry.id(),
269 "type": entry.constraint_type,
270 "status": entry.status(),
271 "message": entry
272 .persistent("message")
273 .and_then(|value| value.as_str())
274 .unwrap_or(""),
275 });
276 let elements = entry.elements();
277 let resolved: Vec<_> = elements
278 .iter()
279 .filter_map(|name| mapping::resolve_element(scene, name).ok())
280 .collect();
281 if resolved.len() != elements.len() || resolved.is_empty() {
282 return row;
283 }
284 let object = row.as_object_mut().expect("row is an object");
285 object.insert(
286 "anchors".into(),
287 serde_json::Value::Array(
288 resolved
289 .iter()
290 .map(|element| {
291 let p = element.world.representative_point();
292 serde_json::json!([p.x, p.y, p.z])
293 })
294 .collect(),
295 ),
296 );
297 object.insert(
298 "directions".into(),
299 serde_json::Value::Array(
300 resolved
301 .iter()
302 .map(|element| match mapping::direction_of(&element.world) {
303 Some(direction) => serde_json::json!([direction.x, direction.y, direction.z]),
304 None => serde_json::Value::Null,
305 })
306 .collect(),
307 ),
308 );
309 object.insert(
314 "geoms".into(),
315 serde_json::Value::Array(
316 resolved
317 .iter()
318 .map(|element| serde_json::json!(geometry_tag(&element.world)))
319 .collect(),
320 ),
321 );
322 if entry.constraint_type == "fixed" {
328 return row;
329 }
330 let mut scratch = entry.clone();
331 if let Ok(mapped) = mapping::map_constraint(&mut scratch, &resolved, env) {
332 if let Some((value, unit)) = mapped.measured {
333 object.insert("value".into(), serde_json::json!(value));
334 object.insert("unit".into(), serde_json::json!(unit));
335 }
336 if let Some(axis) = mapped.angle_axis {
337 object.insert("angleAxis".into(), serde_json::json!([axis.x, axis.y, axis.z]));
338 }
339 if let Some(target) = mapped.target {
340 object.insert("target".into(), serde_json::json!(target));
341 }
342 if !mapped.groups.is_empty() {
343 object.insert("groups".into(), serde_json::json!(mapped.groups));
344 }
345 }
346 row
347}
348
349fn geometry_tag(geometry: &crate::SelectionGeometry) -> &'static str {
352 match geometry {
353 crate::SelectionGeometry::Plane { .. } => "plane",
354 crate::SelectionGeometry::Line { .. } => "line",
355 crate::SelectionGeometry::Axis { .. } => "axis",
356 crate::SelectionGeometry::Circle { .. } => "circle",
357 crate::SelectionGeometry::Sphere { .. } => "sphere",
358 crate::SelectionGeometry::Point { .. } => "point",
359 }
360}
361
362#[wasm_bindgen]
370pub fn assembly_add_constraint_json(
371 constraint_type: &str,
372 params_json: &str,
373) -> Result<String, JsValue> {
374 assembly_add_constraint_impl(constraint_type, params_json).map_err(|error| JsValue::from_str(&error))
375}
376
377pub fn assembly_add_constraint_impl(constraint_type: &str, params_json: &str) -> Result<String, String> {
378 let def = constraints::constraint_type(constraint_type)
379 .ok_or_else(|| format!("Unknown constraint type: {constraint_type}"))?;
380 let mut params: serde_json::Value = serde_json::from_str(params_json)
381 .map_err(|error| format!("constraint params: {error}"))?;
382 if !params.is_object() {
383 return Err("constraint params must be a JSON object".to_string());
384 }
385 with_session(|session| {
386 let id = match params.get("id").and_then(|value| value.as_str()) {
387 Some(id) if !id.is_empty() => id.to_string(),
388 _ => {
389 session.state.id_counter += 1;
390 let id = format!("{}{}", def.short_name, session.state.id_counter);
391 params
392 .as_object_mut()
393 .expect("checked object above")
394 .insert("id".into(), serde_json::Value::String(id.clone()));
395 id
396 }
397 };
398 session.state.constraints.push(ConstraintEntry {
399 constraint_type: constraint_type.to_string(),
400 input_params: params.clone(),
401 persistent_data: serde_json::Value::Object(serde_json::Map::new()),
402 enabled: true,
403 open: true,
404 });
405 let report = solve_session(session);
406 Ok(serde_json::json!({ "id": id, "report": report }).to_string())
407 })
408}
409
410#[wasm_bindgen]
413pub fn assembly_update_constraint_json(id: &str, params_json: &str) -> Result<String, JsValue> {
414 assembly_update_constraint_impl(id, params_json).map_err(|error| JsValue::from_str(&error))
415}
416
417pub fn assembly_update_constraint_impl(id: &str, params_json: &str) -> Result<String, String> {
418 let mut params: serde_json::Value = serde_json::from_str(params_json)
419 .map_err(|error| format!("constraint params: {error}"))?;
420 if !params.is_object() {
421 return Err("constraint params must be a JSON object".to_string());
422 }
423 if params.get("id").and_then(|value| value.as_str()).is_none() {
424 params
425 .as_object_mut()
426 .expect("checked object above")
427 .insert("id".into(), serde_json::Value::String(id.to_string()));
428 }
429 with_session(|session| {
430 let entry = find_entry(session, id)?;
431 entry.input_params = params.clone();
432 Ok(solve_session(session).to_string())
433 })
434}
435
436#[wasm_bindgen]
438pub fn assembly_remove_constraint_json(id: &str) -> Result<String, JsValue> {
439 assembly_remove_constraint_impl(id).map_err(|error| JsValue::from_str(&error))
440}
441
442pub fn assembly_remove_constraint_impl(id: &str) -> Result<String, String> {
443 with_session(|session| {
444 let index = find_index(session, id)?;
445 session.state.constraints.remove(index);
446 Ok(solve_session(session).to_string())
447 })
448}
449
450#[wasm_bindgen]
452pub fn assembly_set_constraint_enabled_json(id: &str, enabled: bool) -> Result<String, JsValue> {
453 assembly_set_constraint_enabled_impl(id, enabled).map_err(|error| JsValue::from_str(&error))
454}
455
456pub fn assembly_set_constraint_enabled_impl(id: &str, enabled: bool) -> Result<String, String> {
457 with_session(|session| {
458 find_entry(session, id)?.enabled = enabled;
459 Ok(solve_session(session).to_string())
460 })
461}
462
463#[wasm_bindgen]
465pub fn assembly_set_constraint_open_json(id: &str, open: bool) -> Result<(), JsValue> {
466 assembly_set_constraint_open_impl(id, open).map_err(|error| JsValue::from_str(&error))
467}
468
469pub fn assembly_set_constraint_open_impl(id: &str, open: bool) -> Result<(), String> {
470 with_session(|session| {
471 find_entry(session, id)?.open = open;
472 Ok(())
473 })
474}
475
476#[wasm_bindgen]
478pub fn assembly_move_constraint_json(id: &str, index: usize) -> Result<String, JsValue> {
479 assembly_move_constraint_impl(id, index).map_err(|error| JsValue::from_str(&error))
480}
481
482pub fn assembly_move_constraint_impl(id: &str, index: usize) -> Result<String, String> {
483 with_session(|session| {
484 let from = find_index(session, id)?;
485 let entry = session.state.constraints.remove(from);
486 let to = index.min(session.state.constraints.len());
487 session.state.constraints.insert(to, entry);
488 Ok(solve_session(session).to_string())
489 })
490}
491
492#[wasm_bindgen]
500pub fn assembly_inferable_types_json() -> String {
501 super::infer::inferable_types_json().to_string()
502}
503
504#[wasm_bindgen]
509pub fn assembly_infer_constraints_json(options_json: &str) -> String {
510 match infer_scan(options_json) {
511 Ok(report) => report.to_string(),
512 Err(error) => refusal(&error),
513 }
514}
515
516#[wasm_bindgen]
521pub fn assembly_apply_inferred_constraints_json(options_json: &str) -> String {
522 match infer_apply(options_json) {
523 Ok(report) => report.to_string(),
524 Err(error) => refusal(&error),
525 }
526}
527
528fn refusal(error: &str) -> String {
529 serde_json::json!({ "ok": false, "error": error }).to_string()
530}
531
532fn infer_scan(options_json: &str) -> Result<serde_json::Value, String> {
533 let options = super::infer::InferOptions::parse(options_json)?;
534 with_session(|session| Ok(super::infer::scan(&session.state, &session.scene, &options).report()))
535}
536
537fn infer_apply(options_json: &str) -> Result<serde_json::Value, String> {
538 let options = super::infer::InferOptions::parse(options_json)?;
539 with_session(|session| {
540 let scan = super::infer::scan(&session.state, &session.scene, &options);
541 let report = scan.report();
542 let created = super::infer::apply(&mut session.state, &scan.candidates);
543 let solve = if created.is_empty() {
544 serde_json::Value::Null
545 } else {
546 solve_session(session)
547 };
548 Ok(serde_json::json!({
549 "ok": true,
550 "created": created,
551 "report": report,
552 "solve": solve,
553 }))
554 })
555}
556
557#[wasm_bindgen]
559pub fn assembly_run_solve_json() -> Result<String, JsValue> {
560 assembly_run_solve_impl().map_err(|error| JsValue::from_str(&error))
561}
562
563pub fn assembly_run_solve_impl() -> Result<String, String> {
564 with_session(|session| Ok(solve_session(session).to_string()))
565}
566
567fn find_index(session: &Session, id: &str) -> Result<usize, String> {
568 session
569 .state
570 .constraints
571 .iter()
572 .position(|entry| entry.id() == id)
573 .ok_or_else(|| format!("unknown constraint '{id}'"))
574}
575
576fn find_entry<'a>(session: &'a mut Session, id: &str) -> Result<&'a mut ConstraintEntry, String> {
577 let index = find_index(session, id)?;
578 Ok(&mut session.state.constraints[index])
579}