use super::assembly::{StepOccurrence, StepProduct};
use super::parse::Entity;
use super::pmi::{file_mentions_pmi, lift_pmi, NameMap};
use super::{HashMap, HashSet, Resolver};
use crate::feature_pipeline::pmi::{PmiAnnotation, PmiState, PmiView};
pub const OCCURRENCE_REF_PREFIX: &str = "OCC";
pub(super) const PART_IMPORT_FEATURE: &str = "IMPORT3D1";
pub(super) fn pin_occurrence(name: &str, occurrence: usize) -> String {
match name.split_once('#') {
Some((product, local)) if !product.contains('@') => {
format!("{product}@{occurrence}#{local}")
}
_ => name.to_string(),
}
}
fn split_qualified(name: &str) -> Option<(usize, Option<usize>, &str)> {
let (qualifier, local) = name.split_once('#')?;
let (product, occurrence) = match qualifier.split_once('@') {
Some((product, occurrence)) => (product, Some(occurrence.parse().ok()?)),
None => (qualifier, None),
};
Some((product.parse().ok()?, occurrence, local))
}
#[derive(Default)]
pub(super) struct AssemblyPmi {
pub per_product: Vec<Option<PmiState>>,
pub assembly: Option<PmiState>,
pub diagnostics: Vec<String>,
}
pub(super) type ProductFaceRefs = Vec<Vec<Vec<usize>>>;
pub(super) fn lift(
text: &str,
entities: &HashMap<usize, Entity>,
products: &[StepProduct],
face_refs: &ProductFaceRefs,
occurrences: &[StepOccurrence],
roots: &[usize],
global_scale: f64,
mut scale_of: impl FnMut(usize) -> f64,
) -> AssemblyPmi {
let mut out = AssemblyPmi {
per_product: vec![None; products.len()],
..AssemblyPmi::default()
};
if !file_mentions_pmi(text) {
return out;
}
let mut names = NameMap::empty(entities, global_scale);
for (index, product) in products.iter().enumerate() {
let Some(refs) = face_refs.get(index) else {
continue;
};
let resolver = Resolver::new(entities, scale_of(product.pd_ref));
let body_names =
crate::feature_pipeline::imported_solid_names(product.bodies.len(), PART_IMPORT_FEATURE);
names.add_bodies(entities, &resolver, &body_names, refs, &format!("{index}#"));
}
let edges: HashSet<usize> = occurrences.iter().map(|edge| edge.edge_ref).collect();
let pins = occurrence_pins(entities, &edges);
let mut diagnostics = Vec::new();
let Some(state) = lift_pmi(text, entities, &names, &pins, &mut diagnostics) else {
out.diagnostics = diagnostics;
return out;
};
out.diagnostics = diagnostics;
let paths = OccurrencePaths::build(products.len(), occurrences, roots);
let mut assembly = PmiState::default();
let mut per_product: Vec<PmiState> = vec![PmiState::default(); products.len()];
for view in &state.views {
let mut assembly_rows: Vec<PmiAnnotation> = Vec::new();
let mut product_rows: Vec<Vec<PmiAnnotation>> = vec![Vec::new(); products.len()];
for annotation in &view.annotations {
let refs = subject_references(&annotation.params);
let every = all_references(&annotation.params);
let owners: Vec<usize> = {
let mut owners: Vec<usize> = refs.iter().map(|(product, _, _)| *product).collect();
owners.sort_unstable();
owners.dedup();
owners
};
let pinned = refs.iter().find_map(|(_, occurrence, _)| *occurrence);
let outside = |product: usize| {
every
.iter()
.any(|(other, occurrence, _)| *other != product || occurrence.is_some())
};
match (owners.as_slice(), pinned) {
([product], None) if !outside(*product) => {
let mut annotation = annotation.clone();
rewrite_references(&mut annotation.params, |_, _, local, _| local.to_string());
product_rows[*product].push(annotation);
}
_ => {
let mut annotation = annotation.clone();
rewrite_references(
&mut annotation.params,
|product, occurrence, local, original| {
match paths
.chain(product, occurrence)
.or_else(|| paths.chain(product, None))
{
Some(chain) => occurrence_ref(&chain, local),
None => original.to_string(),
}
},
);
for (product, occurrence, local) in &every {
if paths.chain(*product, *occurrence).is_some() {
continue;
}
out.diagnostics.push(if paths.chain(*product, None).is_some() {
format!(
"PMI: '{local}' names occurrence #{} of product {product}, which \
no chain from a root ends in; the reference was resolved through \
that product's first occurrence instead",
occurrence.unwrap_or_default()
)
} else {
format!(
"PMI: '{local}' annotates product {product}, which this import \
placed through no occurrence at all; the reference is kept \
unresolved"
)
});
}
assembly_rows.push(annotation);
}
}
}
if !assembly_rows.is_empty() {
push_view(&mut assembly, view, assembly_rows);
}
for (product, rows) in product_rows.into_iter().enumerate() {
if !rows.is_empty() {
push_view(&mut per_product[product], view, rows);
}
}
}
for (index, state) in per_product.into_iter().enumerate() {
if !state.views.is_empty() {
out.per_product[index] = Some(state);
}
}
if !assembly.views.is_empty() {
out.assembly = Some(assembly);
}
out
}
fn push_view(state: &mut PmiState, source: &PmiView, annotations: Vec<PmiAnnotation>) {
let id = state.next_id("VIEW");
let annotations = annotations
.into_iter()
.map(|mut annotation| {
let prefix = crate::feature_pipeline::pmi::pmi_type(&annotation.kind)
.map(|def| def.short_name)
.unwrap_or("PMI");
let fresh = state.next_id(prefix);
if let Some(object) = annotation.params.as_object_mut() {
object.insert("id".into(), serde_json::Value::String(fresh));
}
annotation
})
.collect();
state.views.push(PmiView {
id,
name: source.name.clone(),
camera: source.camera.clone(),
display: source.display.clone(),
annotations,
});
}
const PLANE_FIELD: &str = "plane";
fn all_references(params: &serde_json::Value) -> Vec<(usize, Option<usize>, String)> {
let mut out = Vec::new();
visit_strings(params, &mut |text| {
if let Some((product, occurrence, local)) = split_qualified(text) {
out.push((product, occurrence, local.to_string()));
}
});
out
}
fn subject_references(params: &serde_json::Value) -> Vec<(usize, Option<usize>, String)> {
let mut out = Vec::new();
let mut push = |text: &str| {
if let Some((product, occurrence, local)) = split_qualified(text) {
out.push((product, occurrence, local.to_string()));
}
};
match params {
serde_json::Value::Object(map) => {
for (key, value) in map {
if key == PLANE_FIELD {
continue;
}
visit_strings(value, &mut push);
}
}
other => visit_strings(other, &mut push),
}
out
}
fn rewrite_references(
params: &mut serde_json::Value,
mut name: impl FnMut(usize, Option<usize>, &str, &str) -> String,
) {
map_strings(params, &mut |text| {
split_qualified(text)
.map(|(product, occurrence, local)| name(product, occurrence, local, text))
});
}
fn visit_strings(value: &serde_json::Value, visit: &mut impl FnMut(&str)) {
match value {
serde_json::Value::String(text) => visit(text),
serde_json::Value::Array(items) => items.iter().for_each(|item| visit_strings(item, visit)),
serde_json::Value::Object(map) => map.values().for_each(|item| visit_strings(item, visit)),
_ => {}
}
}
fn map_strings(value: &mut serde_json::Value, map: &mut impl FnMut(&str) -> Option<String>) {
match value {
serde_json::Value::String(text) => {
if let Some(replacement) = map(text) {
*text = replacement;
}
}
serde_json::Value::Array(items) => items.iter_mut().for_each(|item| map_strings(item, map)),
serde_json::Value::Object(object) => {
object.values_mut().for_each(|item| map_strings(item, map))
}
_ => {}
}
}
fn occurrence_ref(chain: &[usize], local: &str) -> String {
let mut out = String::from(OCCURRENCE_REF_PREFIX);
for edge in chain {
out.push('#');
out.push_str(&edge.to_string());
}
out.push(':');
out.push_str(local);
out
}
pub fn occurrence_ref_parts(name: &str) -> Option<(Vec<usize>, &str)> {
let (head, local) = name.split_once(':')?;
let mut parts = head.split('#');
if parts.next()? != OCCURRENCE_REF_PREFIX {
return None;
}
let chain: Option<Vec<usize>> = parts.map(|edge| edge.parse().ok()).collect();
Some((chain?, local))
}
pub fn rewrite_occurrence_refs(
params: &mut serde_json::Value,
mut namespace: impl FnMut(&[usize]) -> Option<String>,
) -> usize {
let mut unplaced = 0usize;
map_strings(params, &mut |text| {
let (chain, local) = occurrence_ref_parts(text)?;
match namespace(&chain) {
Some(prefix) => Some(format!("{prefix}{local}")),
None => {
unplaced += 1;
None
}
}
});
unplaced
}
fn occurrence_pins(
entities: &HashMap<usize, Entity>,
edges: &HashSet<usize>,
) -> HashMap<usize, usize> {
let mut occurrence_of_shape: HashMap<usize, usize> = HashMap::default();
for (id, entity) in entities {
let Some(args) = entity.find("PRODUCT_DEFINITION_SHAPE") else {
continue;
};
let Some(definition) = args.get(2).and_then(|value| value.as_ref_id().ok()) else {
continue;
};
if edges.contains(&definition) {
occurrence_of_shape.insert(*id, definition);
}
}
let mut pins = HashMap::default();
if occurrence_of_shape.is_empty() {
return pins;
}
for (id, entity) in entities {
let Some(args) = entity.find("SHAPE_ASPECT") else {
continue;
};
let Some(of_shape) = args.get(2).and_then(|value| value.as_ref_id().ok()) else {
continue;
};
if let Some(edge) = occurrence_of_shape.get(&of_shape) {
pins.insert(*id, *edge);
}
}
pins
}
const MAX_CHAINS: usize = 4096;
struct OccurrencePaths {
chains: Vec<Vec<Vec<usize>>>,
}
impl OccurrencePaths {
fn build(products: usize, occurrences: &[StepOccurrence], roots: &[usize]) -> Self {
let mut chains: Vec<Vec<Vec<usize>>> = vec![Vec::new(); products];
let mut edges: Vec<&StepOccurrence> = occurrences.iter().collect();
edges.sort_by_key(|edge| edge.edge_ref);
let mut total = 0usize;
let mut stack: Vec<(usize, Vec<usize>)> =
roots.iter().rev().map(|root| (*root, Vec::new())).collect();
while let Some((product, chain)) = stack.pop() {
if total >= MAX_CHAINS || product >= products {
break;
}
total += 1;
chains[product].push(chain.clone());
for edge in edges.iter().rev() {
if edge.parent != product || chain.contains(&edge.edge_ref) {
continue;
}
let mut next = chain.clone();
next.push(edge.edge_ref);
stack.push((edge.child, next));
}
}
Self { chains }
}
fn chain(&self, product: usize, occurrence: Option<usize>) -> Option<Vec<usize>> {
let chains = self.chains.get(product)?;
match occurrence {
Some(edge) => chains
.iter()
.find(|chain| chain.last() == Some(&edge))
.or_else(|| chains.iter().find(|chain| chain.contains(&edge)))
.cloned(),
None => chains.first().cloned(),
}
}
}