use super::*;
use serde_json::Value;
pub enum ComponentInsert<'a> {
Existing {
part_name: &'a str,
},
New {
name: &'a str,
source_key: &'a str,
source_signature: &'a str,
document_json: &'a str,
},
}
pub fn document_signature(doc_json: &str) -> String {
use std::hash::{Hash, Hasher};
fn walk(value: &serde_json::Value, hasher: &mut impl Hasher) {
match value {
serde_json::Value::Null => 0u8.hash(hasher),
serde_json::Value::Bool(flag) => {
1u8.hash(hasher);
flag.hash(hasher);
}
serde_json::Value::Number(number) => {
2u8.hash(hasher);
number.to_string().hash(hasher);
}
serde_json::Value::String(text) => {
3u8.hash(hasher);
text.hash(hasher);
}
serde_json::Value::Array(items) => {
4u8.hash(hasher);
for item in items {
walk(item, hasher);
}
}
serde_json::Value::Object(map) => {
5u8.hash(hasher);
let mut keys: Vec<&String> = map.keys().collect();
keys.sort();
for key in keys {
key.hash(hasher);
walk(&map[key], hasher);
}
}
}
}
let mut hasher = std::collections::hash_map::DefaultHasher::new();
match serde_json::from_str::<serde_json::Value>(doc_json) {
Ok(value) => walk(&value, &mut hasher),
Err(_) => doc_json.hash(&mut hasher),
}
format!("{:016x}", hasher.finish())
}
fn js_err<E: std::fmt::Debug>(error: E) -> String {
format!("{error:?}")
}
fn rigid_inverse_point(transform: &brep_kernel::AffineTransform, world: [f64; 3]) -> [f64; 3] {
let m = &transform.elements;
let d = [world[0] - m[3], world[1] - m[7], world[2] - m[11]];
[
m[0] * d[0] + m[4] * d[1] + m[8] * d[2],
m[1] * d[0] + m[5] * d[1] + m[9] * d[2],
m[2] * d[0] + m[6] * d[1] + m[10] * d[2],
]
}
impl EngineState {
pub fn history_has_assembly(&self) -> bool {
let has_acomp = (0..self.history.len()).any(|index| {
matches!(
self.history.feature_type(index).as_deref(),
Some("ACOMP") | Some("ASSEMBLY COMPONENT")
)
});
has_acomp
|| self
.history
.assembly_block()
.map(|block| !block.is_null())
.unwrap_or(false)
}
pub fn assembly_components(&self) -> &[brep_kernel::ComponentRecord] {
&self.assembly_components
}
pub fn ensure_assembly_synced(&mut self) {
if self.assembly_synced_generation == Some(self.applied_generation) {
return;
}
self.sync_assembly();
}
pub fn assembly_statuses_value(&mut self) -> Value {
self.ensure_assembly_synced();
serde_json::from_str(&brep_kernel::assembly_statuses_json())
.unwrap_or_else(|_| Value::Array(Vec::new()))
}
pub fn assembly_state_value(&mut self) -> Value {
self.ensure_assembly_synced();
serde_json::from_str(&brep_kernel::assembly_state_json()).unwrap_or(Value::Null)
}
pub fn assembly_dof_value(&mut self) -> Value {
self.ensure_assembly_synced();
serde_json::from_str(&brep_kernel::assembly_dof_json()).unwrap_or(Value::Null)
}
pub fn assembly_overlay_value(&mut self) -> Value {
self.ensure_assembly_synced();
serde_json::from_str(&brep_kernel::assembly_overlay_json())
.unwrap_or_else(|_| Value::Array(Vec::new()))
}
pub fn parts_library_names(&mut self) -> Vec<String> {
if self.history_has_assembly() {
self.ensure_assembly_synced();
}
serde_json::from_str::<Value>(&brep_kernel::parts_library_json())
.ok()
.and_then(|value| {
value
.as_object()
.map(|map| map.keys().cloned().collect::<Vec<_>>())
})
.unwrap_or_default()
}
pub(crate) fn sync_assembly(&mut self) {
if !self.history_has_assembly() {
self.assembly_components.clear();
self.history
.set_parts_library(Value::Object(serde_json::Map::new()));
self.assembly_synced_generation = Some(self.applied_generation);
return;
}
let request: brep_kernel::HistoryRequest =
match serde_json::from_value(self.history.prefix_request()) {
Ok(request) => request,
Err(_) => return, };
let result = brep_kernel::execute_history(&request);
let mut components: std::collections::BTreeMap<String, brep_kernel::ComponentRecord> =
std::collections::BTreeMap::new();
for feature in &result.results {
for removed in &feature.removed {
components.remove(removed);
}
for record in &feature.components {
components.insert(record.id.clone(), record.clone());
}
}
self.assembly_components = components.into_values().collect();
self.apply_assembly_fold(false);
let revision = brep_kernel::parts_library_revision();
if self.parts_library_block_revision != Some(revision)
|| !self.history.parts_library_mirrors_store()
{
if let Ok(library) = serde_json::from_str::<Value>(&brep_kernel::parts_library_json()) {
self.history.set_parts_library(library);
self.parts_library_block_revision = Some(revision);
}
}
self.assembly_synced_generation = Some(self.applied_generation);
}
fn apply_assembly_fold(&mut self, checkpoint: bool) {
let document = self.history.request_json_without_parts_library();
match brep_kernel::assembly_apply_document_json(&document) {
Ok(folded) => {
let adopted = if checkpoint {
self.history.adopt_document_checkpointed(&folded)
} else {
self.history.adopt_document(&folded)
};
if let Err(error) = adopted {
self.push_notice(format!("assembly fold failed: {error}"));
}
}
Err(_) => {} }
}
fn after_constraint_mutation(&mut self) {
self.apply_assembly_fold(true);
if self.settings.assembly_auto_solve {
self.rerun_history();
} else {
self.dirty = true;
}
}
pub fn assembly_add_constraint(
&mut self,
constraint_type: &str,
params_json: &str,
) -> Result<String, String> {
self.ensure_assembly_synced();
let reply =
brep_kernel::assembly_add_constraint_json(constraint_type, params_json).map_err(js_err)?;
self.after_constraint_mutation();
let id = serde_json::from_str::<Value>(&reply)
.ok()
.and_then(|value| value.get("id").and_then(|id| id.as_str()).map(String::from))
.unwrap_or_default();
Ok(id)
}
pub fn assembly_update_constraint(&mut self, id: &str, params_json: &str) -> Result<(), String> {
self.ensure_assembly_synced();
brep_kernel::assembly_update_constraint_json(id, params_json).map_err(js_err)?;
self.after_constraint_mutation();
Ok(())
}
pub(crate) fn assembly_update_constraint_no_rerun(
&mut self,
id: &str,
params_json: &str,
) -> Result<(), String> {
self.ensure_assembly_synced();
brep_kernel::assembly_update_constraint_json(id, params_json).map_err(js_err)?;
self.apply_assembly_fold(true);
Ok(())
}
pub fn assembly_remove_constraint(&mut self, id: &str) -> Result<(), String> {
self.ensure_assembly_synced();
brep_kernel::assembly_remove_constraint_json(id).map_err(js_err)?;
self.after_constraint_mutation();
Ok(())
}
pub fn assembly_set_constraint_enabled(&mut self, id: &str, enabled: bool) -> Result<(), String> {
self.ensure_assembly_synced();
brep_kernel::assembly_set_constraint_enabled_json(id, enabled).map_err(js_err)?;
self.after_constraint_mutation();
Ok(())
}
pub fn assembly_set_constraint_open(&mut self, id: &str, open: bool) -> Result<(), String> {
self.ensure_assembly_synced();
if open {
let others: Vec<String> =
serde_json::from_str::<Value>(&brep_kernel::assembly_state_json())
.ok()
.and_then(|state| {
state.get("constraints").and_then(|l| l.as_array()).map(|entries| {
entries
.iter()
.filter_map(|entry| {
let cid =
entry.get("inputParams")?.get("id")?.as_str()?;
let is_open =
entry.get("open").and_then(|v| v.as_bool()).unwrap_or(false);
(is_open && cid != id).then(|| cid.to_string())
})
.collect()
})
})
.unwrap_or_default();
for other in others {
brep_kernel::assembly_set_constraint_open_json(&other, false).map_err(js_err)?;
}
}
brep_kernel::assembly_set_constraint_open_json(id, open).map_err(js_err)?;
self.apply_assembly_fold(false);
Ok(())
}
pub fn assembly_move_constraint(&mut self, id: &str, index: usize) -> Result<(), String> {
self.ensure_assembly_synced();
brep_kernel::assembly_move_constraint_json(id, index).map_err(js_err)?;
self.after_constraint_mutation();
Ok(())
}
pub fn assembly_run_solve(&mut self) -> Result<(), String> {
self.ensure_assembly_synced();
if self.assembly_components().is_empty() {
return Ok(());
}
brep_kernel::assembly_run_solve_json().map_err(js_err)?;
self.apply_assembly_fold(true);
self.rerun_history();
Ok(())
}
pub fn insert_component(&mut self, insert: ComponentInsert<'_>) -> Result<String, String> {
let part_name = match insert {
ComponentInsert::Existing { part_name } => part_name.to_string(),
ComponentInsert::New {
name,
source_key,
source_signature,
document_json,
} => brep_kernel::add_part_to_library(name, source_key, source_signature, document_json)
.map_err(js_err)?,
};
if let Ok(library) = serde_json::from_str::<Value>(&brep_kernel::parts_library_json()) {
self.history.set_parts_library(library);
}
let first = !(0..self.history.len()).any(|index| {
matches!(
self.history.feature_type(index).as_deref(),
Some("ACOMP") | Some("ASSEMBLY COMPONENT")
)
});
let id = self.history.next_feature_id("ACOMP");
let feature = serde_json::json!({
"type": "ACOMP",
"inputParams": {
"id": id,
"partName": part_name,
"transform": { "translate": [0, 0, 0], "rotateEulerDeg": [0, 0, 0] },
"isFixed": first,
},
"persistentData": {}
});
self.add_feature(&feature.to_string())?;
Ok(id)
}
pub fn set_component_fixed(&mut self, component_id: &str, fixed: bool) -> Result<(), String> {
let index = self
.history
.index_of(component_id)
.ok_or_else(|| format!("no component feature '{component_id}'"))?;
let mut params = self
.history
.feature_params(index)
.unwrap_or_else(|| serde_json::json!({}));
if let Some(map) = params.as_object_mut() {
map.insert("isFixed".into(), Value::Bool(fixed));
} else {
return Err(format!("component '{component_id}': malformed inputParams"));
}
self.update_feature_params(component_id, ¶ms.to_string())?;
Ok(())
}
pub fn select_component(&mut self, component_id: &str) {
self.select_components(&[component_id.to_string()]);
}
pub fn component_member_solids(&self, component_id: &str) -> Vec<String> {
self.assembly_components
.iter()
.filter(|record| record.id == component_id)
.flat_map(|record| record.solids.iter().cloned())
.collect()
}
pub fn toggle_component_selection(&mut self, component_id: &str) {
let members = self.component_member_solids(component_id);
if members.is_empty() {
return;
}
let all_selected = members
.iter()
.all(|name| self.emphasis.selected_solids.contains(name));
for name in members {
if all_selected {
self.emphasis.selected_solids.remove(&name);
} else {
self.emphasis.selected_solids.insert(name);
}
}
self.emphasis.generation = self.emphasis.generation.wrapping_add(1);
self.dirty = true;
}
pub fn select_components(&mut self, component_ids: &[String]) {
let members: Vec<String> = self
.assembly_components
.iter()
.filter(|record| component_ids.iter().any(|id| id == &record.id))
.flat_map(|record| record.solids.iter().cloned())
.collect();
let json = serde_json::json!({ "selected": { "solids": members } }).to_string();
let _ = self.emphasis.apply_json(&json);
self.dirty = true;
}
pub fn begin_ref_select_for_constraint(
&mut self,
constraint_id: &str,
path: Vec<String>,
label: String,
filter: Vec<String>,
multiple: bool,
seed_names: Vec<String>,
) {
let restore_index = self.history.rollback();
self.selection_filter = SelectionFilter::from_ref_filter(&filter);
self.ref_select = Some(RefSelectState {
feature_id: constraint_id.to_string(),
path,
label,
filter,
multiple,
names: seed_names,
restore_index,
target: RefSelectTarget::AssemblyConstraint,
});
self.sync_ref_select_emphasis();
}
pub(crate) fn assembly_commit_constraint_refs(
&mut self,
constraint_id: &str,
path: &[String],
names: &[String],
multiple: bool,
) {
let state = self.assembly_state_value();
let Some(entry) = state
.get("constraints")
.and_then(Value::as_array)
.and_then(|constraints| {
constraints.iter().find(|entry| {
entry
.get("inputParams")
.and_then(|params| params.get("id"))
.and_then(Value::as_str)
== Some(constraint_id)
})
})
else {
self.push_notice(format!("unknown constraint '{constraint_id}'"));
return;
};
let mut params = entry
.get("inputParams")
.cloned()
.unwrap_or_else(|| serde_json::json!({}));
let value = if multiple {
Value::Array(names.iter().cloned().map(Value::String).collect())
} else {
Value::String(names.first().cloned().unwrap_or_default())
};
super::selection_ux::set_json_at(&mut params, path, value);
if let Err(error) = self.assembly_update_constraint_no_rerun(constraint_id, ¶ms.to_string())
{
self.push_notice(format!("constraint update failed: {error}"));
}
}
pub(crate) fn component_vertex_ref(
&self,
solid_name: &str,
world_position: [f64; 3],
) -> Option<String> {
let record = self.assembly_components.iter().find(|record| {
record.solids.iter().any(|member| member == solid_name)
})?;
let local = rigid_inverse_point(&record.transform, world_position);
Some(format!(
"{solid_name}@{},{},{}",
local[0], local[1], local[2]
))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn part_document() -> String {
serde_json::json!({
"expressions": "",
"configurator": {},
"features": [{
"type": "P.CU",
"inputParams": {
"id": "Part",
"sizeX": 2.0, "sizeY": 3.0, "sizeZ": 4.0,
"transform": {
"position": [0.0, 0.0, 0.0],
"rotationEuler": [0.0, 0.0, 0.0],
"scale": [1.0, 1.0, 1.0]
},
"boolean": { "targets": [], "operation": "NONE" }
},
"persistentData": {}
}]
})
.to_string()
}
fn fresh_state() -> EngineState {
brep_kernel::clear_history_cache();
EngineState::new()
}
#[test]
fn solve_with_no_components_does_not_panic() {
let mut state = fresh_state();
let _ = state.assembly_run_solve();
}
#[test]
fn document_signature_is_stable_and_content_sensitive() {
let a = r#"{"features":[{"type":"P.CU","inputParams":{"id":"Part","sizeX":10}}]}"#;
let a_reordered = r#"{"features":[{"inputParams":{"sizeX":10,"id":"Part"},"type":"P.CU"}]}"#;
let b = r#"{"features":[{"type":"P.CU","inputParams":{"id":"Part","sizeX":14}}]}"#;
assert_eq!(document_signature(a), document_signature(a));
assert_eq!(document_signature(a), document_signature(a_reordered));
assert_ne!(document_signature(a), document_signature(b));
}
#[test]
fn insert_component_seeds_partname_and_first_instance_fixed() {
let mut state = fresh_state();
let document = part_document();
let id1 = state
.insert_component(ComponentInsert::New {
name: "bracket",
source_key: "bracket",
source_signature: "sig-1",
document_json: &document,
})
.expect("first insert");
assert_eq!(id1, "ACOMP1", "the counter mints the ACOMP<digits> form");
let params = state
.history
.feature_params(state.history.index_of(&id1).unwrap())
.unwrap();
assert_eq!(params["partName"], "bracket");
assert_eq!(params["isFixed"], true, "first instance is EXPLICITLY fixed");
assert_eq!(params["transform"]["translate"], serde_json::json!([0, 0, 0]));
let id2 = state
.insert_component(ComponentInsert::Existing { part_name: "bracket" })
.expect("second insert");
assert_eq!(id2, "ACOMP2");
let params2 = state
.history
.feature_params(state.history.index_of(&id2).unwrap())
.unwrap();
assert_eq!(params2["partName"], "bracket");
assert_eq!(params2["isFixed"], false);
assert_eq!(state.parts_library_names(), vec!["bracket".to_string()]);
let document_value: Value =
serde_json::from_str(&state.history.request_json()).unwrap();
assert!(
document_value["partsLibrary"]["bracket"].is_object(),
"the request document carries the library block for the runner/save"
);
let id3 = state
.insert_component(ComponentInsert::New {
name: "bracket-again",
source_key: "bracket",
source_signature: "sig-1",
document_json: &document,
})
.expect("dedup insert");
let params3 = state
.history
.feature_params(state.history.index_of(&id3).unwrap())
.unwrap();
assert_eq!(params3["partName"], "bracket", "dedup returns the EXISTING entry name");
assert_eq!(state.parts_library_names(), vec!["bracket".to_string()]);
}
#[test]
fn component_projection_and_library_gc_on_last_delete() {
let mut state = fresh_state();
let document = part_document();
state
.insert_component(ComponentInsert::New {
name: "bracket",
source_key: "bracket",
source_signature: "sig-1",
document_json: &document,
})
.unwrap();
state
.insert_component(ComponentInsert::Existing { part_name: "bracket" })
.unwrap();
let components = state.assembly_components();
assert_eq!(components.len(), 2);
assert_eq!(components[0].id, "ACOMP1");
assert_eq!(components[1].id, "ACOMP2");
assert_eq!(components[0].part_name, "bracket");
assert!(components[0].fixed, "first instance grounded");
assert!(!components[1].fixed);
assert_eq!(components[0].solids, vec!["ACOMP1:Part".to_string()]);
assert_eq!(components[1].solids, vec!["ACOMP2:Part".to_string()]);
assert!(state.scene.solid("ACOMP1:Part").is_some());
assert!(state.scene.solid("ACOMP2:Part").is_some());
state.delete_feature("ACOMP2");
assert_eq!(state.assembly_components().len(), 1);
assert_eq!(state.parts_library_names(), vec!["bracket".to_string()]);
state.delete_feature("ACOMP1");
assert!(state.assembly_components().is_empty());
assert!(state.parts_library_names().is_empty(), "orphan entry GC'd");
let document_value: Value =
serde_json::from_str(&state.history.request_json()).unwrap();
assert!(
document_value.get("partsLibrary").is_none(),
"no dangling partsLibrary block after the last instance"
);
}
#[test]
fn set_component_fixed_routes_to_the_feature_and_reruns() {
let mut state = fresh_state();
state
.insert_component(ComponentInsert::New {
name: "bracket",
source_key: "bracket",
source_signature: "sig-1",
document_json: &part_document(),
})
.unwrap();
assert!(state.assembly_components()[0].fixed);
state.set_component_fixed("ACOMP1", false).expect("unfix");
let params = state
.history
.feature_params(state.history.index_of("ACOMP1").unwrap())
.unwrap();
assert_eq!(params["isFixed"], false, "the FEATURE param is the truth");
assert!(
!state.assembly_components()[0].fixed,
"the projection follows after the re-run"
);
state.undo();
assert!(state.assembly_components()[0].fixed, "undo restores the flag");
}
#[test]
fn constraint_add_toggle_reorder_fold_and_undo() {
let mut state = fresh_state();
let document = part_document();
state
.insert_component(ComponentInsert::New {
name: "bracket",
source_key: "bracket",
source_signature: "sig-1",
document_json: &document,
})
.unwrap();
state
.insert_component(ComponentInsert::Existing { part_name: "bracket" })
.unwrap();
let fixed_id = state
.assembly_add_constraint(
"fixed",
&serde_json::json!({ "elements": ["ACOMP2"] }).to_string(),
)
.expect("add fixed");
assert_eq!(fixed_id, "FIXD1");
let document_value: Value =
serde_json::from_str(&state.history.request_json()).unwrap();
assert_eq!(document_value["assembly"]["constraints"][0]["type"], "fixed");
assert_eq!(
document_value["assembly"]["constraints"][0]["inputParams"]["id"],
"FIXD1"
);
let rows = state.assembly_statuses_value();
assert_eq!(rows[0]["id"], "FIXD1");
assert_eq!(rows[0]["enabled"], true);
state
.assembly_set_constraint_enabled(&fixed_id, false)
.expect("disable");
let rows = state.assembly_statuses_value();
assert_eq!(rows[0]["enabled"], false);
assert_eq!(rows[0]["status"], "disabled");
let second = state
.assembly_add_constraint("parallel", "{}")
.expect("add parallel");
assert_eq!(second, "PARA2", "the id counter is shared and monotonic");
state.assembly_move_constraint(&second, 0).expect("reorder");
let rows = state.assembly_statuses_value();
assert_eq!(rows[0]["id"], "PARA2");
assert_eq!(rows[1]["id"], "FIXD1");
state.undo();
let rows = state.assembly_statuses_value();
assert_eq!(rows[0]["id"], "FIXD1");
assert_eq!(rows[1]["id"], "PARA2");
}
#[test]
fn constraint_ref_select_commits_elements_to_the_constraint() {
let mut state = fresh_state();
let document = part_document();
state
.insert_component(ComponentInsert::New {
name: "bracket",
source_key: "bracket",
source_signature: "sig-1",
document_json: &document,
})
.unwrap();
state
.insert_component(ComponentInsert::Existing { part_name: "bracket" })
.unwrap();
let id = state.assembly_add_constraint("parallel", "{}").unwrap();
state.begin_ref_select_for_constraint(
&id,
vec!["elements".into()],
"Elements".into(),
vec!["FACE".into(), "EDGE".into()],
true,
Vec::new(),
);
assert!(state.ref_select_active());
state.ref_select.as_mut().unwrap().names =
vec!["ACOMP1:Part_PZ".to_string(), "ACOMP2:Part_NZ".to_string()];
state.finish_ref_select();
assert!(!state.ref_select_active(), "finish exits the modal");
let constraint_state = state.assembly_state_value();
assert_eq!(
constraint_state["constraints"][0]["inputParams"]["elements"],
serde_json::json!(["ACOMP1:Part_PZ", "ACOMP2:Part_NZ"])
);
let document_value: Value =
serde_json::from_str(&state.history.request_json()).unwrap();
assert_eq!(
document_value["assembly"]["constraints"][0]["inputParams"]["elements"],
serde_json::json!(["ACOMP1:Part_PZ", "ACOMP2:Part_NZ"]),
"the fold persisted the committed refs into the document"
);
}
#[test]
fn component_vertex_ref_is_component_local() {
let mut state = fresh_state();
state
.insert_component(ComponentInsert::New {
name: "bracket",
source_key: "bracket",
source_signature: "sig-1",
document_json: &part_document(),
})
.unwrap();
let mut params = state
.history
.feature_params(state.history.index_of("ACOMP1").unwrap())
.unwrap();
params["transform"]["translate"] = serde_json::json!([10.0, 0.0, 0.0]);
state
.update_feature_params("ACOMP1", ¶ms.to_string())
.unwrap();
let vertex_ref = state
.component_vertex_ref("ACOMP1:Part", [12.0, 3.0, 4.0])
.expect("component member yields a ref");
assert_eq!(vertex_ref, "ACOMP1:Part@2,3,4");
assert!(
state.component_vertex_ref("Loose", [0.0; 3]).is_none(),
"non-component solids build no vertex ref"
);
}
}