//! 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 { 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, 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 } }