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() {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -285,7 +285,7 @@ impl App {
|
||||
match topology::reorder(self.project(), context.owner, occurrence, before) {
|
||||
Ok(topology::group_reorder::Plan::Noop) => {}
|
||||
Ok(topology::group_reorder::Plan::Change(command)) => {
|
||||
if self.execute(command) {
|
||||
if self.execute(*command) {
|
||||
self.selected = occurrence;
|
||||
self.refresh_draft();
|
||||
return self.reveal_process_selection();
|
||||
|
||||
@@ -219,7 +219,7 @@ impl App {
|
||||
{
|
||||
Ok(Plan::Noop) => {}
|
||||
Ok(Plan::Change(command)) => {
|
||||
if self.execute(command) {
|
||||
if self.execute(*command) {
|
||||
self.inspect_process_multiselect_primary();
|
||||
self.status = format!(
|
||||
"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"),
|
||||
};
|
||||
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));
|
||||
let _ = app.update(Message::Menu(model_menu::Event::Open(request)));
|
||||
let opened = app
|
||||
@@ -610,7 +610,10 @@ fn menu(app: &mut App, target: Target, label: &str) -> Message {
|
||||
.as_ref()
|
||||
.expect("Captured Process menu must survive the open epoch change");
|
||||
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)
|
||||
.unwrap()
|
||||
.entries
|
||||
|
||||
@@ -245,7 +245,7 @@ impl Graph<'_> {
|
||||
};
|
||||
let mut request = crate::model_menu::Request::object(object, pointer);
|
||||
request.selection_target = Some(Box::new(target));
|
||||
request.process = Some(context);
|
||||
request.process = Some(Box::new(context));
|
||||
return Some(
|
||||
Action::publish(Message::Menu(crate::model_menu::Event::Open(request)))
|
||||
.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!(destination, Destination::After(f.nodes[1]));
|
||||
let Plan::Change(Command::SetSteps(owner, steps)) =
|
||||
Prepared::new(&f.p, f.owner, snapshot.targets())
|
||||
.unwrap()
|
||||
.plan(destination)
|
||||
.unwrap()
|
||||
let Plan::Change(command) = Prepared::new(&f.p, f.owner, snapshot.targets())
|
||||
.unwrap()
|
||||
.plan(destination)
|
||||
.unwrap()
|
||||
else {
|
||||
panic!("changed steps");
|
||||
};
|
||||
let Command::SetSteps(owner, steps) = *command else {
|
||||
panic!("expected SetSteps");
|
||||
};
|
||||
assert_eq!(owner, f.owner);
|
||||
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)
|
||||
);
|
||||
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]);
|
||||
|
||||
@@ -365,7 +365,7 @@ fn direct_process_reconnect_delete_and_reorder_preserve_other_paths_and_sources(
|
||||
else {
|
||||
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.relations, f.p.relations);
|
||||
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)]
|
||||
pub(crate) enum Plan {
|
||||
Noop,
|
||||
Change(Command),
|
||||
Change(Box<Command>),
|
||||
}
|
||||
|
||||
/// 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(),
|
||||
);
|
||||
}
|
||||
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 {
|
||||
panic!("Expected an actual order change");
|
||||
};
|
||||
assert!(matches!(&command, Command::SetSteps(id, _) if *id == owner));
|
||||
command
|
||||
assert!(matches!(command.as_ref(), Command::SetSteps(id, _) if *id == owner));
|
||||
*command
|
||||
}
|
||||
|
||||
#[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 {
|
||||
panic!("Expected move");
|
||||
};
|
||||
let result = execute_batch(&project, &[command]).unwrap();
|
||||
let result = execute_batch(&project, &[*command]).unwrap();
|
||||
assert_eq!(
|
||||
result.elements[&owner].steps().unwrap(),
|
||||
&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 source = app.document.bundle().clone();
|
||||
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
|
||||
.batch(
|
||||
&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),
|
||||
camera
|
||||
);
|
||||
record_feedback("after-motion", &mut mounted, &app, points[2]);
|
||||
let messages = mounted
|
||||
.batch(
|
||||
&mut app,
|
||||
|
||||
@@ -18,7 +18,7 @@ pub struct Request {
|
||||
pub route_segment: Option<(usize, Position)>,
|
||||
pub creation_point: Option<Position>,
|
||||
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>,
|
||||
}
|
||||
impl Request {
|
||||
@@ -65,7 +65,8 @@ impl Opened {
|
||||
.is_some_and(|c| Some(c) != current_canvas(app))
|
||||
|| request
|
||||
.process
|
||||
.is_some_and(|c| Some(c) != app.topology_context())
|
||||
.as_deref()
|
||||
.is_some_and(|c| Some(*c) != app.topology_context())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
@@ -96,7 +97,8 @@ impl Opened {
|
||||
&& self
|
||||
.request
|
||||
.process
|
||||
.is_none_or(|c| Some(c) == app.topology_context())
|
||||
.as_deref()
|
||||
.is_none_or(|c| Some(*c) == app.topology_context())
|
||||
&& self
|
||||
.request
|
||||
.source_file
|
||||
@@ -181,7 +183,7 @@ pub fn trigger(app: &App) -> iced::Element<'_, Message> {
|
||||
request.canvas = app.route_context();
|
||||
} else if app.page == Page::Behavior {
|
||||
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()) {
|
||||
request.selection_target = Some(Box::new(target.clone()));
|
||||
request.placement = match target {
|
||||
@@ -362,7 +364,7 @@ pub fn popup(app: &App) -> Option<Menu<Message>> {
|
||||
});
|
||||
}
|
||||
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() {
|
||||
Some(Target::ProcessOccurrence(endpoint))
|
||||
if !endpoint.references.is_empty() =>
|
||||
|
||||
Reference in New Issue
Block a user