Combine source navigation and alias rename with canvas ownership guards
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
//! 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 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;
|
||||
@@ -0,0 +1,439 @@
|
||||
use super::*;
|
||||
|
||||
fn add(p: &mut Project, owner: Id, name: &str, body: OccurrenceBody) -> Id {
|
||||
let e = occurrence(p, owner, name, body);
|
||||
let id = e.id;
|
||||
*p = execute_batch(p, &[Command::CreateElement(e)]).unwrap();
|
||||
id
|
||||
}
|
||||
|
||||
fn reused_fixture() -> (Project, Id, Id, Id, [Id; 2], Vec<Id>) {
|
||||
let f = super::super::direct_tests::fixture(Layer::Oa);
|
||||
let mut p = f.p;
|
||||
let sequence = p
|
||||
.make_element(Kind::Process, Layer::Oa, "Reviewed checks", None)
|
||||
.unwrap();
|
||||
let sequence_id = sequence.id;
|
||||
p = execute_batch(&p, &[Command::CreateElement(sequence)]).unwrap();
|
||||
let mut leaves = vec![];
|
||||
for name in [
|
||||
"Check one",
|
||||
"Check two",
|
||||
"Check three",
|
||||
"Check four",
|
||||
"Check five",
|
||||
] {
|
||||
leaves.push(add(
|
||||
&mut p,
|
||||
sequence_id,
|
||||
name,
|
||||
OccurrenceBody::Action {
|
||||
action: Some(f.work[0]),
|
||||
lifeline: None,
|
||||
},
|
||||
));
|
||||
}
|
||||
let first = add(
|
||||
&mut p,
|
||||
f.owner,
|
||||
"First invocation",
|
||||
OccurrenceBody::SequenceReference {
|
||||
sequence: Some(sequence_id),
|
||||
},
|
||||
);
|
||||
let second = add(
|
||||
&mut p,
|
||||
f.owner,
|
||||
"Second invocation",
|
||||
OccurrenceBody::SequenceReference {
|
||||
sequence: Some(sequence_id),
|
||||
},
|
||||
);
|
||||
(p, f.owner, sequence_id, f.work[0], [first, second], leaves)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_usages_exclude_endpoint_coincidence_and_keep_actual_occurrences_and_links() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let f = super::super::direct_tests::fixture(layer);
|
||||
let mut p = f.p;
|
||||
let state = State::default();
|
||||
// Both exchange endpoints have Action contributions, but there is no
|
||||
// Exchange occurrence and no exchange arrow in this process.
|
||||
assert!(state.diagram_usages(&p, f.owner, f.exchange).is_empty());
|
||||
let direct = state.diagram_usages(&p, f.owner, f.work[0]);
|
||||
assert_eq!(direct.len(), 1);
|
||||
assert_eq!(
|
||||
direct[0].target,
|
||||
Target::ProcessOccurrence(OccurrenceEndpoint {
|
||||
references: vec![],
|
||||
occurrence: f.nodes[0]
|
||||
})
|
||||
);
|
||||
let link = state.diagram_usages(&p, f.owner, f.link);
|
||||
assert_eq!(link.len(), 1);
|
||||
assert_eq!(link[0].target, Target::ProcessLink(f.link));
|
||||
let first = add(
|
||||
&mut p,
|
||||
f.owner,
|
||||
"First exchange",
|
||||
OccurrenceBody::Exchange {
|
||||
exchange: Some(f.exchange),
|
||||
sender: None,
|
||||
receiver: None,
|
||||
message_kind: MessageKind::Asynchronous,
|
||||
reply_to: None,
|
||||
values: vec![],
|
||||
operation: None,
|
||||
},
|
||||
);
|
||||
let second = add(
|
||||
&mut p,
|
||||
f.owner,
|
||||
"Second exchange",
|
||||
OccurrenceBody::Exchange {
|
||||
exchange: Some(f.exchange),
|
||||
sender: None,
|
||||
receiver: None,
|
||||
message_kind: MessageKind::Asynchronous,
|
||||
reply_to: None,
|
||||
values: vec![],
|
||||
operation: None,
|
||||
},
|
||||
);
|
||||
let usages = state.diagram_usages(&p, f.owner, f.exchange);
|
||||
assert_eq!(usages.len(), 2);
|
||||
assert_eq!(
|
||||
usages
|
||||
.iter()
|
||||
.map(|usage| usage.target.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
Target::ProcessOccurrence(OccurrenceEndpoint {
|
||||
references: vec![],
|
||||
occurrence: first
|
||||
}),
|
||||
Target::ProcessOccurrence(OccurrenceEndpoint {
|
||||
references: vec![],
|
||||
occurrence: second
|
||||
}),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_usages_keep_caller_paths_and_exclude_unshown_fifth_preview() {
|
||||
let (p, owner, sequence, work, calls, leaves) = reused_fixture();
|
||||
let before = p.clone();
|
||||
let mut state = State::default();
|
||||
let leaf = state.diagram_usages(&p, owner, leaves[0]);
|
||||
assert_eq!(leaf.len(), 2);
|
||||
for (usage, call) in leaf.iter().zip(calls) {
|
||||
assert_eq!(
|
||||
usage.target,
|
||||
Target::ProcessOccurrence(OccurrenceEndpoint {
|
||||
references: vec![call],
|
||||
occurrence: leaves[0]
|
||||
})
|
||||
);
|
||||
}
|
||||
assert_ne!(leaf[0].bounds, leaf[1].bounds);
|
||||
assert!(state.diagram_usages(&p, owner, leaves[4]).is_empty());
|
||||
assert_eq!(state.diagram_usages(&p, owner, sequence).len(), 2);
|
||||
// One local action and four tiles in each of two caller boxes.
|
||||
assert_eq!(state.diagram_usages(&p, owner, work).len(), 9);
|
||||
state.enter(Id::new_v4(), owner);
|
||||
assert!(state.expand(&p, owner, calls[0]));
|
||||
let fifth = state.diagram_usages(&p, owner, leaves[4]);
|
||||
assert_eq!(fifth.len(), 1);
|
||||
assert_eq!(
|
||||
fifth[0].target,
|
||||
Target::ProcessOccurrence(OccurrenceEndpoint {
|
||||
references: vec![calls[0]],
|
||||
occurrence: leaves[4]
|
||||
})
|
||||
);
|
||||
// Another active owner must not leak its expansion to this queried canvas.
|
||||
state.enter(Id::new_v4(), sequence);
|
||||
assert!(state.diagram_usages(&p, owner, leaves[4]).is_empty());
|
||||
assert_eq!(p, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_usage_reveal_validates_before_state_changes_and_centers_the_exact_tile() {
|
||||
let (p, owner, _, _, calls, leaves) = reused_fixture();
|
||||
let before = p.clone();
|
||||
let document = Id::new_v4();
|
||||
let mut state = State::default();
|
||||
state.enter(document, owner);
|
||||
state.viewport(Size::new(880., 460.));
|
||||
state.pan = iced::Vector::new(-3000., -900.);
|
||||
let hidden = Target::ProcessOccurrence(OccurrenceEndpoint {
|
||||
references: vec![calls[0]],
|
||||
occurrence: leaves[4],
|
||||
});
|
||||
let prior = (
|
||||
state.identity,
|
||||
state.pan,
|
||||
state.zoom,
|
||||
state.fit,
|
||||
state.selected_context.clone(),
|
||||
);
|
||||
assert!(!state.reveal_usage(&p, document, owner, &hidden));
|
||||
assert_eq!(
|
||||
(
|
||||
state.identity,
|
||||
state.pan,
|
||||
state.zoom,
|
||||
state.fit,
|
||||
state.selected_context.clone()
|
||||
),
|
||||
prior
|
||||
);
|
||||
assert!(!state.reveal_usage(&p, document, Id::new_v4(), &Target::Whole));
|
||||
assert_eq!(
|
||||
(
|
||||
state.identity,
|
||||
state.pan,
|
||||
state.zoom,
|
||||
state.fit,
|
||||
state.selected_context.clone()
|
||||
),
|
||||
prior
|
||||
);
|
||||
let endpoint = OccurrenceEndpoint {
|
||||
references: vec![calls[1]],
|
||||
occurrence: leaves[0],
|
||||
};
|
||||
let target = Target::ProcessOccurrence(endpoint.clone());
|
||||
assert!(state.reveal_usage(&p, document, owner, &target));
|
||||
assert_eq!(state.selected_context.as_ref().unwrap().endpoint, endpoint);
|
||||
let bounds = state
|
||||
.diagram_usages(&p, owner, leaves[0])
|
||||
.into_iter()
|
||||
.find(|usage| usage.target == target)
|
||||
.unwrap()
|
||||
.bounds;
|
||||
let scale = state.zoom.unwrap();
|
||||
assert!(scale >= 0.6);
|
||||
assert!((bounds.center_x() * scale + state.pan.x - 440.).abs() < 0.01);
|
||||
assert!((bounds.center_y() * scale + state.pan.y - 230.).abs() < 0.01);
|
||||
assert!(state.reveal_usage(&p, document, owner, &Target::Whole));
|
||||
assert!(state.selected_context.is_none());
|
||||
state
|
||||
.update(&p, Event::Start(owner, Tool::Work, None))
|
||||
.unwrap();
|
||||
let draft = state.recovery_snapshot().unwrap();
|
||||
let camera = (state.pan, state.zoom, state.fit);
|
||||
assert!(!state.reveal_usage(&p, document, owner, &target));
|
||||
assert_eq!(state.recovery_snapshot().unwrap(), draft);
|
||||
assert_eq!((state.pan, state.zoom, state.fit), camera);
|
||||
assert_eq!(p, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_contextual_purpose_and_painted_preview_links_keep_distinct_identities() {
|
||||
let (mut p, owner, sequence, work, calls, leaves) = reused_fixture();
|
||||
let source = OccurrenceEndpoint {
|
||||
references: vec![calls[0]],
|
||||
occurrence: leaves[0],
|
||||
};
|
||||
let target = OccurrenceEndpoint {
|
||||
references: vec![calls[1]],
|
||||
occurrence: leaves[0],
|
||||
};
|
||||
let relation = Relation {
|
||||
id: Id::new_v4(),
|
||||
alias: "same_check".into(),
|
||||
name: "One common check".into(),
|
||||
description: String::new(),
|
||||
body: RelationBody::ContextualLink {
|
||||
owner,
|
||||
source,
|
||||
target,
|
||||
purpose: ContextualLinkPurpose::CommonWork { work },
|
||||
},
|
||||
};
|
||||
let contextual = relation.id;
|
||||
p = execute_batch(&p, &[Command::CreateRelation(relation)]).unwrap();
|
||||
let Command::CreateRelation(relation) = link(&p, leaves[0], leaves[1], "next") else {
|
||||
unreachable!()
|
||||
};
|
||||
let local_link = relation.id;
|
||||
p = execute_batch(&p, &[Command::CreateRelation(relation)]).unwrap();
|
||||
let state = State::default();
|
||||
assert!(
|
||||
state
|
||||
.diagram_usages(&p, owner, work)
|
||||
.iter()
|
||||
.any(|usage| usage.target == Target::ProcessLink(contextual)
|
||||
&& usage.role == "common work")
|
||||
);
|
||||
assert_eq!(state.diagram_usages(&p, owner, contextual).len(), 1);
|
||||
let previews = state.diagram_usages(&p, owner, local_link);
|
||||
assert_eq!(previews.len(), 2);
|
||||
for (usage, call) in previews.iter().zip(calls) {
|
||||
assert_eq!(
|
||||
usage.target,
|
||||
Target::ProcessPreviewLink {
|
||||
relation: local_link,
|
||||
source: OccurrenceEndpoint {
|
||||
references: vec![call],
|
||||
occurrence: leaves[0]
|
||||
},
|
||||
target: OccurrenceEndpoint {
|
||||
references: vec![call],
|
||||
occurrence: leaves[1]
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
assert_eq!(state.diagram_usages(&p, sequence, local_link).len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preview_arrow_focus_is_in_the_painted_gap_and_hidden_endpoints_never_fallback() {
|
||||
let (mut p, owner, _, _, calls, leaves) = reused_fixture();
|
||||
let Command::CreateRelation(shown) = link(&p, leaves[0], leaves[1], "next") else {
|
||||
unreachable!()
|
||||
};
|
||||
let shown_id = shown.id;
|
||||
p = execute_batch(&p, &[Command::CreateRelation(shown)]).unwrap();
|
||||
let Command::CreateRelation(hidden) = link(&p, leaves[0], leaves[4], "later") else {
|
||||
unreachable!()
|
||||
};
|
||||
let hidden_id = hidden.id;
|
||||
p = execute_batch(&p, &[Command::CreateRelation(hidden)]).unwrap();
|
||||
let before = p.clone();
|
||||
let document = Id::new_v4();
|
||||
let mut state = State::default();
|
||||
state.enter(document, owner);
|
||||
state.viewport(Size::new(800., 460.));
|
||||
let rows = state.diagram_usages(&p, owner, shown_id);
|
||||
assert_eq!(rows.len(), 2);
|
||||
let graph = Graph::new(&p, owner, owner);
|
||||
for (row, call) in rows.iter().zip(calls) {
|
||||
let node = graph.nodes.iter().find(|n| n.id == call).unwrap();
|
||||
// The painter places 113px tiles 125px apart, beginning 12px in.
|
||||
// The 12px gap is centered 131px into the caller, 134px below its top.
|
||||
assert_eq!(
|
||||
row.bounds.center(),
|
||||
Point::new(node.rect.x + 131., node.rect.y + 134.)
|
||||
);
|
||||
assert!(
|
||||
node.preview
|
||||
.iter()
|
||||
.all(|(e, _)| !node.endpoint_rect(e).contains(row.bounds.center()))
|
||||
);
|
||||
}
|
||||
assert_ne!(rows[0].bounds, rows[1].bounds);
|
||||
assert!(state.diagram_usages(&p, owner, hidden_id).is_empty());
|
||||
assert!(state.reveal_usage(&p, document, owner, &rows[1].target));
|
||||
assert!(
|
||||
state.selected_context.is_none(),
|
||||
"an arrow is not an occurrence authoring context"
|
||||
);
|
||||
let zoom = state.zoom.unwrap();
|
||||
assert!((rows[1].bounds.center_x() * zoom + state.pan.x - 400.).abs() < 0.01);
|
||||
assert!((rows[1].bounds.center_y() * zoom + state.pan.y - 230.).abs() < 0.01);
|
||||
assert!(state.expand(&p, owner, calls[0]));
|
||||
let expanded = state.diagram_usages(&p, owner, hidden_id);
|
||||
assert_eq!(expanded.len(), 1);
|
||||
assert!(
|
||||
matches!(&expanded[0].target, Target::ProcessPreviewLink { source, .. }
|
||||
if source.references == vec![calls[0]])
|
||||
);
|
||||
let mut stale = rows[1].target.clone();
|
||||
if let Target::ProcessPreviewLink { source, .. } = &mut stale {
|
||||
source.references = vec![Id::new_v4()];
|
||||
}
|
||||
let prior = (state.pan, state.zoom, state.fit);
|
||||
assert!(!state.reveal_usage(&p, document, owner, &stale));
|
||||
assert_eq!((state.pan, state.zoom, state.fit), prior);
|
||||
assert_eq!(p, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn visible_preview_segment_excludes_all_opaque_tiles_for_reverse_and_cross_row_arrows() {
|
||||
let tiles = [
|
||||
Rectangle::new(Point::new(12., 118.), Size::new(113., 32.)),
|
||||
Rectangle::new(Point::new(137., 118.), Size::new(113., 32.)),
|
||||
Rectangle::new(Point::new(12., 162.), Size::new(113., 32.)),
|
||||
Rectangle::new(Point::new(137., 162.), Size::new(113., 32.)),
|
||||
];
|
||||
for points in [
|
||||
[Point::new(250., 134.), Point::new(12., 134.)],
|
||||
[Point::new(125., 134.), Point::new(137., 178.)],
|
||||
[Point::new(250., 178.), Point::new(12., 134.)],
|
||||
] {
|
||||
let [a, b] = visible_preview_segment(points, tiles.into_iter()).unwrap();
|
||||
for fraction in [0.1, 0.5, 0.9] {
|
||||
let point = a + (b - a) * fraction;
|
||||
assert!(tiles.iter().all(|tile| !tile.contains(point)));
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
visible_preview_segment(
|
||||
[Point::new(30., 134.), Point::new(100., 134.)],
|
||||
tiles.into_iter()
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
assert!(
|
||||
visible_preview_segment([Point::new(1., 1.), Point::new(1., 1.)], tiles.into_iter())
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expanded_nested_tiles_do_not_invent_links_the_preview_painter_does_not_emit() {
|
||||
let (mut p, owner, sequence, work, calls, _) = reused_fixture();
|
||||
let nested = p
|
||||
.make_element(Kind::Process, Layer::Oa, "Nested checks", None)
|
||||
.unwrap();
|
||||
let nested_id = nested.id;
|
||||
p = execute_batch(&p, &[Command::CreateElement(nested)]).unwrap();
|
||||
let leaves: Vec<_> = ["Nested one", "Nested two"]
|
||||
.into_iter()
|
||||
.map(|name| {
|
||||
add(
|
||||
&mut p,
|
||||
nested_id,
|
||||
name,
|
||||
OccurrenceBody::Action {
|
||||
action: Some(work),
|
||||
lifeline: None,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let Command::CreateRelation(relation) = link(&p, leaves[0], leaves[1], "next") else {
|
||||
unreachable!()
|
||||
};
|
||||
let relation_id = relation.id;
|
||||
p = execute_batch(&p, &[Command::CreateRelation(relation)]).unwrap();
|
||||
let reference = add(
|
||||
&mut p,
|
||||
sequence,
|
||||
"Nested invocation",
|
||||
OccurrenceBody::SequenceReference {
|
||||
sequence: Some(nested_id),
|
||||
},
|
||||
);
|
||||
let mut state = State::default();
|
||||
state.enter(Id::new_v4(), owner);
|
||||
state.expanded.insert(calls[0]);
|
||||
state.contextual_expanded.insert(OccurrenceEndpoint {
|
||||
references: vec![calls[0]],
|
||||
occurrence: reference,
|
||||
});
|
||||
let shown = state.diagram_usages(&p, owner, leaves[0]);
|
||||
assert_eq!(shown.len(), 1);
|
||||
assert!(
|
||||
matches!(&shown[0].target, Target::ProcessOccurrence(endpoint)
|
||||
if endpoint.references == vec![calls[0], reference])
|
||||
);
|
||||
assert!(state.diagram_usages(&p, owner, relation_id).is_empty());
|
||||
assert_eq!(state.diagram_usages(&p, nested_id, relation_id).len(), 1);
|
||||
}
|
||||
@@ -183,6 +183,32 @@ pub(super) fn view<'a>(app: &'a App, owner: Id, out: Column<'a, Message>) -> Ele
|
||||
&state.expanded,
|
||||
&state.contextual_expanded,
|
||||
);
|
||||
graph.selected_preview = state
|
||||
.qualified_selection(p, owner)
|
||||
.filter(|address| {
|
||||
address
|
||||
.endpoint
|
||||
.references
|
||||
.first()
|
||||
.copied()
|
||||
.unwrap_or(address.endpoint.occurrence)
|
||||
== app.selected
|
||||
})
|
||||
.map(|address| address.endpoint.clone());
|
||||
graph.selected_preview_link = app
|
||||
.diagram_focus(crate::diagram_usage::Surface::Process(owner))
|
||||
.and_then(|usage| match &usage.target {
|
||||
crate::diagram_usage::Target::ProcessPreviewLink {
|
||||
relation,
|
||||
source,
|
||||
target,
|
||||
} => Some(PreviewLink {
|
||||
relation: *relation,
|
||||
source: source.clone(),
|
||||
target: target.clone(),
|
||||
}),
|
||||
_ => None,
|
||||
});
|
||||
graph.scale = state.scale(graph.size, (app.window_size.width - 600.).max(200.));
|
||||
graph.camera_pan = state.pan;
|
||||
graph.pending_insertion = state
|
||||
|
||||
Reference in New Issue
Block a user