Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a63654f782 |
@@ -349,6 +349,7 @@ fn reference_field(key: &str) -> bool {
|
|||||||
| "sa"
|
| "sa"
|
||||||
| "diagram"
|
| "diagram"
|
||||||
| "edge"
|
| "edge"
|
||||||
|
| "machine"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
fn enum_field(key: &str) -> bool {
|
fn enum_field(key: &str) -> bool {
|
||||||
|
|||||||
@@ -1190,6 +1190,7 @@ impl RelationBody {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
pub enum ProjectionKind {
|
pub enum ProjectionKind {
|
||||||
Overview,
|
Overview,
|
||||||
Decomposition,
|
Decomposition,
|
||||||
@@ -1197,10 +1198,21 @@ pub enum ProjectionKind {
|
|||||||
Traceability,
|
Traceability,
|
||||||
FunctionalDataflow,
|
FunctionalDataflow,
|
||||||
Capabilities,
|
Capabilities,
|
||||||
|
StateMachine { machine: Id },
|
||||||
}
|
}
|
||||||
impl ProjectionKind {
|
impl ProjectionKind {
|
||||||
|
pub fn state_machine(self) -> Option<Id> {
|
||||||
|
match self {
|
||||||
|
Self::StateMachine { machine } => Some(machine),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn includes(self, kind: Kind) -> bool {
|
pub fn includes(self, kind: Kind) -> bool {
|
||||||
match self {
|
match self {
|
||||||
|
Self::StateMachine { .. } => {
|
||||||
|
matches!(kind, Kind::StateMachine | Kind::Region) || kind.is_state()
|
||||||
|
}
|
||||||
Self::Allocation => kind.is_performer() || kind.is_work(),
|
Self::Allocation => kind.is_performer() || kind.is_work(),
|
||||||
Self::FunctionalDataflow => kind.is_work(),
|
Self::FunctionalDataflow => kind.is_work(),
|
||||||
Self::Capabilities => {
|
Self::Capabilities => {
|
||||||
@@ -1221,6 +1233,7 @@ impl ProjectionKind {
|
|||||||
impl std::fmt::Display for ProjectionKind {
|
impl std::fmt::Display for ProjectionKind {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.write_str(match self {
|
f.write_str(match self {
|
||||||
|
Self::StateMachine { .. } => "State machine",
|
||||||
Self::Allocation => "Architecture · allocated work",
|
Self::Allocation => "Architecture · allocated work",
|
||||||
Self::FunctionalDataflow => "Functional dataflow",
|
Self::FunctionalDataflow => "Functional dataflow",
|
||||||
Self::Capabilities => "Missions & capabilities",
|
Self::Capabilities => "Missions & capabilities",
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
use crate::*;
|
use crate::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
#[path = "projection/state.rs"]
|
||||||
|
mod state;
|
||||||
|
pub(crate) use state::node_size as state_node_size;
|
||||||
|
pub use state::{state_header_height, state_machine_contains, state_symbol_size};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct ProjectedNode {
|
pub struct ProjectedNode {
|
||||||
@@ -106,9 +110,13 @@ pub(crate) fn parents(p: &Project, d: &Diagram) -> BTreeMap<Id, Option<Id>> {
|
|||||||
.nodes
|
.nodes
|
||||||
.values()
|
.values()
|
||||||
.filter(|n| {
|
.filter(|n| {
|
||||||
p.elements
|
p.elements.get(&n.element).is_some_and(|e| {
|
||||||
.get(&n.element)
|
e.kind() != Kind::Port
|
||||||
.is_some_and(|e| e.kind() != Kind::Port && d.projection.includes(e.kind()))
|
&& d.projection.includes(e.kind())
|
||||||
|
&& d.projection
|
||||||
|
.state_machine()
|
||||||
|
.is_none_or(|machine| state_machine_contains(p, machine, e.id))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.map(|n| (n.element, n.id))
|
.map(|n| (n.element, n.id))
|
||||||
.collect();
|
.collect();
|
||||||
@@ -218,6 +226,11 @@ pub fn translated_routes(
|
|||||||
let (source, target) = edge_endpoint_placements(p, d, route.edge)?;
|
let (source, target) = edge_endpoint_placements(p, d, route.edge)?;
|
||||||
let source_delta = deltas.get(&source)?;
|
let source_delta = deltas.get(&source)?;
|
||||||
let target_delta = deltas.get(&target)?;
|
let target_delta = deltas.get(&target)?;
|
||||||
|
if d.projection.state_machine().is_some()
|
||||||
|
&& !state::boundary_moves_with(p, d, route.edge, source_delta, deltas)
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
if source_delta != target_delta
|
if source_delta != target_delta
|
||||||
|| (source_delta.x == 0. && source_delta.y == 0.)
|
|| (source_delta.x == 0. && source_delta.y == 0.)
|
||||||
|| route.waypoints.is_empty()
|
|| route.waypoints.is_empty()
|
||||||
@@ -248,7 +261,12 @@ pub fn project_diagram(p: &Project, d: &Diagram) -> ProjectedView {
|
|||||||
let Some(e) = p.elements.get(&n.element) else {
|
let Some(e) = p.elements.get(&n.element) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if e.kind() == Kind::Port || !d.projection.includes(e.kind()) {
|
if e.kind() == Kind::Port
|
||||||
|
|| !d.projection.includes(e.kind())
|
||||||
|
|| d.projection
|
||||||
|
.state_machine()
|
||||||
|
.is_some_and(|machine| !state_machine_contains(p, machine, e.id))
|
||||||
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if d.projection != ProjectionKind::Traceability && e.layer() != d.layer {
|
if d.projection != ProjectionKind::Traceability && e.layer() != d.layer {
|
||||||
@@ -282,6 +300,11 @@ pub fn project_diagram(p: &Project, d: &Diagram) -> ProjectedView {
|
|||||||
n.size
|
n.size
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
let size = if d.projection.state_machine().is_some() {
|
||||||
|
state::node_size(p, e, size, n.collapsed)
|
||||||
|
} else {
|
||||||
|
size
|
||||||
|
};
|
||||||
nodes.push(ProjectedNode {
|
nodes.push(ProjectedNode {
|
||||||
id: n.id,
|
id: n.id,
|
||||||
element: n.element,
|
element: n.element,
|
||||||
@@ -302,11 +325,25 @@ pub fn project_diagram(p: &Project, d: &Diagram) -> ProjectedView {
|
|||||||
nodes.sort_by_key(|n| (n.depth, n.id));
|
nodes.sort_by_key(|n| (n.depth, n.id));
|
||||||
for index in (0..nodes.len()).rev() {
|
for index in (0..nodes.len()).rev() {
|
||||||
let child = nodes[index].clone();
|
let child = nodes[index].clone();
|
||||||
|
// Boundary vertices are pinned to their containing State/Machine after
|
||||||
|
// containment has been measured. They must not expand that boundary.
|
||||||
|
if d.projection.state_machine().is_some()
|
||||||
|
&& p.elements
|
||||||
|
.get(&child.element)
|
||||||
|
.is_some_and(|e| matches!(e.kind(), Kind::EntryPoint | Kind::ExitPoint))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if let Some(parent) = child
|
if let Some(parent) = child
|
||||||
.visual_parent
|
.visual_parent
|
||||||
.and_then(|id| nodes.iter().position(|n| n.id == id))
|
.and_then(|id| nodes.iter().position(|n| n.id == id))
|
||||||
{
|
{
|
||||||
let footer = port_footer(p, nodes[parent].element);
|
let footer = port_footer(p, nodes[parent].element);
|
||||||
|
let header = if d.projection.state_machine().is_some() {
|
||||||
|
state_header_height(p, nodes[parent].element)
|
||||||
|
} else {
|
||||||
|
64.
|
||||||
|
};
|
||||||
let p = &mut nodes[parent];
|
let p = &mut nodes[parent];
|
||||||
let right =
|
let right =
|
||||||
(p.position.x + p.size.width).max(child.position.x + child.size.width + 24.);
|
(p.position.x + p.size.width).max(child.position.x + child.size.width + 24.);
|
||||||
@@ -314,11 +351,14 @@ pub fn project_diagram(p: &Project, d: &Diagram) -> ProjectedView {
|
|||||||
(p.position.y + p.size.height).max(child.position.y + child.size.height + footer);
|
(p.position.y + p.size.height).max(child.position.y + child.size.height + footer);
|
||||||
p.position.x = p.position.x.min(child.position.x - 24.);
|
p.position.x = p.position.x.min(child.position.x - 24.);
|
||||||
// Reserve two readable header lines above the first nested card.
|
// Reserve two readable header lines above the first nested card.
|
||||||
p.position.y = p.position.y.min(child.position.y - 64.);
|
p.position.y = p.position.y.min(child.position.y - header);
|
||||||
p.size.width = right - p.position.x;
|
p.size.width = right - p.position.x;
|
||||||
p.size.height = bottom - p.position.y;
|
p.size.height = bottom - p.position.y;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if d.projection.state_machine().is_some() {
|
||||||
|
state::snap_boundaries(p, &mut nodes);
|
||||||
|
}
|
||||||
let visible: BTreeSet<_> = nodes.iter().map(|n| n.id).collect();
|
let visible: BTreeSet<_> = nodes.iter().map(|n| n.id).collect();
|
||||||
let placed: BTreeMap<_, _> = d.nodes.values().map(|n| (n.element, n.id)).collect();
|
let placed: BTreeMap<_, _> = d.nodes.values().map(|n| (n.element, n.id)).collect();
|
||||||
let anchor = |element: Id| {
|
let anchor = |element: Id| {
|
||||||
@@ -397,6 +437,11 @@ pub fn project_diagram(p: &Project, d: &Diagram) -> ProjectedView {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let included = match d.projection {
|
let included = match d.projection {
|
||||||
|
ProjectionKind::StateMachine { machine } => matches!(&r.body,
|
||||||
|
RelationBody::StateTransition { transition_kind, .. }
|
||||||
|
if *transition_kind != TransitionKind::Internal
|
||||||
|
&& state_machine_contains(p, machine, r.source())
|
||||||
|
&& state_machine_contains(p, machine, r.target())),
|
||||||
ProjectionKind::Allocation => matches!(
|
ProjectionKind::Allocation => matches!(
|
||||||
r.kind(),
|
r.kind(),
|
||||||
RelationKind::Interaction
|
RelationKind::Interaction
|
||||||
@@ -422,7 +467,12 @@ pub fn project_diagram(p: &Project, d: &Diagram) -> ProjectedView {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let (Some(source), Some(target)) = (anchor(r.source()), anchor(r.target()))
|
if let (Some(source), Some(target)) = (anchor(r.source()), anchor(r.target()))
|
||||||
&& source != target
|
&& (source != target
|
||||||
|
|| (d.projection.state_machine().is_some()
|
||||||
|
&& r.source() == r.target()
|
||||||
|
&& nodes
|
||||||
|
.iter()
|
||||||
|
.any(|n| n.id == source && n.element == r.source())))
|
||||||
{
|
{
|
||||||
let (source_port, target_port) = match &r.body {
|
let (source_port, target_port) = match &r.body {
|
||||||
RelationBody::FunctionalExchange {
|
RelationBody::FunctionalExchange {
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
//! State-specific presentation policies over the shared persisted placements.
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Membership of a dedicated state view. Other projection kinds may retain
|
||||||
|
/// unrelated hidden placements and must not acquire this additional restriction.
|
||||||
|
pub fn state_machine_contains(p: &Project, machine: Id, element: Id) -> bool {
|
||||||
|
let Some(owner) = p
|
||||||
|
.elements
|
||||||
|
.get(&machine)
|
||||||
|
.filter(|e| e.kind() == Kind::StateMachine)
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
p.elements.get(&element).is_some_and(|e| {
|
||||||
|
e.layer() == owner.layer()
|
||||||
|
&& (matches!(e.kind(), Kind::StateMachine | Kind::Region) || e.kind().is_state())
|
||||||
|
&& validation::ancestor_of_kind(p, e.id, Kind::StateMachine) == Some(machine)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fixed body bounds for UML-style state symbols. Their authored position is
|
||||||
|
/// the top-left of this body; labels and larger pointer targets are derived UI.
|
||||||
|
/// A State or Mode uses its authored rectangular size instead.
|
||||||
|
pub fn state_symbol_size(kind: Kind) -> Option<Size> {
|
||||||
|
let (width, height) = match kind {
|
||||||
|
Kind::Initial => (16., 16.),
|
||||||
|
Kind::Junction => (10., 10.),
|
||||||
|
Kind::Fork | Kind::Join => (8., 36.),
|
||||||
|
Kind::Choice | Kind::ShallowHistory | Kind::DeepHistory => (28., 28.),
|
||||||
|
Kind::Final | Kind::EntryPoint | Kind::ExitPoint | Kind::Terminate => (24., 24.),
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
Some(Size { width, height })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reserved content before nested Regions. The same boundary is used by layout,
|
||||||
|
/// routing and the state editor's action/internal-transition rows.
|
||||||
|
pub fn state_header_height(p: &Project, element: Id) -> f32 {
|
||||||
|
let Some(e) = p.elements.get(&element) else {
|
||||||
|
return 64.;
|
||||||
|
};
|
||||||
|
if !e.kind().is_stable_state() {
|
||||||
|
return 64.;
|
||||||
|
}
|
||||||
|
let actions = matches!(e.shared(), Some(SharedBody::State { entry, exit, do_activity, .. })
|
||||||
|
if !entry.is_empty() || !exit.is_empty() || !do_activity.is_empty());
|
||||||
|
let internal = p
|
||||||
|
.relations
|
||||||
|
.values()
|
||||||
|
.filter(|r| {
|
||||||
|
matches!(&r.body,
|
||||||
|
RelationBody::StateTransition { source, transition_kind: TransitionKind::Internal, .. }
|
||||||
|
if *source == element)
|
||||||
|
})
|
||||||
|
.count();
|
||||||
|
64. + if actions { 28. } else { 0. } + internal as f32 * 56.
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn node_size(p: &Project, e: &Element, size: Size, collapsed: bool) -> Size {
|
||||||
|
if let Some(size) = state_symbol_size(e.kind()) {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
Size {
|
||||||
|
width: size.width.max(if e.kind().is_stable_state() {
|
||||||
|
184.
|
||||||
|
} else {
|
||||||
|
224.
|
||||||
|
}),
|
||||||
|
height: size.height.max(if collapsed {
|
||||||
|
94.
|
||||||
|
} else {
|
||||||
|
state_header_height(p, e.id) + 20.
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn snap_boundaries(p: &Project, nodes: &mut [ProjectedNode]) {
|
||||||
|
let geometry: BTreeMap<_, _> = nodes
|
||||||
|
.iter()
|
||||||
|
.map(|n| (n.element, (n.position, n.size)))
|
||||||
|
.collect();
|
||||||
|
for node in nodes {
|
||||||
|
let Some(e) = p.elements.get(&node.element) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if !matches!(e.kind(), Kind::EntryPoint | Kind::ExitPoint) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let boundary = e
|
||||||
|
.parent
|
||||||
|
.and_then(|id| p.elements.get(&id))
|
||||||
|
.and_then(|region| region.parent);
|
||||||
|
let Some((position, size)) = boundary.and_then(|id| geometry.get(&id)) else {
|
||||||
|
// Partial views may omit the containing boundary. Do not invent a
|
||||||
|
// placement or silently attach the symbol to a different container.
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
node.position.x = position.x
|
||||||
|
+ if e.kind() == Kind::ExitPoint {
|
||||||
|
size.width
|
||||||
|
} else {
|
||||||
|
0.
|
||||||
|
}
|
||||||
|
- node.size.width / 2.;
|
||||||
|
let top = position.y + boundary.map_or(64., |id| state_header_height(p, id));
|
||||||
|
let bottom = (position.y + size.height - node.size.height - 12.).max(top);
|
||||||
|
node.position.y = node.position.y.clamp(top, bottom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entry/Exit positions are attached to a containing State/Machine, not the
|
||||||
|
// Region that owns them semantically. Moving only that Region changes the
|
||||||
|
// symbol's authored position but does not translate its snapped boundary.
|
||||||
|
pub(super) fn boundary_moves_with(
|
||||||
|
p: &Project,
|
||||||
|
d: &Diagram,
|
||||||
|
edge: Id,
|
||||||
|
delta: &Position,
|
||||||
|
deltas: &BTreeMap<Id, Position>,
|
||||||
|
) -> bool {
|
||||||
|
let Some(r) = p.relations.get(&edge) else {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
[r.source(), r.target()].into_iter().all(|id| {
|
||||||
|
let Some(e) = p.elements.get(&id) else {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
if !matches!(e.kind(), Kind::EntryPoint | Kind::ExitPoint) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let boundary = e
|
||||||
|
.parent
|
||||||
|
.and_then(|id| p.elements.get(&id))
|
||||||
|
.and_then(|region| region.parent);
|
||||||
|
boundary
|
||||||
|
.and_then(|id| d.nodes.values().find(|n| n.element == id))
|
||||||
|
.is_none_or(|node| deltas.get(&node.id) == Some(delta))
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -181,6 +181,11 @@ pub fn remap_references(p: &mut Project, old: Id, new: Id) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
for d in p.diagrams.values_mut() {
|
for d in p.diagrams.values_mut() {
|
||||||
|
if let ProjectionKind::StateMachine { machine } = &mut d.projection
|
||||||
|
&& *machine == old
|
||||||
|
{
|
||||||
|
*machine = new;
|
||||||
|
}
|
||||||
for route in &mut d.routes {
|
for route in &mut d.routes {
|
||||||
if route.edge == old {
|
if route.edge == old {
|
||||||
route.edge = new;
|
route.edge = new;
|
||||||
@@ -275,6 +280,12 @@ fn deletion_impact_inner(p: &Project, id: Id, defer_exchange_allocations: bool)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for d in p.diagrams.values() {
|
for d in p.diagrams.values() {
|
||||||
|
if d.projection
|
||||||
|
.state_machine()
|
||||||
|
.is_some_and(|machine| removed.contains(&machine))
|
||||||
|
{
|
||||||
|
removed.insert(d.id);
|
||||||
|
}
|
||||||
for n in d.nodes.values() {
|
for n in d.nodes.values() {
|
||||||
if removed.contains(&d.id) || removed.contains(&n.element) {
|
if removed.contains(&d.id) || removed.contains(&n.element) {
|
||||||
removed.insert(n.id);
|
removed.insert(n.id);
|
||||||
|
|||||||
@@ -341,6 +341,19 @@ fn route_diagram_impl(
|
|||||||
let nodes: BTreeMap<_, _> = view.nodes.iter().map(|n| (n.id, n)).collect();
|
let nodes: BTreeMap<_, _> = view.nodes.iter().map(|n| (n.id, n)).collect();
|
||||||
let rectangles: Vec<_> = nodes.values().map(|n| Rect::node(n)).collect();
|
let rectangles: Vec<_> = nodes.values().map(|n| Rect::node(n)).collect();
|
||||||
let authored: BTreeMap<_, _> = diagram.routes.iter().map(|r| (r.edge, r)).collect();
|
let authored: BTreeMap<_, _> = diagram.routes.iter().map(|r| (r.edge, r)).collect();
|
||||||
|
let headers: BTreeMap<_, _> = nodes
|
||||||
|
.values()
|
||||||
|
.map(|node| {
|
||||||
|
(
|
||||||
|
node.id,
|
||||||
|
if diagram.projection.state_machine().is_some() && !node.collapsed {
|
||||||
|
projection::state_header_height(project, node.element)
|
||||||
|
} else {
|
||||||
|
HEADER
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
let mut ordered: Vec<_> = view.edges.iter().collect();
|
let mut ordered: Vec<_> = view.edges.iter().collect();
|
||||||
ordered.sort_by_key(|e| e.id);
|
ordered.sort_by_key(|e| e.id);
|
||||||
let mut edges = BTreeMap::new();
|
let mut edges = BTreeMap::new();
|
||||||
@@ -395,7 +408,7 @@ fn route_diagram_impl(
|
|||||||
.map(|r| {
|
.map(|r| {
|
||||||
let mut r = *r;
|
let mut r = *r;
|
||||||
if traversable.contains(&r.id) {
|
if traversable.contains(&r.id) {
|
||||||
r.bottom = r.bottom.min(r.top + HEADER);
|
r.bottom = r.bottom.min(r.top + headers[&r.id]);
|
||||||
}
|
}
|
||||||
r.inflate(CLEARANCE)
|
r.inflate(CLEARANCE)
|
||||||
})
|
})
|
||||||
@@ -554,6 +567,15 @@ fn route_edge(
|
|||||||
let mut candidates = Vec::new();
|
let mut candidates = Vec::new();
|
||||||
for a in endpoints(source, edge.source_port, view) {
|
for a in endpoints(source, edge.source_port, view) {
|
||||||
for b in endpoints(target, edge.target_port, view) {
|
for b in endpoints(target, edge.target_port, view) {
|
||||||
|
// A real self-transition must leave and return at distinct anchors.
|
||||||
|
// Reusing one anchor makes the cheapest path a degenerate stub.
|
||||||
|
if edge.source == edge.target
|
||||||
|
&& edge.source_port.is_none()
|
||||||
|
&& edge.target_port.is_none()
|
||||||
|
&& a.point == b.point
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let estimate = manhattan(a.stub, pins.first().copied().unwrap_or(b.stub))
|
let estimate = manhattan(a.stub, pins.first().copied().unwrap_or(b.stub))
|
||||||
+ pins.windows(2).map(|p| manhattan(p[0], p[1])).sum::<f32>()
|
+ pins.windows(2).map(|p| manhattan(p[0], p[1])).sum::<f32>()
|
||||||
+ pins.last().map_or(0.0, |&p| manhattan(p, b.stub));
|
+ pins.last().map_or(0.0, |&p| manhattan(p, b.stub));
|
||||||
@@ -1000,10 +1022,76 @@ fn grid_path(
|
|||||||
(None, false)
|
(None, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Full semantic label shared by state transition routes and internal rows.
|
||||||
|
/// Expressions remain descriptive; no state-machine execution is implied.
|
||||||
|
pub fn state_transition_summary(p: &Project, r: &crate::Relation) -> String {
|
||||||
|
let crate::RelationBody::StateTransition {
|
||||||
|
transition_kind,
|
||||||
|
triggers,
|
||||||
|
trigger,
|
||||||
|
guard,
|
||||||
|
effects,
|
||||||
|
effect,
|
||||||
|
..
|
||||||
|
} = &r.body
|
||||||
|
else {
|
||||||
|
return r.name.clone();
|
||||||
|
};
|
||||||
|
let activation = if !triggers.is_empty() {
|
||||||
|
triggers
|
||||||
|
.iter()
|
||||||
|
.map(|id| p.named(*id))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" OR ")
|
||||||
|
} else if !trigger.trim().is_empty() {
|
||||||
|
format!("Unresolved trigger: {trigger}")
|
||||||
|
} else if p
|
||||||
|
.elements
|
||||||
|
.get(&r.source())
|
||||||
|
.is_some_and(|e| e.kind().is_stable_state())
|
||||||
|
{
|
||||||
|
"Completion".into()
|
||||||
|
} else {
|
||||||
|
"Transient routing".into()
|
||||||
|
};
|
||||||
|
let mut result = format!("{transition_kind:?} \u{b7} {activation}");
|
||||||
|
if !triggers.is_empty() && !trigger.is_empty() {
|
||||||
|
result.push_str(&format!(" \u{b7} {trigger}"));
|
||||||
|
}
|
||||||
|
if !guard.is_empty() {
|
||||||
|
result.push_str(&format!(" [{guard}]"));
|
||||||
|
}
|
||||||
|
if !effects.is_empty() {
|
||||||
|
result.push_str(&format!(
|
||||||
|
" / {}",
|
||||||
|
effects
|
||||||
|
.iter()
|
||||||
|
.map(|id| p.named(*id))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if !effect.is_empty() {
|
||||||
|
result.push_str(&format!(" / {effect}"));
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
fn label_text(project: &Project, edge: Id) -> String {
|
fn label_text(project: &Project, edge: Id) -> String {
|
||||||
let Some(relation) = project.relations.get(&edge) else {
|
let Some(relation) = project.relations.get(&edge) else {
|
||||||
return String::new();
|
return String::new();
|
||||||
};
|
};
|
||||||
|
if relation.kind() == RelationKind::StateTransition {
|
||||||
|
let text = state_transition_summary(project, relation);
|
||||||
|
return if text.chars().count() > 48 {
|
||||||
|
text.chars()
|
||||||
|
.take(47)
|
||||||
|
.chain(std::iter::once('\u{2026}'))
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
text
|
||||||
|
};
|
||||||
|
}
|
||||||
let text = match relation.kind() {
|
let text = match relation.kind() {
|
||||||
RelationKind::Interaction
|
RelationKind::Interaction
|
||||||
| RelationKind::FunctionalExchange
|
| RelationKind::FunctionalExchange
|
||||||
|
|||||||
@@ -859,9 +859,19 @@ fn layout(p: &mut Project, diagram: Id) -> Result<(), ModelError> {
|
|||||||
if !seen.insert(id) {
|
if !seen.insert(id) {
|
||||||
return Err(ModelError("Diagram containment cycle.".into()));
|
return Err(ModelError("Diagram containment cycle.".into()));
|
||||||
}
|
}
|
||||||
let own = crate::projection::minimum_node_size(p, d.nodes[&id].element, d.nodes[&id].size);
|
let node = &d.nodes[&id];
|
||||||
|
let own = crate::projection::minimum_node_size(p, node.element, node.size);
|
||||||
|
let own = if d.projection.state_machine().is_some() {
|
||||||
|
crate::projection::state_node_size(p, &p.elements[&node.element], own, false)
|
||||||
|
} else {
|
||||||
|
own
|
||||||
|
};
|
||||||
let mut width = own.width;
|
let mut width = own.width;
|
||||||
let mut height = 64.;
|
let mut height = if d.projection.state_machine().is_some() {
|
||||||
|
crate::projection::state_header_height(p, node.element)
|
||||||
|
} else {
|
||||||
|
64.
|
||||||
|
};
|
||||||
let kids = children.get(&Some(id));
|
let kids = children.get(&Some(id));
|
||||||
if let Some(kids) = kids {
|
if let Some(kids) = kids {
|
||||||
for child in kids {
|
for child in kids {
|
||||||
@@ -886,9 +896,10 @@ fn layout(p: &mut Project, diagram: Id) -> Result<(), ModelError> {
|
|||||||
children: &BTreeMap<Option<Id>, Vec<Id>>,
|
children: &BTreeMap<Option<Id>, Vec<Id>>,
|
||||||
sizes: &BTreeMap<Id, Size>,
|
sizes: &BTreeMap<Id, Size>,
|
||||||
positions: &mut BTreeMap<Id, Position>,
|
positions: &mut BTreeMap<Id, Position>,
|
||||||
|
headers: &BTreeMap<Id, f32>,
|
||||||
) {
|
) {
|
||||||
positions.insert(id, position);
|
positions.insert(id, position);
|
||||||
let mut y = position.y + 64.;
|
let mut y = position.y + headers[&id];
|
||||||
if let Some(kids) = children.get(&Some(id)) {
|
if let Some(kids) = children.get(&Some(id)) {
|
||||||
for child in kids {
|
for child in kids {
|
||||||
place(
|
place(
|
||||||
@@ -900,6 +911,7 @@ fn layout(p: &mut Project, diagram: Id) -> Result<(), ModelError> {
|
|||||||
children,
|
children,
|
||||||
sizes,
|
sizes,
|
||||||
positions,
|
positions,
|
||||||
|
headers,
|
||||||
);
|
);
|
||||||
y += sizes[child].height + 16.;
|
y += sizes[child].height + 16.;
|
||||||
}
|
}
|
||||||
@@ -957,6 +969,20 @@ fn layout(p: &mut Project, diagram: Id) -> Result<(), ModelError> {
|
|||||||
for id in &roots {
|
for id in &roots {
|
||||||
measure(p, *id, d, &children, &mut sizes, &mut seen)?;
|
measure(p, *id, d, &children, &mut sizes, &mut seen)?;
|
||||||
}
|
}
|
||||||
|
let headers = d
|
||||||
|
.nodes
|
||||||
|
.values()
|
||||||
|
.map(|n| {
|
||||||
|
(
|
||||||
|
n.id,
|
||||||
|
if d.projection.state_machine().is_some() {
|
||||||
|
crate::projection::state_header_height(p, n.element)
|
||||||
|
} else {
|
||||||
|
64.
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
let mut positions = BTreeMap::new();
|
let mut positions = BTreeMap::new();
|
||||||
let mut x = 60.;
|
let mut x = 60.;
|
||||||
let mut y = 60.;
|
let mut y = 60.;
|
||||||
@@ -967,7 +993,14 @@ fn layout(p: &mut Project, diagram: Id) -> Result<(), ModelError> {
|
|||||||
y += row_height + 60.;
|
y += row_height + 60.;
|
||||||
row_height = 0.;
|
row_height = 0.;
|
||||||
}
|
}
|
||||||
place(*id, Position { x, y }, &children, &sizes, &mut positions);
|
place(
|
||||||
|
*id,
|
||||||
|
Position { x, y },
|
||||||
|
&children,
|
||||||
|
&sizes,
|
||||||
|
&mut positions,
|
||||||
|
&headers,
|
||||||
|
);
|
||||||
x += sizes[id].width + 240.;
|
x += sizes[id].width + 240.;
|
||||||
row_height = row_height.max(sizes[id].height);
|
row_height = row_height.max(sizes[id].height);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -657,6 +657,20 @@ pub fn validate(p: &Project) -> Vec<Issue> {
|
|||||||
}
|
}
|
||||||
text(&mut out, d.id, "name", &d.name);
|
text(&mut out, d.id, "name", &d.name);
|
||||||
text(&mut out, d.id, "description", &d.description);
|
text(&mut out, d.id, "description", &d.description);
|
||||||
|
if let Some(machine) = d.projection.state_machine()
|
||||||
|
&& !p
|
||||||
|
.elements
|
||||||
|
.get(&machine)
|
||||||
|
.is_some_and(|e| e.kind() == Kind::StateMachine && e.layer() == d.layer)
|
||||||
|
{
|
||||||
|
error(
|
||||||
|
&mut out,
|
||||||
|
d.id,
|
||||||
|
"state_diagram_context",
|
||||||
|
"projection",
|
||||||
|
"State diagram context must reference an existing state machine in the diagram layer.",
|
||||||
|
);
|
||||||
|
}
|
||||||
let mut routed_edges = BTreeSet::new();
|
let mut routed_edges = BTreeSet::new();
|
||||||
for (index, route) in d.routes.iter().enumerate() {
|
for (index, route) in d.routes.iter().enumerate() {
|
||||||
if !routed_edges.insert(route.edge) {
|
if !routed_edges.insert(route.edge) {
|
||||||
@@ -677,6 +691,21 @@ pub fn validate(p: &Project) -> Vec<Issue> {
|
|||||||
"Route edge must reference an existing relation or trace.",
|
"Route edge must reference an existing relation or trace.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if let Some(machine) = d.projection.state_machine()
|
||||||
|
&& !p.relations.get(&route.edge).is_some_and(|r| {
|
||||||
|
r.kind() == RelationKind::StateTransition
|
||||||
|
&& crate::projection::state_machine_contains(p, machine, r.source())
|
||||||
|
&& crate::projection::state_machine_contains(p, machine, r.target())
|
||||||
|
})
|
||||||
|
{
|
||||||
|
error(
|
||||||
|
&mut out,
|
||||||
|
d.id,
|
||||||
|
"state_diagram_route",
|
||||||
|
"routes",
|
||||||
|
"State diagram routes belong to transitions in its machine. Remove or move incompatible routes in the same transaction.",
|
||||||
|
);
|
||||||
|
}
|
||||||
if route.waypoints.len() > MAX_ROUTE_POINTS {
|
if route.waypoints.len() > MAX_ROUTE_POINTS {
|
||||||
error(
|
error(
|
||||||
&mut out,
|
&mut out,
|
||||||
@@ -760,6 +789,17 @@ pub fn validate(p: &Project) -> Vec<Issue> {
|
|||||||
"Diagram nodes reference existing elements in the selected layer.",
|
"Diagram nodes reference existing elements in the selected layer.",
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
if let Some(machine) = d.projection.state_machine()
|
||||||
|
&& !crate::projection::state_machine_contains(p, machine, n.element)
|
||||||
|
{
|
||||||
|
error(
|
||||||
|
&mut out,
|
||||||
|
n.id,
|
||||||
|
"state_diagram_member",
|
||||||
|
"element",
|
||||||
|
"State diagrams contain only their machine, its regions and vertices. Remove or relocate incompatible placements in the same transaction.",
|
||||||
|
);
|
||||||
|
}
|
||||||
for (value, field) in [
|
for (value, field) in [
|
||||||
(n.position.x, "x"),
|
(n.position.x, "x"),
|
||||||
(n.position.y, "y"),
|
(n.position.y, "y"),
|
||||||
|
|||||||
@@ -0,0 +1,744 @@
|
|||||||
|
use oa_core::{projection::*, routing::*, source_project::*, *};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
fn point(x: f32, y: f32) -> Position {
|
||||||
|
Position { x, y }
|
||||||
|
}
|
||||||
|
fn add(p: &mut Project, kind: Kind, name: &str, parent: Option<Id>) -> Id {
|
||||||
|
p.add_element(kind, Layer::Oa, name, parent).unwrap()
|
||||||
|
}
|
||||||
|
fn view(p: &mut Project, machine: Id, alias: &str) -> Id {
|
||||||
|
let id = Id::new_v4();
|
||||||
|
p.diagrams.insert(
|
||||||
|
id,
|
||||||
|
Diagram {
|
||||||
|
id,
|
||||||
|
alias: alias.into(),
|
||||||
|
name: alias.into(),
|
||||||
|
description: String::new(),
|
||||||
|
layer: Layer::Oa,
|
||||||
|
projection: ProjectionKind::StateMachine { machine },
|
||||||
|
nodes: BTreeMap::new(),
|
||||||
|
routes: vec![],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
id
|
||||||
|
}
|
||||||
|
fn node(p: &Project, d: Id, e: Id) -> Id {
|
||||||
|
p.diagrams[&d]
|
||||||
|
.nodes
|
||||||
|
.values()
|
||||||
|
.find(|n| n.element == e)
|
||||||
|
.unwrap()
|
||||||
|
.id
|
||||||
|
}
|
||||||
|
fn place(p: &mut Project, d: Id, e: Id, x: f32, y: f32, w: f32, h: f32) -> Id {
|
||||||
|
let id = p.place(d, e, point(x, y));
|
||||||
|
p.diagrams
|
||||||
|
.get_mut(&d)
|
||||||
|
.unwrap()
|
||||||
|
.nodes
|
||||||
|
.get_mut(&id)
|
||||||
|
.unwrap()
|
||||||
|
.size = Size {
|
||||||
|
width: w,
|
||||||
|
height: h,
|
||||||
|
};
|
||||||
|
id
|
||||||
|
}
|
||||||
|
fn transition(p: &mut Project, a: Id, b: Id, kind: TransitionKind) -> Id {
|
||||||
|
let id = p.relate(RelationKind::StateTransition, "Transition", a, b);
|
||||||
|
if let RelationBody::StateTransition {
|
||||||
|
transition_kind, ..
|
||||||
|
} = &mut p.relations.get_mut(&id).unwrap().body
|
||||||
|
{
|
||||||
|
*transition_kind = kind;
|
||||||
|
}
|
||||||
|
id
|
||||||
|
}
|
||||||
|
fn code(p: &Project, expected: &str) {
|
||||||
|
assert!(
|
||||||
|
validation::validate(p).iter().any(|i| i.code == expected),
|
||||||
|
"{:?}",
|
||||||
|
validation::validate(p)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
fn orthogonal(points: &[Position]) {
|
||||||
|
assert!(
|
||||||
|
points
|
||||||
|
.windows(2)
|
||||||
|
.all(|p| p[0].x == p[1].x || p[0].y == p[1].y),
|
||||||
|
"{points:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
struct Fixture {
|
||||||
|
p: Project,
|
||||||
|
machine: Id,
|
||||||
|
region: Id,
|
||||||
|
a: Id,
|
||||||
|
b: Id,
|
||||||
|
inner: Id,
|
||||||
|
leaf: Id,
|
||||||
|
entry: Id,
|
||||||
|
exit: Id,
|
||||||
|
d: Id,
|
||||||
|
external: Id,
|
||||||
|
self_edge: Id,
|
||||||
|
internal: Id,
|
||||||
|
local: Id,
|
||||||
|
}
|
||||||
|
fn fixture() -> Fixture {
|
||||||
|
let mut p = Project::empty();
|
||||||
|
let performer = add(&mut p, Kind::Entity, "Crew", None);
|
||||||
|
let machine = add(
|
||||||
|
&mut p,
|
||||||
|
Kind::StateMachine,
|
||||||
|
"Operating modes",
|
||||||
|
Some(performer),
|
||||||
|
);
|
||||||
|
let region = add(&mut p, Kind::Region, "Main region", Some(machine));
|
||||||
|
let initial = add(&mut p, Kind::Initial, "Start", Some(region));
|
||||||
|
let a = add(&mut p, Kind::State, "Operating", Some(region));
|
||||||
|
let b = add(&mut p, Kind::State, "Standby", Some(region));
|
||||||
|
let inner = add(&mut p, Kind::Region, "Nested region", Some(a));
|
||||||
|
let leaf = add(&mut p, Kind::State, "Working", Some(inner));
|
||||||
|
let entry = add(&mut p, Kind::EntryPoint, "Enter", Some(inner));
|
||||||
|
let exit = add(&mut p, Kind::ExitPoint, "Exit", Some(inner));
|
||||||
|
transition(&mut p, initial, a, TransitionKind::External);
|
||||||
|
let external = transition(&mut p, a, b, TransitionKind::External);
|
||||||
|
if let RelationBody::StateTransition { guard, .. } =
|
||||||
|
&mut p.relations.get_mut(&external).unwrap().body
|
||||||
|
{
|
||||||
|
*guard = "ready".into();
|
||||||
|
}
|
||||||
|
let self_edge = transition(&mut p, b, b, TransitionKind::External);
|
||||||
|
let internal = transition(&mut p, a, a, TransitionKind::Internal);
|
||||||
|
let local = transition(&mut p, entry, leaf, TransitionKind::Local);
|
||||||
|
transition(&mut p, leaf, exit, TransitionKind::External);
|
||||||
|
let d = view(&mut p, machine, "operating_view");
|
||||||
|
for (e, x, y, w, h) in [
|
||||||
|
(machine, 0., 0., 1100., 900.),
|
||||||
|
(region, 24., 64., 1000., 800.),
|
||||||
|
(initial, 60., 160., 224., 94.),
|
||||||
|
(a, 180., 180., 440., 500.),
|
||||||
|
(b, 760., 180., 224., 94.),
|
||||||
|
(inner, 204., 320., 380., 300.),
|
||||||
|
(leaf, 310., 420., 224., 94.),
|
||||||
|
(entry, -200., 390., 224., 94.),
|
||||||
|
(exit, 2000., 550., 224., 94.),
|
||||||
|
] {
|
||||||
|
place(&mut p, d, e, x, y, w, h);
|
||||||
|
}
|
||||||
|
validate_candidate(&p).unwrap();
|
||||||
|
Fixture {
|
||||||
|
p,
|
||||||
|
machine,
|
||||||
|
region,
|
||||||
|
a,
|
||||||
|
b,
|
||||||
|
inner,
|
||||||
|
leaf,
|
||||||
|
entry,
|
||||||
|
exit,
|
||||||
|
d,
|
||||||
|
external,
|
||||||
|
self_edge,
|
||||||
|
internal,
|
||||||
|
local,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn strict_context_wire_source_and_admission_contract() {
|
||||||
|
let f = fixture();
|
||||||
|
let json = serde_json::json!({"StateMachine":{"machine":f.machine}});
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_value(f.p.diagrams[&f.d].projection).unwrap(),
|
||||||
|
json
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::from_value::<ProjectionKind>(serde_json::json!("Overview")).unwrap(),
|
||||||
|
ProjectionKind::Overview
|
||||||
|
);
|
||||||
|
for invalid in [
|
||||||
|
serde_json::json!("StateMachine"),
|
||||||
|
serde_json::json!({"StateMachine":{}}),
|
||||||
|
serde_json::json!({"StateMachine":{"machine":null}}),
|
||||||
|
serde_json::json!({"StateMachine":{"machine":f.machine,"extra":1}}),
|
||||||
|
] {
|
||||||
|
assert!(serde_json::from_value::<ProjectionKind>(invalid).is_err());
|
||||||
|
}
|
||||||
|
let source = language::format(&f.p);
|
||||||
|
assert!(
|
||||||
|
source.contains(
|
||||||
|
"projection {\n StateMachine {\n machine @operating_modes;"
|
||||||
|
),
|
||||||
|
"{source}"
|
||||||
|
);
|
||||||
|
assert_eq!(language::parse(&source).unwrap(), f.p);
|
||||||
|
assert!(
|
||||||
|
language::parse(&source.replace(
|
||||||
|
"machine @operating_modes;",
|
||||||
|
"machine @operating_modes; extra 1;"
|
||||||
|
))
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
let mut limits = language::ParseLimits::UNLIMITED;
|
||||||
|
limits.max_records = f.p.identities().len() - 1;
|
||||||
|
assert!(language::parse_limited(&source, limits).is_err());
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::from_slice::<Project>(&serde_json::to_vec(&f.p).unwrap()).unwrap(),
|
||||||
|
f.p
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn contexts_restrict_membership_without_changing_legacy_hidden_placements() {
|
||||||
|
let mut f = fixture();
|
||||||
|
let other = add(&mut f.p, Kind::StateMachine, "Other machine", None);
|
||||||
|
let foreign_region = add(&mut f.p, Kind::Region, "Other region", Some(other));
|
||||||
|
let foreign = add(&mut f.p, Kind::State, "Other state", Some(foreign_region));
|
||||||
|
assert!(state_machine_contains(&f.p, f.machine, f.leaf));
|
||||||
|
assert!(!state_machine_contains(&f.p, f.machine, foreign));
|
||||||
|
let foreign_node = f.p.place(f.d, foreign, point(0., 0.));
|
||||||
|
code(&f.p, "state_diagram_member");
|
||||||
|
assert!(
|
||||||
|
!project_view(&f.p, f.d)
|
||||||
|
.nodes
|
||||||
|
.iter()
|
||||||
|
.any(|n| n.id == foreign_node)
|
||||||
|
);
|
||||||
|
f.p.diagrams.get_mut(&f.d).unwrap().projection = ProjectionKind::FunctionalDataflow;
|
||||||
|
validate_candidate(&f.p).unwrap();
|
||||||
|
assert!(project_view(&f.p, f.d).nodes.is_empty());
|
||||||
|
for machine in [Id::nil(), f.region] {
|
||||||
|
f.p.diagrams.get_mut(&f.d).unwrap().projection = ProjectionKind::StateMachine { machine };
|
||||||
|
code(&f.p, "state_diagram_context");
|
||||||
|
}
|
||||||
|
f.p.diagrams.get_mut(&f.d).unwrap().projection =
|
||||||
|
ProjectionKind::StateMachine { machine: f.machine };
|
||||||
|
f.p.diagrams
|
||||||
|
.get_mut(&f.d)
|
||||||
|
.unwrap()
|
||||||
|
.nodes
|
||||||
|
.remove(&foreign_node);
|
||||||
|
f.p.diagrams.get_mut(&f.d).unwrap().layer = Layer::Sa;
|
||||||
|
code(&f.p, "state_diagram_context");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_and_partial_views_are_read_only_and_omit_unrelated_edges() {
|
||||||
|
let mut f = fixture();
|
||||||
|
let empty = view(&mut f.p, f.machine, "empty_state_view");
|
||||||
|
validate_candidate(&f.p).unwrap();
|
||||||
|
let before = f.p.clone();
|
||||||
|
assert!(project_view(&f.p, empty).nodes.is_empty());
|
||||||
|
assert_eq!(before, f.p);
|
||||||
|
let unrelated =
|
||||||
|
f.p.relate(RelationKind::Availability, "Availability", f.a, f.b);
|
||||||
|
let partial = f.p.diagrams.get_mut(&f.d).unwrap();
|
||||||
|
partial.nodes.retain(|_, n| [f.a, f.b].contains(&n.element));
|
||||||
|
let before = f.p.clone();
|
||||||
|
let projected = project_view(&f.p, f.d);
|
||||||
|
let _ = route_diagram(&f.p, &f.p.diagrams[&f.d], &projected);
|
||||||
|
assert_eq!(before, f.p);
|
||||||
|
assert!(
|
||||||
|
!projected
|
||||||
|
.edges
|
||||||
|
.iter()
|
||||||
|
.any(|e| e.id == unrelated || e.id == f.internal)
|
||||||
|
);
|
||||||
|
assert!(projected.edges.iter().any(|e| e.id == f.self_edge));
|
||||||
|
assert!(projected.nodes.iter().all(|n| n.visual_parent.is_none()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn real_state_symbols_boundaries_internal_rows_and_self_loops_share_geometry() {
|
||||||
|
let f = fixture();
|
||||||
|
let projected = project_view(&f.p, f.d);
|
||||||
|
let bounds = |id| projected.nodes.iter().find(|n| n.element == id).unwrap();
|
||||||
|
let a = bounds(f.a);
|
||||||
|
let entry = bounds(f.entry);
|
||||||
|
let exit = bounds(f.exit);
|
||||||
|
assert_eq!(entry.size, state_symbol_size(Kind::EntryPoint).unwrap());
|
||||||
|
assert_eq!(entry.position.x + entry.size.width / 2., a.position.x);
|
||||||
|
assert_eq!(
|
||||||
|
exit.position.x + exit.size.width / 2.,
|
||||||
|
a.position.x + a.size.width
|
||||||
|
);
|
||||||
|
assert!(entry.position.y >= a.position.y + state_header_height(&f.p, f.a));
|
||||||
|
assert_eq!(entry.visual_parent, Some(node(&f.p, f.d, f.inner)));
|
||||||
|
assert_eq!(state_header_height(&f.p, f.a), 120.);
|
||||||
|
assert!(!projected.edges.iter().any(|e| e.id == f.internal));
|
||||||
|
assert!(projected.edges.iter().any(|e| e.id == f.local));
|
||||||
|
let routed = route_diagram(&f.p, &f.p.diagrams[&f.d], &projected);
|
||||||
|
let path = &routed.edges[&f.self_edge];
|
||||||
|
orthogonal(&path.points);
|
||||||
|
assert_ne!(
|
||||||
|
path.points.first(),
|
||||||
|
path.points.last(),
|
||||||
|
"Self-transition returns at a different boundary anchor"
|
||||||
|
);
|
||||||
|
assert!(path.points.len() >= 5, "{:?}", path.points);
|
||||||
|
assert!(path.warnings.is_empty(), "{:?}", path.warnings);
|
||||||
|
let b = bounds(f.b);
|
||||||
|
assert!(path.points.iter().any(|p| p.x < b.position.x
|
||||||
|
|| p.x > b.position.x + b.size.width
|
||||||
|
|| p.y < b.position.y
|
||||||
|
|| p.y > b.position.y + b.size.height));
|
||||||
|
assert!(routed.edges[&f.external].label.text.contains("[ready]"));
|
||||||
|
for edge in routed.edges.values() {
|
||||||
|
orthogonal(&edge.points);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn folding_retains_authored_routes_without_false_self_transitions() {
|
||||||
|
let mut f = fixture();
|
||||||
|
let hidden_self = transition(&mut f.p, f.leaf, f.leaf, TransitionKind::External);
|
||||||
|
let out = transition(&mut f.p, f.leaf, f.b, TransitionKind::External);
|
||||||
|
let pins = vec![point(690., 480.), point(690., 230.)];
|
||||||
|
f.p.diagrams.get_mut(&f.d).unwrap().routes.push(EdgeRoute {
|
||||||
|
edge: out,
|
||||||
|
waypoints: pins.clone(),
|
||||||
|
label: None,
|
||||||
|
});
|
||||||
|
let a = node(&f.p, f.d, f.a);
|
||||||
|
f.p.diagrams
|
||||||
|
.get_mut(&f.d)
|
||||||
|
.unwrap()
|
||||||
|
.nodes
|
||||||
|
.get_mut(&a)
|
||||||
|
.unwrap()
|
||||||
|
.collapsed = true;
|
||||||
|
let projected = project_view(&f.p, f.d);
|
||||||
|
assert!(
|
||||||
|
!projected
|
||||||
|
.edges
|
||||||
|
.iter()
|
||||||
|
.any(|e| e.id == hidden_self || e.id == f.local)
|
||||||
|
);
|
||||||
|
let routed = route_diagram(&f.p, &f.p.diagrams[&f.d], &projected);
|
||||||
|
assert!(routed.edges[&out].custom_suspended);
|
||||||
|
assert!(routed.edges[&out].pins.is_empty());
|
||||||
|
assert_eq!(f.p.diagrams[&f.d].routes[0].waypoints, pins);
|
||||||
|
f.p.diagrams
|
||||||
|
.get_mut(&f.d)
|
||||||
|
.unwrap()
|
||||||
|
.nodes
|
||||||
|
.get_mut(&a)
|
||||||
|
.unwrap()
|
||||||
|
.collapsed = false;
|
||||||
|
let restored = route_diagram(&f.p, &f.p.diagrams[&f.d], &project_view(&f.p, f.d));
|
||||||
|
assert_eq!(restored.edges[&out].pins, pins);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grouped_moves_translate_hidden_descendants_and_pins_only_in_one_view() {
|
||||||
|
let mut f = fixture();
|
||||||
|
let copied = view(&mut f.p, f.machine, "second_view");
|
||||||
|
for n in f.p.diagrams[&f.d]
|
||||||
|
.nodes
|
||||||
|
.values()
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
{
|
||||||
|
place(
|
||||||
|
&mut f.p,
|
||||||
|
copied,
|
||||||
|
n.element,
|
||||||
|
n.position.x,
|
||||||
|
n.position.y,
|
||||||
|
n.size.width,
|
||||||
|
n.size.height,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let moved = node(&f.p, f.d, f.a);
|
||||||
|
let label = EdgeLabel {
|
||||||
|
fraction: 0.4,
|
||||||
|
offset: point(3., -9.),
|
||||||
|
};
|
||||||
|
let pins = vec![point(570., 460.)];
|
||||||
|
f.p.diagrams.get_mut(&f.d).unwrap().routes.push(EdgeRoute {
|
||||||
|
edge: f.local,
|
||||||
|
waypoints: pins.clone(),
|
||||||
|
label: Some(label.clone()),
|
||||||
|
});
|
||||||
|
f.p.diagrams.get_mut(&f.d).unwrap().routes.push(EdgeRoute {
|
||||||
|
edge: f.external,
|
||||||
|
waypoints: vec![point(690., 230.)],
|
||||||
|
label: None,
|
||||||
|
});
|
||||||
|
f.p.diagrams
|
||||||
|
.get_mut(&f.d)
|
||||||
|
.unwrap()
|
||||||
|
.nodes
|
||||||
|
.get_mut(&moved)
|
||||||
|
.unwrap()
|
||||||
|
.collapsed = true;
|
||||||
|
let before = f.p.clone();
|
||||||
|
let mut session = Session::try_new(f.p).unwrap();
|
||||||
|
session
|
||||||
|
.execute(Command::MoveNode {
|
||||||
|
diagram: f.d,
|
||||||
|
node: moved,
|
||||||
|
position: point(220., 240.),
|
||||||
|
with_descendants: true,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let after = session.project();
|
||||||
|
assert_eq!(after.diagrams[&copied], before.diagrams[&copied]);
|
||||||
|
for e in [f.a, f.inner, f.leaf, f.entry, f.exit] {
|
||||||
|
let n = node(&before, f.d, e);
|
||||||
|
let old = before.diagrams[&f.d].nodes[&n].position;
|
||||||
|
assert_eq!(
|
||||||
|
after.diagrams[&f.d].nodes[&n].position,
|
||||||
|
point(old.x + 40., old.y + 60.)
|
||||||
|
);
|
||||||
|
assert_eq!(after.elements[&e], before.elements[&e]);
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
after.diagrams[&f.d].routes[0].waypoints,
|
||||||
|
vec![point(610., 520.)]
|
||||||
|
);
|
||||||
|
assert_eq!(after.diagrams[&f.d].routes[0].label, Some(label));
|
||||||
|
assert_eq!(
|
||||||
|
after.diagrams[&f.d].routes[1],
|
||||||
|
before.diagrams[&f.d].routes[1]
|
||||||
|
);
|
||||||
|
let after = after.clone();
|
||||||
|
session.undo();
|
||||||
|
assert_eq!(session.project(), &before);
|
||||||
|
session.redo();
|
||||||
|
assert_eq!(session.project(), &after);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cross_machine_reparent_requires_atomic_placement_and_route_repair() {
|
||||||
|
let mut f = fixture();
|
||||||
|
let orphan = add(&mut f.p, Kind::State, "Independent", Some(f.region));
|
||||||
|
let edge = transition(&mut f.p, orphan, orphan, TransitionKind::External);
|
||||||
|
let placement = f.p.place(f.d, orphan, point(30., 750.));
|
||||||
|
f.p.diagrams.get_mut(&f.d).unwrap().routes.push(EdgeRoute {
|
||||||
|
edge,
|
||||||
|
waypoints: vec![],
|
||||||
|
label: None,
|
||||||
|
});
|
||||||
|
let machine = add(&mut f.p, Kind::StateMachine, "Other machine", None);
|
||||||
|
let region = add(&mut f.p, Kind::Region, "Other region", Some(machine));
|
||||||
|
let other = view(&mut f.p, machine, "other_view");
|
||||||
|
let before = f.p.clone();
|
||||||
|
assert!(
|
||||||
|
prepare_command(
|
||||||
|
&f.p,
|
||||||
|
&Command::Reparent {
|
||||||
|
element: orphan,
|
||||||
|
parent: Some(region)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
assert_eq!(f.p, before);
|
||||||
|
let repaired = prepare_command(
|
||||||
|
&f.p,
|
||||||
|
&Command::Batch(vec![
|
||||||
|
Command::Reparent {
|
||||||
|
element: orphan,
|
||||||
|
parent: Some(region),
|
||||||
|
},
|
||||||
|
Command::RelocateNode {
|
||||||
|
node: placement,
|
||||||
|
diagram: other,
|
||||||
|
},
|
||||||
|
Command::RemoveEdgeRoute { diagram: f.d, edge },
|
||||||
|
Command::SetEdgeRoute {
|
||||||
|
diagram: other,
|
||||||
|
route: EdgeRoute {
|
||||||
|
edge,
|
||||||
|
waypoints: vec![],
|
||||||
|
label: None,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.project;
|
||||||
|
assert_eq!(repaired.diagrams[&other].nodes[&placement].element, orphan);
|
||||||
|
assert!(!repaired.diagrams[&f.d].nodes.contains_key(&placement));
|
||||||
|
assert!(repaired.diagrams[&f.d].routes.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn remapping_and_machine_deletion_include_dedicated_views_and_dependents() {
|
||||||
|
let mut f = fixture();
|
||||||
|
let sa_owner =
|
||||||
|
f.p.add_element(Kind::System, Layer::Sa, "SA system", None)
|
||||||
|
.unwrap();
|
||||||
|
let counterpart =
|
||||||
|
f.p.add_element(Kind::StateMachine, Layer::Sa, "SA modes", Some(sa_owner))
|
||||||
|
.unwrap();
|
||||||
|
let trace = Id::new_v4();
|
||||||
|
f.p.traces.insert(
|
||||||
|
trace,
|
||||||
|
Trace {
|
||||||
|
id: trace,
|
||||||
|
alias: "machine_trace".into(),
|
||||||
|
name: "Machine trace".into(),
|
||||||
|
description: String::new(),
|
||||||
|
oa: f.machine,
|
||||||
|
sa: counterpart,
|
||||||
|
kind: TraceKind::State,
|
||||||
|
baseline: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let artifact = Id::new_v4();
|
||||||
|
f.p.artifact_links.insert(
|
||||||
|
artifact,
|
||||||
|
ArtifactLink {
|
||||||
|
id: artifact,
|
||||||
|
alias: "view_review".into(),
|
||||||
|
name: "View review".into(),
|
||||||
|
description: String::new(),
|
||||||
|
owner: f.d,
|
||||||
|
relation: ArtifactRelation::References,
|
||||||
|
uri: "file:///review.txt".into(),
|
||||||
|
revision: None,
|
||||||
|
fragment: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
validate_candidate(&f.p).unwrap();
|
||||||
|
let renamed = Id::new_v4();
|
||||||
|
let remapped = prepare_command(&f.p, &Command::Reidentify(f.machine, renamed))
|
||||||
|
.unwrap()
|
||||||
|
.project;
|
||||||
|
assert_eq!(
|
||||||
|
remapped.diagrams[&f.d].projection.state_machine(),
|
||||||
|
Some(renamed)
|
||||||
|
);
|
||||||
|
assert_eq!(remapped.elements[&f.region].parent, Some(renamed));
|
||||||
|
let impact = references::deletion_impact(&remapped, renamed);
|
||||||
|
for id in [f.d, artifact, trace, node(&remapped, f.d, f.a)] {
|
||||||
|
assert!(impact.contains(&id));
|
||||||
|
}
|
||||||
|
assert!(!impact.contains(&counterpart));
|
||||||
|
let deleted = prepare_command(&remapped, &Command::Delete(renamed))
|
||||||
|
.unwrap()
|
||||||
|
.project;
|
||||||
|
assert!(!deleted.diagrams.contains_key(&f.d));
|
||||||
|
assert!(deleted.elements.contains_key(&counterpart));
|
||||||
|
let hidden = prepare_command(&f.p, &Command::Delete(node(&f.p, f.d, f.machine)))
|
||||||
|
.unwrap()
|
||||||
|
.project;
|
||||||
|
assert!(hidden.diagrams.contains_key(&f.d));
|
||||||
|
assert!(hidden.elements.contains_key(&f.machine));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn source_updates_preserve_context_identity_and_unrelated_authored_bytes() {
|
||||||
|
let f = fixture();
|
||||||
|
let mut bundle = format_bundle(&f.p);
|
||||||
|
bundle
|
||||||
|
.files
|
||||||
|
.get_mut(&bundle.entry.clone())
|
||||||
|
.unwrap()
|
||||||
|
.insert_str(0, "// state source rationale\n");
|
||||||
|
let original = compile_bundle(&bundle, ProjectLimits::default()).unwrap();
|
||||||
|
let fresh = Id::new_v4();
|
||||||
|
let prepared = prepare_command(
|
||||||
|
&f.p,
|
||||||
|
&Command::Batch(vec![
|
||||||
|
Command::Reidentify(f.machine, fresh),
|
||||||
|
Command::SetAlias(fresh, "reviewed_modes".into()),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let updated = update_bundle_with_identity_map(
|
||||||
|
&original,
|
||||||
|
&prepared.project,
|
||||||
|
&prepared.identity_map,
|
||||||
|
ProjectLimits::default(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(updated.decoded.project, prepared.project);
|
||||||
|
assert!(
|
||||||
|
updated
|
||||||
|
.bundle
|
||||||
|
.files
|
||||||
|
.values()
|
||||||
|
.any(|s| s.contains("machine @reviewed_modes;"))
|
||||||
|
);
|
||||||
|
assert!(updated.bundle.files[&bundle.entry].starts_with("// state source rationale\n"));
|
||||||
|
assert_eq!(
|
||||||
|
compile_bundle(&updated.bundle, ProjectLimits::default())
|
||||||
|
.unwrap()
|
||||||
|
.bundle,
|
||||||
|
updated.bundle
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn layout_reserves_internal_rows_and_state_routes_retain_hard_pins() {
|
||||||
|
let mut f = fixture();
|
||||||
|
let pins = vec![point(730., 130.), point(1060., 130.)];
|
||||||
|
f.p.diagrams.get_mut(&f.d).unwrap().routes.push(EdgeRoute {
|
||||||
|
edge: f.self_edge,
|
||||||
|
waypoints: pins.clone(),
|
||||||
|
label: Some(EdgeLabel {
|
||||||
|
fraction: 0.25,
|
||||||
|
offset: point(11., 7.),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
let before = f.p.clone();
|
||||||
|
let routed = route_diagram(&f.p, &f.p.diagrams[&f.d], &project_view(&f.p, f.d));
|
||||||
|
assert_eq!(routed.edges[&f.self_edge].pins, pins);
|
||||||
|
let mut previous = 0;
|
||||||
|
for pin in &pins {
|
||||||
|
let i = routed.edges[&f.self_edge]
|
||||||
|
.points
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.skip(previous)
|
||||||
|
.find(|(_, p)| *p == pin)
|
||||||
|
.unwrap()
|
||||||
|
.0;
|
||||||
|
previous = i + 1;
|
||||||
|
}
|
||||||
|
assert_eq!(f.p, before);
|
||||||
|
let laid = prepare_command(&f.p, &Command::Layout(f.d))
|
||||||
|
.unwrap()
|
||||||
|
.project;
|
||||||
|
let nodes = &laid.diagrams[&f.d].nodes;
|
||||||
|
let a = &nodes[&node(&laid, f.d, f.a)];
|
||||||
|
let inner = &nodes[&node(&laid, f.d, f.inner)];
|
||||||
|
assert!(inner.position.y >= a.position.y + state_header_height(&laid, f.a));
|
||||||
|
let first = language::format(&laid);
|
||||||
|
assert_eq!(language::parse(&first).unwrap(), laid);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reconnect_keeps_identity_and_validates_replacement_without_double_counting() {
|
||||||
|
let f = fixture();
|
||||||
|
let initial =
|
||||||
|
f.p.elements
|
||||||
|
.values()
|
||||||
|
.find(|e| e.kind() == Kind::Initial)
|
||||||
|
.unwrap()
|
||||||
|
.id;
|
||||||
|
let edge =
|
||||||
|
f.p.relations
|
||||||
|
.values()
|
||||||
|
.find(|r| r.source() == initial)
|
||||||
|
.unwrap();
|
||||||
|
let mut replacement = edge.clone();
|
||||||
|
if let RelationBody::StateTransition { target, .. } = &mut replacement.body {
|
||||||
|
*target = f.b;
|
||||||
|
}
|
||||||
|
let after = prepare_command(&f.p, &Command::EditRelation(edge.id, replacement.clone()))
|
||||||
|
.unwrap()
|
||||||
|
.project;
|
||||||
|
assert_eq!(after.relations[&edge.id], replacement);
|
||||||
|
let mut invalid = replacement.clone();
|
||||||
|
if let RelationBody::StateTransition { target, .. } = &mut invalid.body {
|
||||||
|
*target = initial;
|
||||||
|
}
|
||||||
|
assert!(prepare_command(&after, &Command::EditRelation(edge.id, invalid)).is_err());
|
||||||
|
assert_eq!(after.relations[&edge.id], replacement);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn moving_a_region_does_not_translate_pins_attached_to_a_stationary_state_boundary() {
|
||||||
|
let mut f = fixture();
|
||||||
|
let pins = vec![point(280., 390.), point(280., 470.)];
|
||||||
|
f.p.diagrams.get_mut(&f.d).unwrap().routes.push(EdgeRoute {
|
||||||
|
edge: f.local,
|
||||||
|
waypoints: pins.clone(),
|
||||||
|
label: None,
|
||||||
|
});
|
||||||
|
let before = project_view(&f.p, f.d);
|
||||||
|
let entry_before = before
|
||||||
|
.nodes
|
||||||
|
.iter()
|
||||||
|
.find(|n| n.element == f.entry)
|
||||||
|
.unwrap()
|
||||||
|
.position;
|
||||||
|
let region = node(&f.p, f.d, f.inner);
|
||||||
|
let old = f.p.diagrams[&f.d].nodes[®ion].position;
|
||||||
|
let after = prepare_command(
|
||||||
|
&f.p,
|
||||||
|
&Command::MoveNode {
|
||||||
|
diagram: f.d,
|
||||||
|
node: region,
|
||||||
|
position: point(old.x + 10., old.y + 10.),
|
||||||
|
with_descendants: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.project;
|
||||||
|
let projected = project_view(&after, f.d);
|
||||||
|
let entry_after = projected
|
||||||
|
.nodes
|
||||||
|
.iter()
|
||||||
|
.find(|n| n.element == f.entry)
|
||||||
|
.unwrap()
|
||||||
|
.position;
|
||||||
|
assert_eq!(
|
||||||
|
entry_after.x, entry_before.x,
|
||||||
|
"Entry stays on the unmoved composite boundary"
|
||||||
|
);
|
||||||
|
assert_eq!(after.diagrams[&f.d].routes[0].waypoints, pins);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn initial_transition_attaches_to_the_compact_symbol_and_preview_keeps_settled_geometry() {
|
||||||
|
let mut f = fixture();
|
||||||
|
let initial =
|
||||||
|
f.p.elements
|
||||||
|
.values()
|
||||||
|
.find(|e| e.kind() == Kind::Initial)
|
||||||
|
.unwrap()
|
||||||
|
.id;
|
||||||
|
let edge =
|
||||||
|
f.p.relations
|
||||||
|
.values()
|
||||||
|
.find(|r| r.source() == initial)
|
||||||
|
.unwrap()
|
||||||
|
.id;
|
||||||
|
let projected = project_view(&f.p, f.d);
|
||||||
|
let n = projected
|
||||||
|
.nodes
|
||||||
|
.iter()
|
||||||
|
.find(|n| n.element == initial)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
n.size,
|
||||||
|
Size {
|
||||||
|
width: 16.,
|
||||||
|
height: 16.
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let settled = route_diagram(&f.p, &f.p.diagrams[&f.d], &projected);
|
||||||
|
let first = settled.edges[&edge].points[0];
|
||||||
|
assert!(
|
||||||
|
[
|
||||||
|
point(n.position.x, n.position.y + 8.),
|
||||||
|
point(n.position.x + 16., n.position.y + 8.),
|
||||||
|
point(n.position.x + 8., n.position.y),
|
||||||
|
point(n.position.x + 8., n.position.y + 16.)
|
||||||
|
]
|
||||||
|
.contains(&first)
|
||||||
|
);
|
||||||
|
f.p.diagrams.get_mut(&f.d).unwrap().routes.push(EdgeRoute {
|
||||||
|
edge: f.self_edge,
|
||||||
|
waypoints: vec![],
|
||||||
|
label: Some(EdgeLabel {
|
||||||
|
fraction: 0.3,
|
||||||
|
offset: point(11., 9.),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
let preview = route_edge_preview(&f.p, &f.p.diagrams[&f.d], &projected, &settled, f.self_edge);
|
||||||
|
assert_eq!(
|
||||||
|
preview.edges[&f.self_edge].points,
|
||||||
|
settled.edges[&f.self_edge].points
|
||||||
|
);
|
||||||
|
assert_eq!(preview.edges[&edge], settled.edges[&edge]);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user