Preserve the full-desktop-1 captured source. Qualified previews inspect their exact subject read-only; group movement preserves source order and other columns. Native acceptance and strict-lint payload-size corrections follow separately.
307 lines
11 KiB
Rust
307 lines
11 KiB
Rust
//! 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<Id>,
|
|
contextual_expanded: BTreeSet<OccurrenceEndpoint>,
|
|
}
|
|
|
|
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<oa_core::topology_context::ContextAddress> {
|
|
self.selected_context.clone()
|
|
}
|
|
|
|
pub(crate) fn selection_targets(&self, p: &Project, owner: Id) -> Vec<Target> {
|
|
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<Rectangle> {
|
|
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<Usage> {
|
|
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;
|