From b15345123a7feaf630ef24d45f591c76c6715778 Mon Sep 17 00:00:00 2001 From: "sorokin.oleg.edu" Date: Wed, 23 Sep 2026 05:47:08 -0700 Subject: [PATCH] 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. --- crates/oa-desktop/src/app.rs | 2 +- crates/oa-desktop/src/app/process_canvas.rs | 2 +- .../oa-desktop/src/app/process_multiselect.rs | 2 +- .../src/app/process_multiselect/tests.rs | 7 +++- .../src/behavior/topology/direct_selection.rs | 2 +- .../topology/direct_selection_tests.rs | 14 ++++--- .../src/behavior/topology/direct_tests.rs | 2 +- .../src/behavior/topology/group_reorder.rs | 6 ++- .../behavior/topology/group_reorder/tests.rs | 6 +-- .../src/behavior/topology/layout_tests.rs | 42 +++++++++++++++++++ crates/oa-desktop/src/model_menu.rs | 12 +++--- 11 files changed, 74 insertions(+), 23 deletions(-) diff --git a/crates/oa-desktop/src/app.rs b/crates/oa-desktop/src/app.rs index c9d87f46..01cde07b 100644 --- a/crates/oa-desktop/src/app.rs +++ b/crates/oa-desktop/src/app.rs @@ -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); } diff --git a/crates/oa-desktop/src/app/process_canvas.rs b/crates/oa-desktop/src/app/process_canvas.rs index b2eba34d..9df5a654 100644 --- a/crates/oa-desktop/src/app/process_canvas.rs +++ b/crates/oa-desktop/src/app/process_canvas.rs @@ -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(); diff --git a/crates/oa-desktop/src/app/process_multiselect.rs b/crates/oa-desktop/src/app/process_multiselect.rs index eac08b63..2ee9b420 100644 --- a/crates/oa-desktop/src/app/process_multiselect.rs +++ b/crates/oa-desktop/src/app/process_multiselect.rs @@ -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", diff --git a/crates/oa-desktop/src/app/process_multiselect/tests.rs b/crates/oa-desktop/src/app/process_multiselect/tests.rs index cd197e23..a0389d7d 100644 --- a/crates/oa-desktop/src/app/process_multiselect/tests.rs +++ b/crates/oa-desktop/src/app/process_multiselect/tests.rs @@ -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 diff --git a/crates/oa-desktop/src/behavior/topology/direct_selection.rs b/crates/oa-desktop/src/behavior/topology/direct_selection.rs index 8c35ce1a..7d233e29 100644 --- a/crates/oa-desktop/src/behavior/topology/direct_selection.rs +++ b/crates/oa-desktop/src/behavior/topology/direct_selection.rs @@ -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(), diff --git a/crates/oa-desktop/src/behavior/topology/direct_selection_tests.rs b/crates/oa-desktop/src/behavior/topology/direct_selection_tests.rs index 40593446..48e41de2 100644 --- a/crates/oa-desktop/src/behavior/topology/direct_selection_tests.rs +++ b/crates/oa-desktop/src/behavior/topology/direct_selection_tests.rs @@ -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]); diff --git a/crates/oa-desktop/src/behavior/topology/direct_tests.rs b/crates/oa-desktop/src/behavior/topology/direct_tests.rs index 827db59c..8148f7b3 100644 --- a/crates/oa-desktop/src/behavior/topology/direct_tests.rs +++ b/crates/oa-desktop/src/behavior/topology/direct_tests.rs @@ -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()); diff --git a/crates/oa-desktop/src/behavior/topology/group_reorder.rs b/crates/oa-desktop/src/behavior/topology/group_reorder.rs index 3981b123..74e2a585 100644 --- a/crates/oa-desktop/src/behavior/topology/group_reorder.rs +++ b/crates/oa-desktop/src/behavior/topology/group_reorder.rs @@ -11,7 +11,7 @@ pub(crate) enum Destination { #[derive(Debug)] pub(crate) enum Plan { Noop, - Change(Command), + Change(Box), } /// 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, + )))) } } diff --git a/crates/oa-desktop/src/behavior/topology/group_reorder/tests.rs b/crates/oa-desktop/src/behavior/topology/group_reorder/tests.rs index 0ca0312f..b46417ed 100644 --- a/crates/oa-desktop/src/behavior/topology/group_reorder/tests.rs +++ b/crates/oa-desktop/src/behavior/topology/group_reorder/tests.rs @@ -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] diff --git a/crates/oa-desktop/src/behavior/topology/layout_tests.rs b/crates/oa-desktop/src/behavior/topology/layout_tests.rs index 77409024..704511d5 100644 --- a/crates/oa-desktop/src/behavior/topology/layout_tests.rs +++ b/crates/oa-desktop/src/behavior/topology/layout_tests.rs @@ -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, diff --git a/crates/oa-desktop/src/model_menu.rs b/crates/oa-desktop/src/model_menu.rs index a72c0fcf..212d063e 100644 --- a/crates/oa-desktop/src/model_menu.rs +++ b/crates/oa-desktop/src/model_menu.rs @@ -18,7 +18,7 @@ pub struct Request { pub route_segment: Option<(usize, Position)>, pub creation_point: Option, pub canvas: Option, - pub process: Option, + pub process: Option>, pub source_file: Option, } 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> { }); } 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() =>