Add direct diagram connections, movable ports, and contextual authoring
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
//! A palette operation is transient; only a completed connection enters history.
|
||||
use crate::{connection_tool::Tool, route_edit::Context};
|
||||
use oa_core::{Id, Project, projection::ProjectedView};
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Event {
|
||||
Choose(Option<Tool>),
|
||||
Begin(Tool, Id),
|
||||
Endpoint(Context, Id),
|
||||
Restart(Context),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Session {
|
||||
pub context: Context,
|
||||
pub tool: Tool,
|
||||
pub source: Option<Id>,
|
||||
pub candidates: BTreeSet<Id>,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
pub fn new(project: &Project, view: &ProjectedView, context: Context, tool: Tool) -> Self {
|
||||
let candidates = visible(view)
|
||||
.filter(|id| tool.can_start(project, *id))
|
||||
.collect();
|
||||
Self {
|
||||
context,
|
||||
tool,
|
||||
source: None,
|
||||
candidates,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(&mut self, project: &Project, view: &ProjectedView, source: Id) {
|
||||
self.source = Some(source);
|
||||
let compatible = self.tool.targets(project, source);
|
||||
self.candidates = visible(view).filter(|id| compatible.contains(id)).collect();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn visible(view: &ProjectedView) -> impl Iterator<Item = Id> + '_ {
|
||||
view.nodes
|
||||
.iter()
|
||||
.map(|n| n.element)
|
||||
.chain(view.ports.iter().map(|p| p.element))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::app::{App, Message, Page};
|
||||
use oa_core::*;
|
||||
|
||||
fn fixture() -> (App, Id, Id, Id) {
|
||||
let mut p = Project::empty();
|
||||
let a = p.add(Kind::Function, "Send command", 0., 0.);
|
||||
let b = p.add(Kind::Function, "Receive command", 400., 0.);
|
||||
let hidden = p
|
||||
.add_element(Kind::Function, Layer::Sa, "Hidden work", None)
|
||||
.unwrap();
|
||||
(App::with_project(p), a, b, hidden)
|
||||
}
|
||||
fn send(app: &mut App, event: Event) {
|
||||
let _ = app.update(Message::Connection(event));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn visible_endpoint_drawing_commits_ports_and_exchange_once_with_exact_history() {
|
||||
let (mut app, a, b, hidden) = fixture();
|
||||
let before = app.document.bundle().clone();
|
||||
let revision = app.document.revision();
|
||||
send(&mut app, Event::Choose(Some(Tool::FunctionalExchange)));
|
||||
assert!(
|
||||
!app.connection
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.candidates
|
||||
.contains(&hidden)
|
||||
);
|
||||
let context = app.route_context().unwrap();
|
||||
send(&mut app, Event::Endpoint(context, a));
|
||||
send(&mut app, Event::Endpoint(context, hidden));
|
||||
assert_eq!(app.document.revision(), revision);
|
||||
assert_eq!(app.connection.as_ref().unwrap().source, Some(a));
|
||||
send(&mut app, Event::Endpoint(context, b));
|
||||
assert!(app.connection.is_none());
|
||||
assert_eq!(app.document.revision(), revision + 1);
|
||||
let relation = &app.project().relations[&app.selected];
|
||||
assert_eq!(relation.kind(), RelationKind::FunctionalExchange);
|
||||
assert_eq!((relation.source(), relation.target()), (a, b));
|
||||
assert_eq!(
|
||||
app.project()
|
||||
.elements
|
||||
.values()
|
||||
.filter(|e| e.kind() == Kind::Port)
|
||||
.count(),
|
||||
2
|
||||
);
|
||||
let after = app.document.bundle().clone();
|
||||
let _ = app.update(Message::Undo);
|
||||
assert_eq!(*app.document.bundle(), before);
|
||||
let _ = app.update(Message::Redo);
|
||||
assert_eq!(*app.document.bundle(), after);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancel_navigation_revision_and_dirty_drafts_cannot_commit_a_stale_connection() {
|
||||
let (mut app, a, b, _) = fixture();
|
||||
send(&mut app, Event::Choose(Some(Tool::FunctionalExchange)));
|
||||
let context = app.route_context().unwrap();
|
||||
send(&mut app, Event::Endpoint(context, a));
|
||||
let _ = app.update(Message::Escape);
|
||||
send(&mut app, Event::Endpoint(context, b));
|
||||
assert!(app.project().relations.is_empty());
|
||||
send(&mut app, Event::Choose(Some(Tool::FunctionalExchange)));
|
||||
let current = app.route_context().unwrap();
|
||||
send(&mut app, Event::Endpoint(current, a));
|
||||
send(&mut app, Event::Endpoint(context, b));
|
||||
assert_eq!(
|
||||
app.connection.as_ref().unwrap().source,
|
||||
Some(a),
|
||||
"A stale click must leave the newer drawing session intact"
|
||||
);
|
||||
let revision = app.document.revision();
|
||||
let _ = app.update(Message::Route(crate::route_edit::Event::AddBend));
|
||||
assert_eq!(app.document.revision(), revision);
|
||||
assert!(app.connection.is_some());
|
||||
let _ = app.update(Message::Page(Page::Catalog));
|
||||
assert!(app.connection.is_none());
|
||||
let _ = app.update(Message::Page(Page::Diagram));
|
||||
send(&mut app, Event::Choose(Some(Tool::FunctionalExchange)));
|
||||
send(&mut app, Event::Endpoint(context, b));
|
||||
assert!(app.project().relations.is_empty());
|
||||
let _ = app.update(Message::Select(a));
|
||||
let _ = app.update(Message::Field("/name".into(), "Pending rename".into()));
|
||||
assert!(app.draft_dirty());
|
||||
send(&mut app, Event::Choose(Some(Tool::FunctionalExchange)));
|
||||
assert!(app.connection.is_none());
|
||||
assert_eq!(app.project().named(a), "Send command");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user