Add direct diagram connections, movable ports, and contextual authoring
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
//! Border dragging changes diagram presentation, never semantic ownership.
|
||||
use crate::route_edit::{Context, point, position};
|
||||
use iced::Point;
|
||||
use oa_core::{projection::*, *};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Event {
|
||||
Commit(Context, Id, PortAnchor),
|
||||
Reset(Context, Id, Id),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Gesture {
|
||||
pub context: Context,
|
||||
pub owner: ProjectedNode,
|
||||
pub anchor: PortAnchor,
|
||||
pub start: Point,
|
||||
pub changed: bool,
|
||||
}
|
||||
impl Gesture {
|
||||
pub fn new(context: Context, owner: ProjectedNode, port: &ProjectedPort, start: Point) -> Self {
|
||||
let anchor = PortAnchor {
|
||||
port: port.element,
|
||||
side: port.side,
|
||||
offset: port_anchor_offset(&owner, port.side, port.position),
|
||||
};
|
||||
Self {
|
||||
context,
|
||||
owner,
|
||||
anchor,
|
||||
start,
|
||||
changed: false,
|
||||
}
|
||||
}
|
||||
pub fn move_to(&mut self, cursor: Point, zoom: f32) {
|
||||
if !self.changed && cursor.distance(self.start) * zoom < 3. {
|
||||
return;
|
||||
}
|
||||
self.changed = true;
|
||||
let distance = |side| {
|
||||
let offset = port_anchor_offset(&self.owner, side, position(cursor));
|
||||
cursor.distance(point(port_anchor_position(&self.owner, side, offset)))
|
||||
};
|
||||
let nearest = [
|
||||
PortAnchorSide::Left,
|
||||
PortAnchorSide::Right,
|
||||
PortAnchorSide::Top,
|
||||
PortAnchorSide::Bottom,
|
||||
]
|
||||
.into_iter()
|
||||
.min_by(|a, b| distance(*a).total_cmp(&distance(*b)))
|
||||
.unwrap();
|
||||
// Six screen pixels of hysteresis avoids flickering between corner sides.
|
||||
if distance(self.anchor.side) > distance(nearest) + 6. / zoom {
|
||||
self.anchor.side = nearest;
|
||||
}
|
||||
self.anchor.offset =
|
||||
(port_anchor_offset(&self.owner, self.anchor.side, position(cursor)) * 100.).round()
|
||||
/ 100.;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_anchor(diagram: &mut Diagram, node: Id, anchor: PortAnchor) {
|
||||
if let Some(node) = diagram.nodes.get_mut(&node) {
|
||||
if let Some(previous) = node.port_anchors.iter_mut().find(|a| a.port == anchor.port) {
|
||||
*previous = anchor;
|
||||
} else {
|
||||
node.port_anchors.push(anchor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Graphical ownership edits intentionally reset anchors left on the old owner.
|
||||
/// Raw source/API edits remain strict and must explicitly repair their anchors.
|
||||
pub fn repair_owner_change(project: &Project, command: Command) -> Command {
|
||||
let change = match &command {
|
||||
Command::Reparent { element, parent } => Some((*element, *parent)),
|
||||
Command::EditElement(original, element) => Some((*original, element.parent)),
|
||||
_ => None,
|
||||
};
|
||||
let Some((port, parent)) = change.filter(|(id, parent)| {
|
||||
project
|
||||
.elements
|
||||
.get(id)
|
||||
.is_some_and(|e| e.kind() == Kind::Port && e.parent != *parent)
|
||||
}) else {
|
||||
return command;
|
||||
};
|
||||
let mut commands: Vec<_> = project
|
||||
.diagrams
|
||||
.values()
|
||||
.flat_map(|d| {
|
||||
d.nodes
|
||||
.values()
|
||||
.filter(|n| {
|
||||
Some(n.element) != parent && n.port_anchors.iter().any(|a| a.port == port)
|
||||
})
|
||||
.map(move |n| Command::RemovePortAnchor {
|
||||
diagram: d.id,
|
||||
node: n.id,
|
||||
port,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
if commands.is_empty() {
|
||||
return command;
|
||||
}
|
||||
commands.push(command);
|
||||
Command::Batch(commands)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::app::{App, Message};
|
||||
|
||||
fn fixture() -> (App, Id, Id, Id) {
|
||||
let mut p = Project::empty();
|
||||
let a = p.add(Kind::Function, "Transmit", 100., 100.);
|
||||
let b = p.add(Kind::Function, "Alternate owner", 500., 100.);
|
||||
let port = Id::new_v4();
|
||||
let element = make_port(
|
||||
&p,
|
||||
port,
|
||||
a,
|
||||
"Output",
|
||||
PortContract::FunctionOutput {
|
||||
outgoing_items: vec![],
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
p.elements.insert(port, element);
|
||||
(App::with_project(p), a, b, port)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_border_drags_commit_exact_preview_and_restore_automatic_anchors_in_history() {
|
||||
for side in [
|
||||
PortAnchorSide::Left,
|
||||
PortAnchorSide::Right,
|
||||
PortAnchorSide::Top,
|
||||
PortAnchorSide::Bottom,
|
||||
] {
|
||||
let (mut app, owner, _, port_id) = fixture();
|
||||
let context = app.route_context().unwrap();
|
||||
let view = project_view(app.project(), context.diagram);
|
||||
let node = view.nodes.iter().find(|n| n.element == owner).unwrap();
|
||||
let port = view.ports.iter().find(|p| p.element == port_id).unwrap();
|
||||
let mut gesture = Gesture::new(context, node.clone(), port, point(port.position));
|
||||
let end = port_anchor_position(node, side, 0.25);
|
||||
gesture.move_to(point(end), 0.7);
|
||||
assert_eq!(gesture.anchor.side, side);
|
||||
assert_eq!(gesture.anchor.offset, 0.25);
|
||||
let before = app.document.bundle().clone();
|
||||
let _ = app.update(Message::PortAnchor(Event::Commit(
|
||||
context,
|
||||
node.id,
|
||||
gesture.anchor,
|
||||
)));
|
||||
let after = app.document.bundle().clone();
|
||||
let projected = project_view(app.project(), context.diagram);
|
||||
assert_eq!(
|
||||
projected.nodes.iter().find(|n| n.id == node.id).unwrap(),
|
||||
node
|
||||
);
|
||||
assert_eq!(
|
||||
projected
|
||||
.ports
|
||||
.iter()
|
||||
.find(|p| p.element == port_id)
|
||||
.unwrap()
|
||||
.position,
|
||||
end
|
||||
);
|
||||
let _ = app.update(Message::Undo);
|
||||
assert_eq!(*app.document.bundle(), before);
|
||||
let _ = app.update(Message::Redo);
|
||||
assert_eq!(*app.document.bundle(), after);
|
||||
let latest = app.route_context().unwrap();
|
||||
let _ = app.update(Message::PortAnchor(Event::Reset(latest, node.id, port_id)));
|
||||
assert!(
|
||||
app.project().diagrams[&context.diagram].nodes[&node.id]
|
||||
.port_anchors
|
||||
.is_empty()
|
||||
);
|
||||
let _ = app.update(Message::Undo);
|
||||
assert_eq!(*app.document.bundle(), after);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graphical_port_reparent_clears_old_anchors_in_the_same_history_operation() {
|
||||
let (mut app, owner, next, port) = fixture();
|
||||
let context = app.route_context().unwrap();
|
||||
let node = project_view(app.project(), context.diagram)
|
||||
.nodes
|
||||
.iter()
|
||||
.find(|n| n.element == owner)
|
||||
.unwrap()
|
||||
.id;
|
||||
let _ = app.update(Message::PortAnchor(Event::Commit(
|
||||
context,
|
||||
node,
|
||||
PortAnchor {
|
||||
port,
|
||||
side: PortAnchorSide::Top,
|
||||
offset: 0.5,
|
||||
},
|
||||
)));
|
||||
let before = app.document.bundle().clone();
|
||||
let revision = app.document.revision();
|
||||
let _ = app.update(Message::Command(Command::Reparent {
|
||||
element: port,
|
||||
parent: Some(next),
|
||||
}));
|
||||
assert_eq!(app.document.revision(), revision + 1, "{}", app.status);
|
||||
assert_eq!(app.project().elements[&port].parent, Some(next));
|
||||
assert!(
|
||||
app.project().diagrams[&context.diagram].nodes[&node]
|
||||
.port_anchors
|
||||
.is_empty()
|
||||
);
|
||||
let _ = app.update(Message::Undo);
|
||||
assert_eq!(*app.document.bundle(), before);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user