//! Read-only usages of the actual process graph, including bounded reuse tiles. use super::*; use crate::diagram_usage::{Surface, Target, Usage}; /// The only presentation inputs needed by a background usage query. Drafts, /// focus, camera, history and widget state never cross the worker boundary. #[derive(Clone)] pub(crate) struct UsageSnapshot { identity: Option<(Id, Id)>, expanded: BTreeSet, contextual_expanded: BTreeSet, } impl UsageSnapshot { pub(crate) fn into_query_state(self) -> State { State { identity: self.identity, expanded: self.expanded, contextual_expanded: self.contextual_expanded, ..State::default() } } } impl State { pub(crate) fn selection_address(&self) -> Option { self.selected_context.clone() } pub(crate) fn selection_targets(&self, p: &Project, owner: Id) -> Vec { let graph = self.usage_graph(p, owner); let mut targets = vec![]; for node in &graph.nodes { targets.push(Target::ProcessOccurrence(OccurrenceEndpoint { references: vec![], occurrence: node.id, })); targets.extend( node.preview .iter() .map(|(endpoint, _)| Target::ProcessOccurrence(endpoint.clone())), ); for link in &node.preview_links { let target = Target::ProcessPreviewLink { relation: link.relation, source: link.source.clone(), target: link.target.clone(), }; if graph.usage_bounds(&target).is_some() { targets.push(target); } } } targets.extend(graph.edges.iter().map(|edge| Target::ProcessLink(edge.id))); targets } pub(crate) fn usage_snapshot(&self) -> UsageSnapshot { UsageSnapshot { identity: self.identity, expanded: self.expanded.clone(), contextual_expanded: self.contextual_expanded.clone(), } } } fn owner_exists(p: &Project, owner: Id) -> bool { p.elements .get(&owner) .is_some_and(|e| matches!(e.kind(), Kind::Process | Kind::FunctionalChain)) } fn occurrence_role(p: &Project, occurrence: Id, subject: Id) -> Option<&'static str> { let e = p.elements.get(&occurrence)?; if occurrence == subject { return Some("contribution"); } match e.shared()? { SharedBody::Occurrence { body: OccurrenceBody::Action { action: Some(id), .. }, .. } if *id == subject => Some("action occurrence"), SharedBody::Occurrence { body: OccurrenceBody::Exchange { exchange: Some(id), .. }, .. } if *id == subject => Some("exchange occurrence"), SharedBody::Occurrence { body: OccurrenceBody::SequenceReference { sequence: Some(id) }, .. } if *id == subject => Some("reused process / chain"), _ => None, } } impl Graph<'_> { fn usage_bounds(&self, target: &Target) -> Option { match target { Target::Whole => Some(Rectangle::new(Point::ORIGIN, self.size)), Target::ProcessOccurrence(endpoint) => { let root = endpoint .references .first() .copied() .unwrap_or(endpoint.occurrence); let node = self.nodes.iter().find(|node| node.id == root)?; if endpoint.references.is_empty() { Some(node.rect) } else { // endpoint_rect intentionally has a proxy fallback for other callers; // a usage promises an actual tile and must never take that fallback. node.preview .iter() .any(|(shown, _)| shown == endpoint) .then(|| node.endpoint_rect(endpoint)) } } Target::ProcessLink(id) => { let edge = self.edges.iter().find(|edge| edge.id == *id)?; if !edge.guard.is_empty() { Some(edge.label) } else { let segment = edge .points .windows(2) .nth(edge.points.len().saturating_sub(1) / 2)?; let center = Point::new( (segment[0].x + segment[1].x) / 2., (segment[0].y + segment[1].y) / 2., ); Some(Rectangle::new( center - iced::Vector::new(16., 16.), Size::new(32., 32.), )) } } Target::ProcessPreviewLink { relation, source, target, } => { let node = self .nodes .iter() .find(|node| source.references.first() == Some(&node.id))?; let link = node.preview_links.iter().find(|link| { link.relation == *relation && link.source == *source && link.target == *target })?; let [a, b] = node.preview_link_visible_segment(link)?; Some(Rectangle::new( Point::new((a.x + b.x) / 2. - 16., (a.y + b.y) / 2. - 16.), Size::new(32., 32.), )) } _ => None, } } } impl State { /// Inactive owners use the same default preview policy as entering their canvas. fn usage_graph<'a>(&self, p: &'a Project, owner: Id) -> Graph<'a> { if self.identity.is_some_and(|(_, current)| current == owner) { Graph::expanded_qualified(p, owner, owner, &self.expanded, &self.contextual_expanded) } else { Graph::new(p, owner, owner) } } pub(crate) fn diagram_usages(&self, p: &Project, owner: Id, subject: Id) -> Vec { if !owner_exists(p, owner) || !(p.elements.contains_key(&subject) || p.relations.contains_key(&subject)) { return vec![]; } let graph = self.usage_graph(p, owner); let mut usages = vec![]; let mut add = |target: Target, role: &str| { if let Some(bounds) = graph.usage_bounds(&target) { usages.push(Usage { subject, surface: Surface::Process(owner), target, role: role.into(), bounds, }); } }; if subject == owner { add(Target::Whole, "process / chain canvas"); } for node in &graph.nodes { if let Some(role) = occurrence_role(p, node.id, subject) { add( Target::ProcessOccurrence(OccurrenceEndpoint { references: vec![], occurrence: node.id, }), role, ); } for (endpoint, _) in &node.preview { if let Some(role) = occurrence_role(p, endpoint.occurrence, subject) { add( Target::ProcessOccurrence(endpoint.clone()), &format!("reused {role}"), ); } } for link in &node.preview_links { if link.relation == subject { add( Target::ProcessPreviewLink { relation: link.relation, source: link.source.clone(), target: link.target.clone(), }, "reused sequence link", ); } } } for edge in &graph.edges { let role = if edge.id == subject { Some("process link") } else { match p.relations.get(&edge.id).map(|r| &r.body) { Some(RelationBody::ContextualLink { purpose: ContextualLinkPurpose::Exchange { exchange }, .. }) if *exchange == subject => Some("assembly exchange"), Some(RelationBody::ContextualLink { purpose: ContextualLinkPurpose::CommonWork { work }, .. }) if *work == subject => Some("common work"), _ => None, } }; if let Some(role) = role { add(Target::ProcessLink(edge.id), role); } } usages } /// Returns success, not whether the camera changed. App must separately guard /// its other drafts and revalidate the queried subject's current usage. pub(crate) fn reveal_usage( &mut self, p: &Project, document: Id, owner: Id, target: &Target, ) -> bool { if !owner_exists(p, owner) || self.draft.is_some() { return false; } let same = self.identity == Some((document, owner)); let graph = if same { self.usage_graph(p, owner) } else { Graph::new(p, owner, owner) }; let Some(bounds) = graph.usage_bounds(target) else { return false; }; let address = if let Target::ProcessOccurrence(endpoint) = target { let Ok(resolved) = oa_core::topology_context::TopologyIndex::new(p).resolve(owner, endpoint) else { return false; }; (!endpoint.references.is_empty()).then_some(resolved.address) } else { None }; let viewport = self.viewport.unwrap_or(Size::new(500., 300.)); self.enter(document, owner); if !same { self.contextual_expanded.clear(); } self.selected_context = address; self.tool = CanvasTool::Select; self.viewport = Some(viewport); let fit = ((viewport.width - 48.).max(1.) / bounds.width.max(1.)) .min((viewport.height - 48.).max(1.) / bounds.height.max(1.)); let scale = if matches!(target, Target::Whole) { fit.clamp(0.0001, 1.) } else { fit.clamp(0.6, 1.25) }; self.zoom = Some(scale); self.fit = false; self.pan = iced::Vector::new( viewport.width / 2. - bounds.center_x() * scale, viewport.height / 2. - bounds.center_y() * scale, ); true } } #[cfg(test)] mod tests;