161 lines
5.8 KiB
Rust
161 lines
5.8 KiB
Rust
//! One boundary definition shared by persisted routing and attachment gestures.
|
|
use super::*;
|
|
use crate::{EdgeAttachment, EdgeEnd, Kind, PortAnchorSide};
|
|
|
|
/// Semantic authority, independent of whether projection currently exposes a port.
|
|
pub fn semantic_port_endpoint(project: &Project, edge: Id, end: EdgeEnd) -> Option<Id> {
|
|
let endpoint = if let Some(relation) = project.relations.get(&edge) {
|
|
match (&relation.body, end) {
|
|
(RelationBody::FunctionalExchange { source_port, .. }, EdgeEnd::Source) => *source_port,
|
|
(RelationBody::FunctionalExchange { target_port, .. }, EdgeEnd::Target) => *target_port,
|
|
(_, EdgeEnd::Source) => relation.source(),
|
|
(_, EdgeEnd::Target) => relation.target(),
|
|
}
|
|
} else {
|
|
let trace = project.traces.get(&edge)?;
|
|
match end {
|
|
EdgeEnd::Source => trace.oa,
|
|
EdgeEnd::Target => trace.sa,
|
|
}
|
|
};
|
|
project
|
|
.elements
|
|
.get(&endpoint)
|
|
.filter(|e| e.kind() == Kind::Port)
|
|
.map(|_| endpoint)
|
|
}
|
|
|
|
/// Intersect the chosen cardinal scan line with the painted shape. Offset is
|
|
/// measured over the complete projected bounding side, including its corners.
|
|
/// Terminate's X uses its outermost stroke intersection rather than a contour.
|
|
pub fn attachment_point(
|
|
project: &Project,
|
|
diagram: &Diagram,
|
|
node: &ProjectedNode,
|
|
attachment: &EdgeAttachment,
|
|
) -> Position {
|
|
let vertical = matches!(
|
|
attachment.side,
|
|
PortAnchorSide::Left | PortAnchorSide::Right
|
|
);
|
|
let (extent, depth) = if vertical {
|
|
(node.size.height, node.size.width)
|
|
} else {
|
|
(node.size.width, node.size.height)
|
|
};
|
|
let along = attachment.offset.clamp(0., 1.) * extent;
|
|
let t = 2. * attachment.offset.clamp(0., 1.) - 1.;
|
|
let kind = diagram
|
|
.projection
|
|
.state_machine()
|
|
.and_then(|_| project.elements.get(&node.element))
|
|
.map(|e| e.kind());
|
|
let inset = match kind {
|
|
Some(Kind::Choice) => depth * 0.5 * t.abs(),
|
|
Some(Kind::Terminate) => {
|
|
depth
|
|
* attachment
|
|
.offset
|
|
.clamp(0., 1.)
|
|
.min(1. - attachment.offset.clamp(0., 1.))
|
|
}
|
|
Some(
|
|
Kind::Initial
|
|
| Kind::Junction
|
|
| Kind::Final
|
|
| Kind::ShallowHistory
|
|
| Kind::DeepHistory
|
|
| Kind::EntryPoint
|
|
| Kind::ExitPoint,
|
|
) => depth * 0.5 * (1. - (1. - t * t).max(0.).sqrt()),
|
|
Some(Kind::Region | Kind::Fork | Kind::Join) => 0.,
|
|
_ => {
|
|
let radius = 10_f32.min(extent / 2.).min(depth / 2.);
|
|
let corner = (radius - along.min(extent - along)).max(0.);
|
|
radius - (radius * radius - corner * corner).max(0.).sqrt()
|
|
}
|
|
};
|
|
match attachment.side {
|
|
PortAnchorSide::Left => pos(node.position.x + inset, node.position.y + along),
|
|
PortAnchorSide::Right => pos(
|
|
node.position.x + node.size.width - inset,
|
|
node.position.y + along,
|
|
),
|
|
PortAnchorSide::Top => pos(node.position.x + along, node.position.y + inset),
|
|
PortAnchorSide::Bottom => pos(
|
|
node.position.x + along,
|
|
node.position.y + node.size.height - inset,
|
|
),
|
|
}
|
|
}
|
|
|
|
/// Choose a cardinal bounding side, then map its fraction onto the painted
|
|
/// contour. Contour distances cannot distinguish the sides of a circle at its
|
|
/// extrema: Right/0 and Top/0.5 denote the same point but different lead directions.
|
|
/// Hysteresis is in diagram units, capped for small symbols. Only UI snapping
|
|
/// rounds to hundredths: source/API values retain their complete precision.
|
|
pub fn attachment_from_point(
|
|
node: &ProjectedNode,
|
|
cursor: Position,
|
|
previous: Option<PortAnchorSide>,
|
|
hysteresis: f32,
|
|
) -> EdgeAttachment {
|
|
let candidate = |side| EdgeAttachment {
|
|
side,
|
|
offset: projection::port_anchor_offset(node, side, cursor),
|
|
};
|
|
// Measure side intent at the box, not from a distant overshoot: Euclidean
|
|
// distance differences converge outside a small symbol and would otherwise
|
|
// let hysteresis retain a perpendicular side indefinitely.
|
|
let boundary_cursor = pos(
|
|
cursor
|
|
.x
|
|
.clamp(node.position.x, node.position.x + node.size.width),
|
|
cursor
|
|
.y
|
|
.clamp(node.position.y, node.position.y + node.size.height),
|
|
);
|
|
let distance = |side| {
|
|
length(
|
|
projection::port_anchor_position(node, side, candidate(side).offset),
|
|
boundary_cursor,
|
|
)
|
|
};
|
|
let mut side = [
|
|
PortAnchorSide::Left,
|
|
PortAnchorSide::Right,
|
|
PortAnchorSide::Top,
|
|
PortAnchorSide::Bottom,
|
|
]
|
|
.into_iter()
|
|
.min_by(|a, b| distance(*a).total_cmp(&distance(*b)))
|
|
.unwrap();
|
|
let hysteresis = hysteresis
|
|
.max(0.)
|
|
.min(node.size.width.min(node.size.height) * 0.2);
|
|
if let Some(old) = previous
|
|
&& distance(old) <= distance(side) + hysteresis
|
|
{
|
|
side = old;
|
|
}
|
|
let mut attachment = candidate(side);
|
|
attachment.offset = (attachment.offset * 100.).round() / 100.;
|
|
attachment
|
|
}
|
|
|
|
pub(super) fn endpoint(
|
|
project: &Project,
|
|
diagram: &Diagram,
|
|
node: &ProjectedNode,
|
|
attachment: EdgeAttachment,
|
|
) -> Endpoint {
|
|
let point = attachment_point(project, diagram, node, &attachment);
|
|
let stub = match attachment.side {
|
|
PortAnchorSide::Left => pos(node.position.x - PORT_STUB, point.y),
|
|
PortAnchorSide::Right => pos(node.position.x + node.size.width + PORT_STUB, point.y),
|
|
PortAnchorSide::Top => pos(point.x, node.position.y - PORT_STUB),
|
|
PortAnchorSide::Bottom => pos(point.x, node.position.y + node.size.height + PORT_STUB),
|
|
};
|
|
Endpoint { point, stub }
|
|
}
|