use super::{FLAGS_KEY, ITEM_KEY, QUANTITY_KEY, VISIBLE_KEY};
use crate::column_tree::{RowAction, RowNode};
use crate::panels::assembly_components::{is_acomp_segment, ChainNode};
use crate::panels::bom_columns::{BomColumn, Scope};
use brep_render::engine_state::EngineState;
use serde_json::Value;
use std::collections::{HashMap, HashSet};
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct NestedLine {
pub paths: Vec<String>,
pub part_name: String,
pub part_names: Vec<String>,
pub part_source: Option<String>,
pub owner_names: Vec<String>,
pub owner_source: Option<String>,
pub feature_id: String,
pub owner_uses: usize,
}
pub(crate) struct NestedCtx<'a> {
pub columns: &'a [BomColumn],
pub selected: &'a HashSet<String>,
pub solids: &'a [(String, bool)],
pub visible_under: &'a HashMap<String, bool>,
pub opened: &'a HashSet<String>,
pub root_parts: &'a [String],
pub library: &'a Value,
pub lines: &'a mut HashMap<String, NestedLine>,
pub uses: UseCounts,
pub top_params: &'a HashMap<String, Value>,
}
pub(crate) fn visibility_by_prefix(solids: &[(String, bool)]) -> HashMap<String, bool> {
let mut map: HashMap<String, bool> = HashMap::new();
for (name, visible) in solids {
for (at, _) in name.match_indices(':') {
let all = map.entry(name[..at].to_string()).or_insert(true);
*all &= *visible;
}
}
map
}
fn source_of(entry: &Value) -> Option<String> {
entry["sourceKey"].as_str().filter(|s| !s.is_empty()).map(str::to_string)
}
fn acomp<'a>(document: &'a Value, id: &str) -> Option<&'a Value> {
document["features"].as_array()?.iter().find(|feature| {
matches!(feature["type"].as_str(), Some("ACOMP" | "ASSEMBLY COMPONENT"))
&& feature["inputParams"]["id"].as_str() == Some(id)
})
}
pub(crate) fn definition<'a>(library: &'a Value, names: &[String]) -> Option<&'a Value> {
let (first, rest) = names.split_first()?;
let mut document = &library[first]["document"];
for name in rest {
document = &document["partsLibrary"][name]["document"];
}
document.is_object().then_some(document)
}
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn count_uses(root_parts: &[String], library: &Value, names: &[String], source: Option<&str>) -> usize {
fn walk<'a>(
parts: &mut dyn Iterator<Item = &'a str>,
library: &'a Value,
chain: &mut Vec<String>,
names: &[String],
source: Option<&str>,
) -> usize {
let mut count = 0;
for name in parts {
let entry = &library[name];
chain.push(name.to_string());
let same = match source {
Some(source) => entry["sourceKey"].as_str() == Some(source),
None => chain.as_slice() == names,
};
if same {
count += 1;
} else if chain.len() < 32 {
let document = &entry["document"];
let mut inner = document["features"]
.as_array()
.into_iter()
.flatten()
.filter(|f| matches!(f["type"].as_str(), Some("ACOMP" | "ASSEMBLY COMPONENT")))
.filter_map(|f| f["inputParams"]["partName"].as_str());
count += walk(&mut inner, &document["partsLibrary"], chain, names, source);
}
chain.pop();
}
count
}
let mut parts = root_parts.iter().map(String::as_str);
walk(&mut parts, library, &mut Vec::new(), names, source)
}
pub(crate) struct UseCounts {
by_source: HashMap<String, usize>,
by_chain: HashMap<Vec<String>, usize>,
}
impl UseCounts {
pub(crate) fn new(root_parts: &[String], library: &Value) -> Self {
fn walk<'a>(
parts: &mut dyn Iterator<Item = &'a str>,
library: &'a Value,
chain: &mut Vec<String>,
sources: &mut Vec<&'a str>,
counts: &mut UseCounts,
) {
for name in parts {
let entry = &library[name];
chain.push(name.to_string());
*counts.by_chain.entry(chain.clone()).or_default() += 1;
let source = entry["sourceKey"].as_str();
if let Some(source) = source {
if !sources.contains(&source) {
*counts.by_source.entry(source.to_string()).or_default() += 1;
}
}
if chain.len() < 32 {
let document = &entry["document"];
let mut inner = document["features"]
.as_array()
.into_iter()
.flatten()
.filter(|f| matches!(f["type"].as_str(), Some("ACOMP" | "ASSEMBLY COMPONENT")))
.filter_map(|f| f["inputParams"]["partName"].as_str());
sources.extend(source);
walk(&mut inner, &document["partsLibrary"], chain, sources, counts);
if source.is_some() {
sources.pop();
}
}
chain.pop();
}
}
let mut counts = UseCounts { by_source: HashMap::new(), by_chain: HashMap::new() };
let mut parts = root_parts.iter().map(String::as_str);
walk(&mut parts, library, &mut Vec::new(), &mut Vec::new(), &mut counts);
counts
}
pub(crate) fn get(&self, names: &[String], source: Option<&str>) -> usize {
match source {
Some(source) => self.by_source.get(source).copied().unwrap_or(0),
None => self.by_chain.get(names).copied().unwrap_or(0),
}
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn rows(
ctx: &mut NestedCtx<'_>,
row_key: &str,
paths: &[String],
names: &[String],
source: Option<&str>,
nodes: &[ChainNode],
document: &Value,
) -> Vec<RowNode> {
let owner_uses = ctx.uses.get(names, source);
nodes
.iter()
.filter(|node| is_acomp_segment(&node.label))
.map(|node| {
let id = format!("{row_key}:{}", node.label);
let child_paths: Vec<String> = paths.iter().map(|p| format!("{p}:{}", node.label)).collect();
let feature = acomp(document, &node.label);
let part_name = feature
.and_then(|f| f["inputParams"]["partName"].as_str())
.filter(|name| !name.is_empty())
.map(str::to_string);
let entry = part_name.as_ref().map(|name| &document["partsLibrary"][name]).unwrap_or(&Value::Null);
let child_document = &entry["document"];
let mut child_names = names.to_vec();
child_names.push(part_name.clone().unwrap_or_default());
let child_source = source_of(entry);
let line = NestedLine {
paths: child_paths.clone(),
part_name: part_name.clone().unwrap_or_else(|| node.label.clone()),
part_names: child_names.clone(),
part_source: child_source.clone(),
owner_names: names.to_vec(),
owner_source: source.map(str::to_string),
feature_id: node.label.clone(),
owner_uses,
};
let mut cells = HashMap::new();
cells.insert(ITEM_KEY.to_string(), Value::String(line.part_name.clone()));
cells.insert(QUANTITY_KEY.to_string(), Value::from(child_paths.len() as u64));
let visible = child_paths.iter().all(|p| ctx.visible_under.get(p).copied().unwrap_or(true));
cells.insert(VISIBLE_KEY.to_string(), Value::Bool(visible));
let fixed_states: Vec<Result<bool, String>> = child_paths
.iter()
.map(|path| {
let (top, rest) = path.split_once(':').unwrap_or((path, ""));
let params = ctx.top_params.get(top).unwrap_or(&Value::Null);
let part = params["partName"].as_str().unwrap_or_default();
brep_render::engine_state::nested_fixed_state(params, rest, &ctx.library[part]["document"])
})
.collect();
let fixed = !fixed_states.is_empty() && fixed_states.iter().all(|state| state == &Ok(true));
let mut flags = Vec::new();
if fixed {
flags.push(serde_json::json!({
"glyph": crate::panels::assembly_components::FIXED_GLYPH,
"tooltip": "Grounded — unfix it before moving it",
}));
}
cells.insert(FLAGS_KEY.to_string(), Value::Array(flags));
let occurrence = feature.map(|f| &f["inputParams"]["bom"]).unwrap_or(&Value::Null);
let part_attributes = &child_document[brep_render::engine_state::PART_ATTRIBUTES];
let mut locked = HashMap::new();
let plm_part = child_source
.as_deref()
.is_some_and(|key| crate::plm::bom::revision_of_document(key).is_some());
for column in ctx.columns {
let key = column.key();
if key == QUANTITY_KEY {
continue;
}
let value = match column.scope {
Scope::Part => &part_attributes[&column.field],
Scope::Occurrence => &occurrence[&column.field],
};
if !value.is_null() {
cells.insert(key.clone(), value.clone());
}
match column.scope {
Scope::Occurrence if owner_uses > 1 => {
locked.insert(
key,
format!(
"Stored on this placement inside '{}', which is placed {owner_uses} times; edit it in '{}' to change every placement",
names.last().map(String::as_str).unwrap_or(""),
names.last().map(String::as_str).unwrap_or(""),
),
);
}
Scope::Part if plm_part && super::PLM_PART_FIELDS.iter().any(|f| column.field.eq_ignore_ascii_case(f)) => {
locked.insert(key, "The PLM's own: it is not edited here".to_string());
}
_ => {}
}
}
let selected = is_selected(ctx, &child_paths);
let actions = crate::panels::component_actions::component_menu(
&crate::panels::component_actions::MenuSubject {
ids: child_paths.clone(),
embedded: child_source.as_deref().is_none_or(str::is_empty),
fixed: fixed_states.clone(),
visible,
},
);
let children = rows(
ctx,
&id,
&child_paths,
&child_names,
child_source.as_deref(),
&node.children,
child_document,
);
let resolvable = part_name.is_some() && child_document.is_object();
ctx.lines.insert(id.clone(), line);
let expanded = ctx.opened.contains(&id);
RowNode {
children,
id,
cells,
editable: resolvable,
selected,
expanded,
actions: if resolvable { actions } else { Vec::new() },
locked,
}
})
.collect()
}
fn is_selected(ctx: &NestedCtx<'_>, paths: &[String]) -> bool {
paths.iter().any(|path| {
let prefix = format!("{path}:");
if !ctx.selected.iter().any(|name| name.starts_with(&prefix)) {
return false;
}
let parent = match path.rsplit_once(':') {
Some((parent, _)) => format!("{parent}:"),
None => return true,
};
let mut parent_solids = ctx.solids.iter().filter(|(name, _)| name.starts_with(&parent)).peekable();
parent_solids.peek().is_some()
&& !parent_solids.all(|(name, _)| ctx.selected.contains(name))
})
}
pub(crate) fn select_paths(state: &mut EngineState, paths: &[String], additive: bool) {
let members: Vec<String> = state
.scene
.solids()
.iter()
.filter(|solid| paths.iter().any(|p| solid.name.starts_with(&format!("{p}:"))))
.map(|solid| solid.name.clone())
.collect();
let mut solids: Vec<String> = if additive {
state.emphasis.selected_solids.iter().cloned().collect()
} else {
Vec::new()
};
let all_in = !members.is_empty() && members.iter().all(|m| solids.contains(m));
if additive && all_in {
solids.retain(|s| !members.contains(s));
} else {
for member in members {
if !solids.contains(&member) {
solids.push(member);
}
}
}
let json = serde_json::json!({ "selected": { "solids": solids } }).to_string();
let _ = state.emphasis.apply_json(&json);
state.dirty = true;
}
pub(crate) fn write_record(owner: &mut Value, record_key: &str, key: &str, value: Value) {
let remove = value.is_null() || value.as_str().is_some_and(str::is_empty);
let Some(object) = owner.as_object_mut() else { return };
if remove {
if let Some(record) = object.get_mut(record_key).and_then(Value::as_object_mut) {
record.remove(key);
if record.is_empty() {
object.remove(record_key);
}
}
return;
}
let record = object
.entry(record_key.to_string())
.or_insert_with(|| Value::Object(Default::default()));
if !record.is_object() {
*record = Value::Object(Default::default());
}
record[key] = value;
}
pub(crate) fn apply_definition(
state: &mut EngineState,
names: &[String],
source: Option<&str>,
document: &Value,
) -> Result<(), String> {
let mut assembly: Value =
serde_json::from_str(&state.history_request_json()).map_err(|e| e.to_string())?;
let mut notes = Vec::new();
if !crate::document::work_part::refresh_parts(&mut assembly, names, source, document, &mut notes)? {
return Err("the assembly no longer holds this definition".into());
}
state.edit_document_json(&assembly.to_string())?;
for note in notes {
state.push_notice(format!("BOM: {note}"));
}
Ok(())
}
pub(crate) fn set_part_attribute(
state: &mut EngineState,
names: &[String],
source: Option<&str>,
field: &str,
value: Value,
) -> Result<(), String> {
let mut document = definition(state.history.parts_library(), names)
.cloned()
.ok_or("the part's definition is missing")?;
write_record(&mut document, brep_render::engine_state::PART_ATTRIBUTES, field, value);
apply_definition(state, names, source, &document)
}
pub(crate) fn apply_edit(
state: &mut EngineState,
line: &NestedLine,
column: &BomColumn,
value: Value,
) -> Result<(), String> {
match column.scope {
Scope::Part => set_part_attribute(state, &line.part_names, line.part_source.as_deref(), &column.field, value),
Scope::Occurrence => {
if line.owner_uses > 1 {
return Err(format!(
"'{}' is placed {} times; edit the placement in its own document",
line.owner_names.last().map(String::as_str).unwrap_or(""),
line.owner_uses
));
}
let mut document = definition(state.history.parts_library(), &line.owner_names)
.cloned()
.ok_or("the sub-assembly's definition is missing")?;
let feature = document["features"]
.as_array_mut()
.and_then(|features| {
features.iter_mut().find(|f| f["inputParams"]["id"].as_str() == Some(&line.feature_id))
})
.ok_or("the placement is missing from its sub-assembly")?;
write_record(
&mut feature["inputParams"],
brep_render::engine_state::OCCURRENCE_ATTRIBUTES,
&column.field,
value,
);
apply_definition(state, &line.owner_names, line.owner_source.as_deref(), &document)
}
}
}
pub(crate) fn line_state(row: &RowNode, line: &NestedLine) -> Value {
serde_json::json!({
"partName": line.part_name,
"paths": line.paths,
"selected": row.selected,
"editable": row.editable,
"cells": row.cells,
"locked": row.locked,
"ownerUses": line.owner_uses,
})
}