Bound Process payload sizes and capture rendered order feedback
Box the optional Process menu context and changed order command without changing identity or command semantics. Add opt-in actual WGPU readbacks to the split-event OA/SA reorder evaluator. Strict all-target lint, format and affected desktop paths pass; native acceptance follows.
This commit is contained in:
@@ -2388,7 +2388,7 @@ impl App {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
if request.process.is_some() {
|
if request.process.is_some() {
|
||||||
request.process = self.topology_context();
|
request.process = self.topology_context().map(Box::new);
|
||||||
}
|
}
|
||||||
self.model_menu = crate::model_menu::Opened::new(self, request, self.menu_sequence);
|
self.model_menu = crate::model_menu::Opened::new(self, request, self.menu_sequence);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ impl App {
|
|||||||
match topology::reorder(self.project(), context.owner, occurrence, before) {
|
match topology::reorder(self.project(), context.owner, occurrence, before) {
|
||||||
Ok(topology::group_reorder::Plan::Noop) => {}
|
Ok(topology::group_reorder::Plan::Noop) => {}
|
||||||
Ok(topology::group_reorder::Plan::Change(command)) => {
|
Ok(topology::group_reorder::Plan::Change(command)) => {
|
||||||
if self.execute(command) {
|
if self.execute(*command) {
|
||||||
self.selected = occurrence;
|
self.selected = occurrence;
|
||||||
self.refresh_draft();
|
self.refresh_draft();
|
||||||
return self.reveal_process_selection();
|
return self.reveal_process_selection();
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ impl App {
|
|||||||
{
|
{
|
||||||
Ok(Plan::Noop) => {}
|
Ok(Plan::Noop) => {}
|
||||||
Ok(Plan::Change(command)) => {
|
Ok(Plan::Change(command)) => {
|
||||||
if self.execute(command) {
|
if self.execute(*command) {
|
||||||
self.inspect_process_multiselect_primary();
|
self.inspect_process_multiselect_primary();
|
||||||
self.status = format!(
|
self.status = format!(
|
||||||
"Reordered {} contributions · Undo restores their order",
|
"Reordered {} contributions · Undo restores their order",
|
||||||
|
|||||||
@@ -602,7 +602,7 @@ fn menu(app: &mut App, target: Target, label: &str) -> Message {
|
|||||||
_ => panic!("This fixture opens contribution menus"),
|
_ => panic!("This fixture opens contribution menus"),
|
||||||
};
|
};
|
||||||
let mut request = model_menu::Request::object(object, iced::Point::new(180., 150.));
|
let mut request = model_menu::Request::object(object, iced::Point::new(180., 150.));
|
||||||
request.process = app.topology_context();
|
request.process = app.topology_context().map(Box::new);
|
||||||
request.selection_target = Some(Box::new(target));
|
request.selection_target = Some(Box::new(target));
|
||||||
let _ = app.update(Message::Menu(model_menu::Event::Open(request)));
|
let _ = app.update(Message::Menu(model_menu::Event::Open(request)));
|
||||||
let opened = app
|
let opened = app
|
||||||
@@ -610,7 +610,10 @@ fn menu(app: &mut App, target: Target, label: &str) -> Message {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.expect("Captured Process menu must survive the open epoch change");
|
.expect("Captured Process menu must survive the open epoch change");
|
||||||
assert!(opened.valid(app));
|
assert!(opened.valid(app));
|
||||||
assert_eq!(opened.request.process, app.topology_context());
|
assert_eq!(
|
||||||
|
opened.request.process.as_deref().copied(),
|
||||||
|
app.topology_context()
|
||||||
|
);
|
||||||
model_menu::popup(app)
|
model_menu::popup(app)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.entries
|
.entries
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ impl Graph<'_> {
|
|||||||
};
|
};
|
||||||
let mut request = crate::model_menu::Request::object(object, pointer);
|
let mut request = crate::model_menu::Request::object(object, pointer);
|
||||||
request.selection_target = Some(Box::new(target));
|
request.selection_target = Some(Box::new(target));
|
||||||
request.process = Some(context);
|
request.process = Some(Box::new(context));
|
||||||
return Some(
|
return Some(
|
||||||
Action::publish(Message::Menu(crate::model_menu::Event::Open(request)))
|
Action::publish(Message::Menu(crate::model_menu::Event::Open(request)))
|
||||||
.and_capture(),
|
.and_capture(),
|
||||||
|
|||||||
@@ -127,14 +127,16 @@ fn group_drag_uses_midpoint_anchors_and_original_order_and_singleton_after_gap()
|
|||||||
};
|
};
|
||||||
assert_eq!(snapshot, graph.selection.clone().unwrap());
|
assert_eq!(snapshot, graph.selection.clone().unwrap());
|
||||||
assert_eq!(destination, Destination::After(f.nodes[1]));
|
assert_eq!(destination, Destination::After(f.nodes[1]));
|
||||||
let Plan::Change(Command::SetSteps(owner, steps)) =
|
let Plan::Change(command) = Prepared::new(&f.p, f.owner, snapshot.targets())
|
||||||
Prepared::new(&f.p, f.owner, snapshot.targets())
|
.unwrap()
|
||||||
.unwrap()
|
.plan(destination)
|
||||||
.plan(destination)
|
.unwrap()
|
||||||
.unwrap()
|
|
||||||
else {
|
else {
|
||||||
panic!("changed steps");
|
panic!("changed steps");
|
||||||
};
|
};
|
||||||
|
let Command::SetSteps(owner, steps) = *command else {
|
||||||
|
panic!("expected SetSteps");
|
||||||
|
};
|
||||||
assert_eq!(owner, f.owner);
|
assert_eq!(owner, f.owner);
|
||||||
assert_eq!(steps, vec![f.nodes[1], f.nodes[0], f.nodes[2]]);
|
assert_eq!(steps, vec![f.nodes[1], f.nodes[0], f.nodes[2]]);
|
||||||
}
|
}
|
||||||
@@ -302,7 +304,7 @@ fn qualified_occurrences_and_visible_preview_links_keep_exact_invocation_paths()
|
|||||||
matches!(press(&graph, &mut input, at), Some(Message::DiagramSelection(SelectionEvent::Choose(_, target, Choice::Toggle))) if target == expected)
|
matches!(press(&graph, &mut input, at), Some(Message::DiagramSelection(SelectionEvent::Choose(_, target, Choice::Toggle))) if target == expected)
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
matches!(send(&graph, &mut input, canvas::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)), at), Some(Message::Menu(crate::model_menu::Event::Open(request))) if request.selection_target.as_deref() == Some(&expected) && request.process == graph.interaction.map(|(context,_,_)| context))
|
matches!(send(&graph, &mut input, canvas::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)), at), Some(Message::Menu(crate::model_menu::Event::Open(request))) if request.selection_target.as_deref() == Some(&expected) && request.process.as_deref().copied() == graph.interaction.map(|(context,_,_)| context))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
assert_ne!(targets[0], targets[1]);
|
assert_ne!(targets[0], targets[1]);
|
||||||
|
|||||||
@@ -365,7 +365,7 @@ fn direct_process_reconnect_delete_and_reorder_preserve_other_paths_and_sources(
|
|||||||
else {
|
else {
|
||||||
panic!("changed order");
|
panic!("changed order");
|
||||||
};
|
};
|
||||||
let reordered = execute_batch(&f.p, &[command]).unwrap();
|
let reordered = execute_batch(&f.p, &[*command]).unwrap();
|
||||||
assert_eq!(reordered.elements[&f.owner].steps().unwrap()[0], f.nodes[2]);
|
assert_eq!(reordered.elements[&f.owner].steps().unwrap()[0], f.nodes[2]);
|
||||||
assert_eq!(reordered.relations, f.p.relations);
|
assert_eq!(reordered.relations, f.p.relations);
|
||||||
assert!(direct::reorder(&f.p, f.owner, f.nodes[0], f.nodes[1]).is_err());
|
assert!(direct::reorder(&f.p, f.owner, f.nodes[0], f.nodes[1]).is_err());
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ pub(crate) enum Destination {
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) enum Plan {
|
pub(crate) enum Plan {
|
||||||
Noop,
|
Noop,
|
||||||
Change(Command),
|
Change(Box<Command>),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Captured semantic facts only: no project clone, projection or routed scene.
|
/// Captured semantic facts only: no project clone, projection or routed scene.
|
||||||
@@ -156,7 +156,9 @@ impl Prepared {
|
|||||||
"This reorder would change process columns; the whole move was refused.".into(),
|
"This reorder would change process columns; the whole move was refused.".into(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Ok(Plan::Change(Command::SetSteps(self.owner, ordered)))
|
Ok(Plan::Change(Box::new(Command::SetSteps(
|
||||||
|
self.owner, ordered,
|
||||||
|
))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,8 +56,8 @@ fn changed(project: &Project, owner: Id, targets: &[Target], drop: Destination)
|
|||||||
let Plan::Change(command) = plan(project, owner, targets, drop).unwrap() else {
|
let Plan::Change(command) = plan(project, owner, targets, drop).unwrap() else {
|
||||||
panic!("Expected an actual order change");
|
panic!("Expected an actual order change");
|
||||||
};
|
};
|
||||||
assert!(matches!(&command, Command::SetSteps(id, _) if *id == owner));
|
assert!(matches!(command.as_ref(), Command::SetSteps(id, _) if *id == owner));
|
||||||
command
|
*command
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -395,7 +395,7 @@ fn process_group_columns_parallel_links_keep_exact_ranks_and_relation_records()
|
|||||||
let Plan::Change(command) = prepared.plan(Destination::Before(a)).unwrap() else {
|
let Plan::Change(command) = prepared.plan(Destination::Before(a)).unwrap() else {
|
||||||
panic!("Expected move");
|
panic!("Expected move");
|
||||||
};
|
};
|
||||||
let result = execute_batch(&project, &[command]).unwrap();
|
let result = execute_batch(&project, &[*command]).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.elements[&owner].steps().unwrap(),
|
result.elements[&owner].steps().unwrap(),
|
||||||
&vec![b, u, c, v, a]
|
&vec![b, u, c, v, a]
|
||||||
|
|||||||
@@ -192,6 +192,47 @@ async fn production_process_multiselect_reverse_click_group_drop_has_one_history
|
|||||||
let snapshot = app.multiselect_snapshot().unwrap();
|
let snapshot = app.multiselect_snapshot().unwrap();
|
||||||
let source = app.document.bundle().clone();
|
let source = app.document.bundle().clone();
|
||||||
let camera = (app.behavior.topology.pan, app.behavior.topology.zoom);
|
let camera = (app.behavior.topology.pan, app.behavior.topology.zoom);
|
||||||
|
// Reuse the existing opt-in raw WGPU evidence convention. These are
|
||||||
|
// actual production-tree pixels, not a reconstructed preview image.
|
||||||
|
let record_feedback = |stage: &str, mounted: &mut Mounted, app: &App, cursor: Point| {
|
||||||
|
let Some(path) = std::env::var_os("ARCADIA_RENDER_EVIDENCE_DIR") else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let path = std::path::PathBuf::from(path);
|
||||||
|
std::fs::create_dir_all(&path).unwrap();
|
||||||
|
let name = format!("process-{layer:?}-{stage}");
|
||||||
|
let pixels = mounted.pixels_at(app, mouse::Cursor::Available(cursor));
|
||||||
|
let width = app.window_size.width as u32;
|
||||||
|
let height = app.window_size.height as u32;
|
||||||
|
assert_eq!(pixels.len(), width as usize * height as usize * 4);
|
||||||
|
let mut file = std::fs::OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.create_new(true)
|
||||||
|
.open(path.join(format!("{name}.rgba")))
|
||||||
|
.unwrap();
|
||||||
|
std::io::Write::write_all(&mut file, &pixels).unwrap();
|
||||||
|
let metadata = serde_json::json!({
|
||||||
|
"width": width, "height": height, "format": "RGBA8",
|
||||||
|
"stage": stage, "layer": format!("{layer:?}"),
|
||||||
|
"canvas": [bounds.x, bounds.y, bounds.width, bounds.height],
|
||||||
|
"cursor": [cursor.x, cursor.y], "scale": scale,
|
||||||
|
"camera": [app.behavior.topology.pan.x, app.behavior.topology.pan.y],
|
||||||
|
"selectionCount": snapshot.targets().len(),
|
||||||
|
"selection": format!("{snapshot:?}"),
|
||||||
|
"source": app.document.bundle(),
|
||||||
|
});
|
||||||
|
let mut file = std::fs::OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.create_new(true)
|
||||||
|
.open(path.join(format!("{name}.json")))
|
||||||
|
.unwrap();
|
||||||
|
std::io::Write::write_all(&mut file, &serde_json::to_vec_pretty(&metadata).unwrap())
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(app.document.bundle(), &source);
|
||||||
|
assert_eq!(app.multiselect_snapshot().unwrap(), snapshot);
|
||||||
|
assert!(!app.document.can_undo());
|
||||||
|
};
|
||||||
|
record_feedback("before-press", &mut mounted, &app, points[1]);
|
||||||
let pressed = mounted
|
let pressed = mounted
|
||||||
.batch(
|
.batch(
|
||||||
&mut app,
|
&mut app,
|
||||||
@@ -235,6 +276,7 @@ async fn production_process_multiselect_reverse_click_group_drop_has_one_history
|
|||||||
(app.behavior.topology.pan, app.behavior.topology.zoom),
|
(app.behavior.topology.pan, app.behavior.topology.zoom),
|
||||||
camera
|
camera
|
||||||
);
|
);
|
||||||
|
record_feedback("after-motion", &mut mounted, &app, points[2]);
|
||||||
let messages = mounted
|
let messages = mounted
|
||||||
.batch(
|
.batch(
|
||||||
&mut app,
|
&mut app,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ pub struct Request {
|
|||||||
pub route_segment: Option<(usize, Position)>,
|
pub route_segment: Option<(usize, Position)>,
|
||||||
pub creation_point: Option<Position>,
|
pub creation_point: Option<Position>,
|
||||||
pub canvas: Option<route_edit::Context>,
|
pub canvas: Option<route_edit::Context>,
|
||||||
pub process: Option<crate::behavior::topology::Context>,
|
pub process: Option<Box<crate::behavior::topology::Context>>,
|
||||||
pub source_file: Option<String>,
|
pub source_file: Option<String>,
|
||||||
}
|
}
|
||||||
impl Request {
|
impl Request {
|
||||||
@@ -65,7 +65,8 @@ impl Opened {
|
|||||||
.is_some_and(|c| Some(c) != current_canvas(app))
|
.is_some_and(|c| Some(c) != current_canvas(app))
|
||||||
|| request
|
|| request
|
||||||
.process
|
.process
|
||||||
.is_some_and(|c| Some(c) != app.topology_context())
|
.as_deref()
|
||||||
|
.is_some_and(|c| Some(*c) != app.topology_context())
|
||||||
{
|
{
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -96,7 +97,8 @@ impl Opened {
|
|||||||
&& self
|
&& self
|
||||||
.request
|
.request
|
||||||
.process
|
.process
|
||||||
.is_none_or(|c| Some(c) == app.topology_context())
|
.as_deref()
|
||||||
|
.is_none_or(|c| Some(*c) == app.topology_context())
|
||||||
&& self
|
&& self
|
||||||
.request
|
.request
|
||||||
.source_file
|
.source_file
|
||||||
@@ -181,7 +183,7 @@ pub fn trigger(app: &App) -> iced::Element<'_, Message> {
|
|||||||
request.canvas = app.route_context();
|
request.canvas = app.route_context();
|
||||||
} else if app.page == Page::Behavior {
|
} else if app.page == Page::Behavior {
|
||||||
request.canvas = app.state_context();
|
request.canvas = app.state_context();
|
||||||
request.process = app.topology_context();
|
request.process = app.topology_context().map(Box::new);
|
||||||
if let Some(target) = app.diagram_selection.as_ref().and_then(|s| s.primary()) {
|
if let Some(target) = app.diagram_selection.as_ref().and_then(|s| s.primary()) {
|
||||||
request.selection_target = Some(Box::new(target.clone()));
|
request.selection_target = Some(Box::new(target.clone()));
|
||||||
request.placement = match target {
|
request.placement = match target {
|
||||||
@@ -362,7 +364,7 @@ pub fn popup(app: &App) -> Option<Menu<Message>> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
entries.push(Entry::separator());
|
entries.push(Entry::separator());
|
||||||
if let Some(context) = request.process {
|
if let Some(context) = request.process.as_deref().copied() {
|
||||||
let endpoint = match request.selection_target.as_deref() {
|
let endpoint = match request.selection_target.as_deref() {
|
||||||
Some(Target::ProcessOccurrence(endpoint))
|
Some(Target::ProcessOccurrence(endpoint))
|
||||||
if !endpoint.references.is_empty() =>
|
if !endpoint.references.is_empty() =>
|
||||||
|
|||||||
Reference in New Issue
Block a user