1use super::*;
2use serde_json::Value;
3
4pub enum ComponentInsert<'a> {
42 Existing {
43 part_name: &'a str,
44 },
45 New {
46 name: &'a str,
47 source_key: &'a str,
48 source_signature: &'a str,
49 document_json: &'a str,
50 },
51}
52
53pub fn document_signature(doc_json: &str) -> String {
65 use std::hash::{Hash, Hasher};
66 fn walk(value: &serde_json::Value, hasher: &mut impl Hasher) {
67 match value {
68 serde_json::Value::Null => 0u8.hash(hasher),
69 serde_json::Value::Bool(flag) => {
70 1u8.hash(hasher);
71 flag.hash(hasher);
72 }
73 serde_json::Value::Number(number) => {
74 2u8.hash(hasher);
75 number.to_string().hash(hasher);
76 }
77 serde_json::Value::String(text) => {
78 3u8.hash(hasher);
79 text.hash(hasher);
80 }
81 serde_json::Value::Array(items) => {
82 4u8.hash(hasher);
83 for item in items {
84 walk(item, hasher);
85 }
86 }
87 serde_json::Value::Object(map) => {
88 5u8.hash(hasher);
89 let mut keys: Vec<&String> = map.keys().collect();
90 keys.sort();
91 for key in keys {
92 key.hash(hasher);
93 walk(&map[key], hasher);
94 }
95 }
96 }
97 }
98 let mut hasher = std::collections::hash_map::DefaultHasher::new();
99 match serde_json::from_str::<serde_json::Value>(doc_json) {
100 Ok(value) => walk(&value, &mut hasher),
101 Err(_) => doc_json.hash(&mut hasher),
102 }
103 format!("{:016x}", hasher.finish())
104}
105
106fn js_err<E: std::fmt::Debug>(error: E) -> String {
112 format!("{error:?}")
113}
114
115fn rigid_inverse_point(transform: &brep_kernel::AffineTransform, world: [f64; 3]) -> [f64; 3] {
118 let m = &transform.elements;
119 let d = [world[0] - m[3], world[1] - m[7], world[2] - m[11]];
120 [
121 m[0] * d[0] + m[4] * d[1] + m[8] * d[2],
122 m[1] * d[0] + m[5] * d[1] + m[9] * d[2],
123 m[2] * d[0] + m[6] * d[1] + m[10] * d[2],
124 ]
125}
126
127impl EngineState {
128 pub fn history_has_assembly(&self) -> bool {
134 let has_acomp = (0..self.history.len()).any(|index| {
135 matches!(
136 self.history.feature_type(index).as_deref(),
137 Some("ACOMP") | Some("ASSEMBLY COMPONENT")
138 )
139 });
140 has_acomp
141 || self
142 .history
143 .assembly_block()
144 .map(|block| !block.is_null())
145 .unwrap_or(false)
146 }
147
148 pub fn assembly_components(&self) -> &[brep_kernel::ComponentRecord] {
152 &self.assembly_components
153 }
154
155 pub fn ensure_assembly_synced(&mut self) {
160 if self.assembly_synced_generation == Some(self.applied_generation) {
161 return;
162 }
163 self.sync_assembly();
164 }
165
166 pub fn assembly_statuses_value(&mut self) -> Value {
169 self.ensure_assembly_synced();
170 serde_json::from_str(&brep_kernel::assembly_statuses_json())
171 .unwrap_or_else(|_| Value::Array(Vec::new()))
172 }
173
174 pub fn assembly_state_value(&mut self) -> Value {
176 self.ensure_assembly_synced();
177 serde_json::from_str(&brep_kernel::assembly_state_json()).unwrap_or(Value::Null)
178 }
179
180 pub fn assembly_dof_value(&mut self) -> Value {
183 self.ensure_assembly_synced();
184 serde_json::from_str(&brep_kernel::assembly_dof_json()).unwrap_or(Value::Null)
185 }
186
187 pub fn assembly_overlay_value(&mut self) -> Value {
191 self.ensure_assembly_synced();
192 serde_json::from_str(&brep_kernel::assembly_overlay_json())
193 .unwrap_or_else(|_| Value::Array(Vec::new()))
194 }
195
196 pub fn parts_library_names(&mut self) -> Vec<String> {
200 if self.history_has_assembly() {
201 self.ensure_assembly_synced();
202 }
203 serde_json::from_str::<Value>(&brep_kernel::parts_library_json())
204 .ok()
205 .and_then(|value| {
206 value
207 .as_object()
208 .map(|map| map.keys().cloned().collect::<Vec<_>>())
209 })
210 .unwrap_or_default()
211 }
212
213 pub(crate) fn sync_assembly(&mut self) {
223 if !self.history_has_assembly() {
224 self.assembly_components.clear();
225 self.history
230 .set_parts_library(Value::Object(serde_json::Map::new()));
231 self.assembly_synced_generation = Some(self.applied_generation);
232 return;
233 }
234 let request: brep_kernel::HistoryRequest =
235 match serde_json::from_value(self.history.prefix_request()) {
236 Ok(request) => request,
237 Err(_) => return, };
239 let result = brep_kernel::execute_history(&request);
240
241 let mut components: std::collections::BTreeMap<String, brep_kernel::ComponentRecord> =
245 std::collections::BTreeMap::new();
246 for feature in &result.results {
247 for removed in &feature.removed {
248 components.remove(removed);
249 }
250 for record in &feature.components {
251 components.insert(record.id.clone(), record.clone());
252 }
253 }
254 self.assembly_components = components.into_values().collect();
255
256 self.apply_assembly_fold(false);
259
260 let revision = brep_kernel::parts_library_revision();
269 if self.parts_library_block_revision != Some(revision)
270 || !self.history.parts_library_mirrors_store()
271 {
272 if let Ok(library) = serde_json::from_str::<Value>(&brep_kernel::parts_library_json()) {
273 self.history.set_parts_library(library);
274 self.parts_library_block_revision = Some(revision);
275 }
276 }
277 self.assembly_synced_generation = Some(self.applied_generation);
278 }
279
280 fn apply_assembly_fold(&mut self, checkpoint: bool) {
286 let document = self.history.request_json_without_parts_library();
293 match brep_kernel::assembly_apply_document_json(&document) {
294 Ok(folded) => {
295 let adopted = if checkpoint {
296 self.history.adopt_document_checkpointed(&folded)
297 } else {
298 self.history.adopt_document(&folded)
299 };
300 if let Err(error) = adopted {
301 self.push_notice(format!("assembly fold failed: {error}"));
302 }
303 }
304 Err(_) => {} }
306 }
307
308 fn after_constraint_mutation(&mut self) {
313 self.apply_assembly_fold(true);
314 if self.settings.assembly_auto_solve {
315 self.rerun_history();
316 } else {
317 self.dirty = true;
318 }
319 }
320
321 pub fn assembly_add_constraint(
326 &mut self,
327 constraint_type: &str,
328 params_json: &str,
329 ) -> Result<String, String> {
330 self.ensure_assembly_synced();
331 let reply =
332 brep_kernel::assembly_add_constraint_json(constraint_type, params_json).map_err(js_err)?;
333 self.after_constraint_mutation();
334 let id = serde_json::from_str::<Value>(&reply)
335 .ok()
336 .and_then(|value| value.get("id").and_then(|id| id.as_str()).map(String::from))
337 .unwrap_or_default();
338 Ok(id)
339 }
340
341 pub fn assembly_update_constraint(&mut self, id: &str, params_json: &str) -> Result<(), String> {
343 self.ensure_assembly_synced();
344 brep_kernel::assembly_update_constraint_json(id, params_json).map_err(js_err)?;
345 self.after_constraint_mutation();
346 Ok(())
347 }
348
349 pub(crate) fn assembly_update_constraint_no_rerun(
353 &mut self,
354 id: &str,
355 params_json: &str,
356 ) -> Result<(), String> {
357 self.ensure_assembly_synced();
358 brep_kernel::assembly_update_constraint_json(id, params_json).map_err(js_err)?;
359 self.apply_assembly_fold(true);
360 Ok(())
361 }
362
363 pub fn assembly_remove_constraint(&mut self, id: &str) -> Result<(), String> {
365 self.ensure_assembly_synced();
366 brep_kernel::assembly_remove_constraint_json(id).map_err(js_err)?;
367 self.after_constraint_mutation();
368 Ok(())
369 }
370
371 pub fn assembly_set_constraint_enabled(&mut self, id: &str, enabled: bool) -> Result<(), String> {
373 self.ensure_assembly_synced();
374 brep_kernel::assembly_set_constraint_enabled_json(id, enabled).map_err(js_err)?;
375 self.after_constraint_mutation();
376 Ok(())
377 }
378
379 pub fn assembly_set_constraint_open(&mut self, id: &str, open: bool) -> Result<(), String> {
388 self.ensure_assembly_synced();
389 if open {
390 let others: Vec<String> =
391 serde_json::from_str::<Value>(&brep_kernel::assembly_state_json())
392 .ok()
393 .and_then(|state| {
394 state.get("constraints").and_then(|l| l.as_array()).map(|entries| {
395 entries
396 .iter()
397 .filter_map(|entry| {
398 let cid =
399 entry.get("inputParams")?.get("id")?.as_str()?;
400 let is_open =
401 entry.get("open").and_then(|v| v.as_bool()).unwrap_or(false);
402 (is_open && cid != id).then(|| cid.to_string())
403 })
404 .collect()
405 })
406 })
407 .unwrap_or_default();
408 for other in others {
409 brep_kernel::assembly_set_constraint_open_json(&other, false).map_err(js_err)?;
410 }
411 }
412 brep_kernel::assembly_set_constraint_open_json(id, open).map_err(js_err)?;
413 self.apply_assembly_fold(false);
414 Ok(())
415 }
416
417 pub fn assembly_move_constraint(&mut self, id: &str, index: usize) -> Result<(), String> {
419 self.ensure_assembly_synced();
420 brep_kernel::assembly_move_constraint_json(id, index).map_err(js_err)?;
421 self.after_constraint_mutation();
422 Ok(())
423 }
424
425 pub fn assembly_run_solve(&mut self) -> Result<(), String> {
429 self.ensure_assembly_synced();
430 if self.assembly_components().is_empty() {
437 return Ok(());
438 }
439 brep_kernel::assembly_run_solve_json().map_err(js_err)?;
440 self.apply_assembly_fold(true);
441 self.rerun_history();
442 Ok(())
443 }
444
445 pub fn insert_component(&mut self, insert: ComponentInsert<'_>) -> Result<String, String> {
455 let part_name = match insert {
456 ComponentInsert::Existing { part_name } => part_name.to_string(),
457 ComponentInsert::New {
458 name,
459 source_key,
460 source_signature,
461 document_json,
462 } => brep_kernel::add_part_to_library(name, source_key, source_signature, document_json)
463 .map_err(js_err)?,
464 };
465 if let Ok(library) = serde_json::from_str::<Value>(&brep_kernel::parts_library_json()) {
468 self.history.set_parts_library(library);
469 }
470 let first = !(0..self.history.len()).any(|index| {
471 matches!(
472 self.history.feature_type(index).as_deref(),
473 Some("ACOMP") | Some("ASSEMBLY COMPONENT")
474 )
475 });
476 let id = self.history.next_feature_id("ACOMP");
477 let feature = serde_json::json!({
478 "type": "ACOMP",
479 "inputParams": {
480 "id": id,
481 "partName": part_name,
482 "transform": { "translate": [0, 0, 0], "rotateEulerDeg": [0, 0, 0] },
483 "isFixed": first,
484 },
485 "persistentData": {}
486 });
487 self.add_feature(&feature.to_string())?;
488 Ok(id)
489 }
490
491 pub fn set_component_fixed(&mut self, component_id: &str, fixed: bool) -> Result<(), String> {
494 let index = self
495 .history
496 .index_of(component_id)
497 .ok_or_else(|| format!("no component feature '{component_id}'"))?;
498 let mut params = self
499 .history
500 .feature_params(index)
501 .unwrap_or_else(|| serde_json::json!({}));
502 if let Some(map) = params.as_object_mut() {
503 map.insert("isFixed".into(), Value::Bool(fixed));
504 } else {
505 return Err(format!("component '{component_id}': malformed inputParams"));
506 }
507 self.update_feature_params(component_id, ¶ms.to_string())?;
508 Ok(())
509 }
510
511 pub fn select_component(&mut self, component_id: &str) {
515 self.select_components(&[component_id.to_string()]);
516 }
517
518 pub fn component_member_solids(&self, component_id: &str) -> Vec<String> {
521 self.assembly_components
522 .iter()
523 .filter(|record| record.id == component_id)
524 .flat_map(|record| record.solids.iter().cloned())
525 .collect()
526 }
527
528 pub fn toggle_component_selection(&mut self, component_id: &str) {
533 let members = self.component_member_solids(component_id);
534 if members.is_empty() {
535 return;
536 }
537 let all_selected = members
538 .iter()
539 .all(|name| self.emphasis.selected_solids.contains(name));
540 for name in members {
541 if all_selected {
542 self.emphasis.selected_solids.remove(&name);
543 } else {
544 self.emphasis.selected_solids.insert(name);
545 }
546 }
547 self.emphasis.generation = self.emphasis.generation.wrapping_add(1);
548 self.dirty = true;
549 }
550
551 pub fn select_components(&mut self, component_ids: &[String]) {
555 let members: Vec<String> = self
556 .assembly_components
557 .iter()
558 .filter(|record| component_ids.iter().any(|id| id == &record.id))
559 .flat_map(|record| record.solids.iter().cloned())
560 .collect();
561 let json = serde_json::json!({ "selected": { "solids": members } }).to_string();
562 let _ = self.emphasis.apply_json(&json);
563 self.dirty = true;
564 }
565
566 pub fn begin_ref_select_for_constraint(
574 &mut self,
575 constraint_id: &str,
576 path: Vec<String>,
577 label: String,
578 filter: Vec<String>,
579 multiple: bool,
580 seed_names: Vec<String>,
581 ) {
582 let restore_index = self.history.rollback();
583 self.selection_filter = SelectionFilter::from_ref_filter(&filter);
584 self.ref_select = Some(RefSelectState {
585 feature_id: constraint_id.to_string(),
586 path,
587 label,
588 filter,
589 multiple,
590 names: seed_names,
591 restore_index,
592 target: RefSelectTarget::AssemblyConstraint,
593 });
594 self.sync_ref_select_emphasis();
595 }
596
597 pub(crate) fn assembly_commit_constraint_refs(
601 &mut self,
602 constraint_id: &str,
603 path: &[String],
604 names: &[String],
605 multiple: bool,
606 ) {
607 let state = self.assembly_state_value();
608 let Some(entry) = state
609 .get("constraints")
610 .and_then(Value::as_array)
611 .and_then(|constraints| {
612 constraints.iter().find(|entry| {
613 entry
614 .get("inputParams")
615 .and_then(|params| params.get("id"))
616 .and_then(Value::as_str)
617 == Some(constraint_id)
618 })
619 })
620 else {
621 self.push_notice(format!("unknown constraint '{constraint_id}'"));
622 return;
623 };
624 let mut params = entry
625 .get("inputParams")
626 .cloned()
627 .unwrap_or_else(|| serde_json::json!({}));
628 let value = if multiple {
629 Value::Array(names.iter().cloned().map(Value::String).collect())
630 } else {
631 Value::String(names.first().cloned().unwrap_or_default())
632 };
633 super::selection_ux::set_json_at(&mut params, path, value);
634 if let Err(error) = self.assembly_update_constraint_no_rerun(constraint_id, ¶ms.to_string())
635 {
636 self.push_notice(format!("constraint update failed: {error}"));
637 }
638 }
639
640 pub(crate) fn component_vertex_ref(
645 &self,
646 solid_name: &str,
647 world_position: [f64; 3],
648 ) -> Option<String> {
649 let record = self.assembly_components.iter().find(|record| {
650 record.solids.iter().any(|member| member == solid_name)
651 })?;
652 let local = rigid_inverse_point(&record.transform, world_position);
653 Some(format!(
654 "{solid_name}@{},{},{}",
655 local[0], local[1], local[2]
656 ))
657 }
658}
659
660#[cfg(test)]
667mod tests {
668 use super::*;
669
670 fn part_document() -> String {
672 serde_json::json!({
673 "expressions": "",
674 "configurator": {},
675 "features": [{
676 "type": "P.CU",
677 "inputParams": {
678 "id": "Part",
679 "sizeX": 2.0, "sizeY": 3.0, "sizeZ": 4.0,
680 "transform": {
681 "position": [0.0, 0.0, 0.0],
682 "rotationEuler": [0.0, 0.0, 0.0],
683 "scale": [1.0, 1.0, 1.0]
684 },
685 "boolean": { "targets": [], "operation": "NONE" }
686 },
687 "persistentData": {}
688 }]
689 })
690 .to_string()
691 }
692
693 fn fresh_state() -> EngineState {
694 brep_kernel::clear_history_cache();
695 EngineState::new()
696 }
697
698 #[test]
701 fn solve_with_no_components_does_not_panic() {
702 let mut state = fresh_state();
703 let _ = state.assembly_run_solve();
704 }
705
706 #[test]
710 fn document_signature_is_stable_and_content_sensitive() {
711 let a = r#"{"features":[{"type":"P.CU","inputParams":{"id":"Part","sizeX":10}}]}"#;
712 let a_reordered = r#"{"features":[{"inputParams":{"sizeX":10,"id":"Part"},"type":"P.CU"}]}"#;
714 let b = r#"{"features":[{"type":"P.CU","inputParams":{"id":"Part","sizeX":14}}]}"#;
716 assert_eq!(document_signature(a), document_signature(a));
717 assert_eq!(document_signature(a), document_signature(a_reordered));
718 assert_ne!(document_signature(a), document_signature(b));
719 }
720
721 #[test]
726 fn insert_component_seeds_partname_and_first_instance_fixed() {
727 let mut state = fresh_state();
728 let document = part_document();
729
730 let id1 = state
731 .insert_component(ComponentInsert::New {
732 name: "bracket",
733 source_key: "bracket",
734 source_signature: "sig-1",
735 document_json: &document,
736 })
737 .expect("first insert");
738 assert_eq!(id1, "ACOMP1", "the counter mints the ACOMP<digits> form");
739 let params = state
740 .history
741 .feature_params(state.history.index_of(&id1).unwrap())
742 .unwrap();
743 assert_eq!(params["partName"], "bracket");
744 assert_eq!(params["isFixed"], true, "first instance is EXPLICITLY fixed");
745 assert_eq!(params["transform"]["translate"], serde_json::json!([0, 0, 0]));
746
747 let id2 = state
749 .insert_component(ComponentInsert::Existing { part_name: "bracket" })
750 .expect("second insert");
751 assert_eq!(id2, "ACOMP2");
752 let params2 = state
753 .history
754 .feature_params(state.history.index_of(&id2).unwrap())
755 .unwrap();
756 assert_eq!(params2["partName"], "bracket");
757 assert_eq!(params2["isFixed"], false);
758
759 assert_eq!(state.parts_library_names(), vec!["bracket".to_string()]);
762 let document_value: Value =
763 serde_json::from_str(&state.history.request_json()).unwrap();
764 assert!(
765 document_value["partsLibrary"]["bracket"].is_object(),
766 "the request document carries the library block for the runner/save"
767 );
768
769 let id3 = state
772 .insert_component(ComponentInsert::New {
773 name: "bracket-again",
774 source_key: "bracket",
775 source_signature: "sig-1",
776 document_json: &document,
777 })
778 .expect("dedup insert");
779 let params3 = state
780 .history
781 .feature_params(state.history.index_of(&id3).unwrap())
782 .unwrap();
783 assert_eq!(params3["partName"], "bracket", "dedup returns the EXISTING entry name");
784 assert_eq!(state.parts_library_names(), vec!["bracket".to_string()]);
785 }
786
787 #[test]
791 fn component_projection_and_library_gc_on_last_delete() {
792 let mut state = fresh_state();
793 let document = part_document();
794 state
795 .insert_component(ComponentInsert::New {
796 name: "bracket",
797 source_key: "bracket",
798 source_signature: "sig-1",
799 document_json: &document,
800 })
801 .unwrap();
802 state
803 .insert_component(ComponentInsert::Existing { part_name: "bracket" })
804 .unwrap();
805
806 let components = state.assembly_components();
807 assert_eq!(components.len(), 2);
808 assert_eq!(components[0].id, "ACOMP1");
809 assert_eq!(components[1].id, "ACOMP2");
810 assert_eq!(components[0].part_name, "bracket");
811 assert!(components[0].fixed, "first instance grounded");
812 assert!(!components[1].fixed);
813 assert_eq!(components[0].solids, vec!["ACOMP1:Part".to_string()]);
814 assert_eq!(components[1].solids, vec!["ACOMP2:Part".to_string()]);
815 assert!(state.scene.solid("ACOMP1:Part").is_some());
817 assert!(state.scene.solid("ACOMP2:Part").is_some());
818
819 state.delete_feature("ACOMP2");
821 assert_eq!(state.assembly_components().len(), 1);
822 assert_eq!(state.parts_library_names(), vec!["bracket".to_string()]);
823
824 state.delete_feature("ACOMP1");
827 assert!(state.assembly_components().is_empty());
828 assert!(state.parts_library_names().is_empty(), "orphan entry GC'd");
829 let document_value: Value =
830 serde_json::from_str(&state.history.request_json()).unwrap();
831 assert!(
832 document_value.get("partsLibrary").is_none(),
833 "no dangling partsLibrary block after the last instance"
834 );
835 }
836
837 #[test]
840 fn set_component_fixed_routes_to_the_feature_and_reruns() {
841 let mut state = fresh_state();
842 state
843 .insert_component(ComponentInsert::New {
844 name: "bracket",
845 source_key: "bracket",
846 source_signature: "sig-1",
847 document_json: &part_document(),
848 })
849 .unwrap();
850 assert!(state.assembly_components()[0].fixed);
851
852 state.set_component_fixed("ACOMP1", false).expect("unfix");
853 let params = state
854 .history
855 .feature_params(state.history.index_of("ACOMP1").unwrap())
856 .unwrap();
857 assert_eq!(params["isFixed"], false, "the FEATURE param is the truth");
858 assert!(
859 !state.assembly_components()[0].fixed,
860 "the projection follows after the re-run"
861 );
862 state.undo();
864 assert!(state.assembly_components()[0].fixed, "undo restores the flag");
865 }
866
867 #[test]
871 fn constraint_add_toggle_reorder_fold_and_undo() {
872 let mut state = fresh_state();
873 let document = part_document();
874 state
875 .insert_component(ComponentInsert::New {
876 name: "bracket",
877 source_key: "bracket",
878 source_signature: "sig-1",
879 document_json: &document,
880 })
881 .unwrap();
882 state
883 .insert_component(ComponentInsert::Existing { part_name: "bracket" })
884 .unwrap();
885
886 let fixed_id = state
888 .assembly_add_constraint(
889 "fixed",
890 &serde_json::json!({ "elements": ["ACOMP2"] }).to_string(),
891 )
892 .expect("add fixed");
893 assert_eq!(fixed_id, "FIXD1");
894
895 let document_value: Value =
898 serde_json::from_str(&state.history.request_json()).unwrap();
899 assert_eq!(document_value["assembly"]["constraints"][0]["type"], "fixed");
900 assert_eq!(
901 document_value["assembly"]["constraints"][0]["inputParams"]["id"],
902 "FIXD1"
903 );
904
905 let rows = state.assembly_statuses_value();
907 assert_eq!(rows[0]["id"], "FIXD1");
908 assert_eq!(rows[0]["enabled"], true);
909
910 state
912 .assembly_set_constraint_enabled(&fixed_id, false)
913 .expect("disable");
914 let rows = state.assembly_statuses_value();
915 assert_eq!(rows[0]["enabled"], false);
916 assert_eq!(rows[0]["status"], "disabled");
917
918 let second = state
920 .assembly_add_constraint("parallel", "{}")
921 .expect("add parallel");
922 assert_eq!(second, "PARA2", "the id counter is shared and monotonic");
923 state.assembly_move_constraint(&second, 0).expect("reorder");
924 let rows = state.assembly_statuses_value();
925 assert_eq!(rows[0]["id"], "PARA2");
926 assert_eq!(rows[1]["id"], "FIXD1");
927
928 state.undo();
930 let rows = state.assembly_statuses_value();
931 assert_eq!(rows[0]["id"], "FIXD1");
932 assert_eq!(rows[1]["id"], "PARA2");
933 }
934
935 #[test]
939 fn constraint_ref_select_commits_elements_to_the_constraint() {
940 let mut state = fresh_state();
941 let document = part_document();
942 state
943 .insert_component(ComponentInsert::New {
944 name: "bracket",
945 source_key: "bracket",
946 source_signature: "sig-1",
947 document_json: &document,
948 })
949 .unwrap();
950 state
951 .insert_component(ComponentInsert::Existing { part_name: "bracket" })
952 .unwrap();
953 let id = state.assembly_add_constraint("parallel", "{}").unwrap();
954
955 state.begin_ref_select_for_constraint(
956 &id,
957 vec!["elements".into()],
958 "Elements".into(),
959 vec!["FACE".into(), "EDGE".into()],
960 true,
961 Vec::new(),
962 );
963 assert!(state.ref_select_active());
964 state.ref_select.as_mut().unwrap().names =
967 vec!["ACOMP1:Part_PZ".to_string(), "ACOMP2:Part_NZ".to_string()];
968 state.finish_ref_select();
969 assert!(!state.ref_select_active(), "finish exits the modal");
970
971 let constraint_state = state.assembly_state_value();
973 assert_eq!(
974 constraint_state["constraints"][0]["inputParams"]["elements"],
975 serde_json::json!(["ACOMP1:Part_PZ", "ACOMP2:Part_NZ"])
976 );
977 let document_value: Value =
978 serde_json::from_str(&state.history.request_json()).unwrap();
979 assert_eq!(
980 document_value["assembly"]["constraints"][0]["inputParams"]["elements"],
981 serde_json::json!(["ACOMP1:Part_PZ", "ACOMP2:Part_NZ"]),
982 "the fold persisted the committed refs into the document"
983 );
984 }
985
986 #[test]
990 fn component_vertex_ref_is_component_local() {
991 let mut state = fresh_state();
992 state
993 .insert_component(ComponentInsert::New {
994 name: "bracket",
995 source_key: "bracket",
996 source_signature: "sig-1",
997 document_json: &part_document(),
998 })
999 .unwrap();
1000 let mut params = state
1002 .history
1003 .feature_params(state.history.index_of("ACOMP1").unwrap())
1004 .unwrap();
1005 params["transform"]["translate"] = serde_json::json!([10.0, 0.0, 0.0]);
1006 state
1007 .update_feature_params("ACOMP1", ¶ms.to_string())
1008 .unwrap();
1009
1010 let vertex_ref = state
1013 .component_vertex_ref("ACOMP1:Part", [12.0, 3.0, 4.0])
1014 .expect("component member yields a ref");
1015 assert_eq!(vertex_ref, "ACOMP1:Part@2,3,4");
1016 assert!(
1017 state.component_vertex_ref("Loose", [0.0; 3]).is_none(),
1018 "non-component solids build no vertex ref"
1019 );
1020 }
1021}