Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
956e33e568 | ||
|
|
d6f45a13a7 | ||
|
|
c578d32011 | ||
|
|
9eb4642e50 | ||
|
|
6c44a2d6a1 | ||
|
|
3dfbe1959d | ||
|
|
f0d64b531a | ||
|
|
a5c60af385 | ||
|
|
3923860ad2 |
@@ -0,0 +1,283 @@
|
||||
//! Native UI wiring fixture/oracle. Expected output comes from candidate Layout,
|
||||
//! not from an independent geometry algorithm. Run only after parent source freeze.
|
||||
use oa_core::*;
|
||||
use serde_json::{Value, json};
|
||||
use std::{collections::BTreeMap, error::Error, fs, path::PathBuf};
|
||||
|
||||
fn id(n: u128) -> Id {
|
||||
Id::from_u128(700_000 + n)
|
||||
}
|
||||
fn at(x: f32, y: f32) -> Position {
|
||||
Position { x, y }
|
||||
}
|
||||
|
||||
fn function(p: &mut Project, n: u128, name: &str, parent: Option<Id>) -> Id {
|
||||
let old = p
|
||||
.add_element(Kind::Function, Layer::Sa, name, parent)
|
||||
.unwrap();
|
||||
reidentify(p, old, id(n)).unwrap();
|
||||
id(n)
|
||||
}
|
||||
|
||||
fn place(p: &mut Project, d: Id, n: u128, element: Id, position: Position, size: Size) {
|
||||
let old = p.place(d, element, position);
|
||||
reidentify(p, old, id(n)).unwrap();
|
||||
let node = p
|
||||
.diagrams
|
||||
.get_mut(&d)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&id(n))
|
||||
.unwrap();
|
||||
node.alias = format!("placement_{n}");
|
||||
node.size = size;
|
||||
}
|
||||
|
||||
fn exchange(p: &mut Project, n: u128, name: &str, source: Id, target: Id) -> Id {
|
||||
let request = FunctionalExchangeRequest {
|
||||
id: id(n),
|
||||
alias: format!("exchange_{n}"),
|
||||
name: name.into(),
|
||||
description: String::new(),
|
||||
source,
|
||||
target,
|
||||
source_port: PortChoice::Create,
|
||||
target_port: PortChoice::Create,
|
||||
items: vec![],
|
||||
};
|
||||
let commands = ports::plan_functional_exchange(p, &request).unwrap();
|
||||
*p = execute_batch(p, &commands).unwrap();
|
||||
id(n)
|
||||
}
|
||||
|
||||
fn route(edge: Id, waypoints: Vec<Position>, fraction: f32, offset: Position) -> EdgeRoute {
|
||||
EdgeRoute {
|
||||
edge,
|
||||
source_attachment: None,
|
||||
target_attachment: None,
|
||||
waypoints,
|
||||
label: Some(EdgeLabel { fraction, offset }),
|
||||
}
|
||||
}
|
||||
|
||||
fn original() -> (Project, Id, Id, Value) {
|
||||
let mut p = Project::empty();
|
||||
p.id = id(0);
|
||||
p.alias = "arrange_native_fixture".into();
|
||||
p.name = "Arrange route reset trial".into();
|
||||
let old = p.ensure_diagram(Layer::Sa);
|
||||
let main = id(1);
|
||||
reidentify(&mut p, old, main).unwrap();
|
||||
{
|
||||
let d = p.diagrams.get_mut(&main).unwrap();
|
||||
d.alias = "main_dispatch".into();
|
||||
d.name = "Main dispatch".into();
|
||||
d.projection = ProjectionKind::FunctionalDataflow;
|
||||
}
|
||||
let group = function(&mut p, 10, "Prepare report", None);
|
||||
let send = function(&mut p, 11, "Send report", Some(group));
|
||||
let check = function(&mut p, 12, "Check report", Some(group));
|
||||
let monitor = function(&mut p, 13, "Monitor report", None);
|
||||
let archive = function(&mut p, 14, "Archive report", None);
|
||||
let card = Size {
|
||||
width: 224.,
|
||||
height: 94.,
|
||||
};
|
||||
place(
|
||||
&mut p,
|
||||
main,
|
||||
20,
|
||||
group,
|
||||
at(40., 80.),
|
||||
Size {
|
||||
width: 400.,
|
||||
height: 420.,
|
||||
},
|
||||
);
|
||||
for (n, element, x, y) in [
|
||||
(21, send, 80., 160.),
|
||||
(22, check, 80., 330.),
|
||||
(23, monitor, 740., 80.),
|
||||
(24, archive, 740., 380.),
|
||||
] {
|
||||
place(&mut p, main, n, element, at(x, y), card);
|
||||
}
|
||||
let authored = exchange(&mut p, 100, "Verified report", send, monitor);
|
||||
exchange(&mut p, 101, "Report for checking", send, check);
|
||||
exchange(&mut p, 102, "Checked report archive", check, archive);
|
||||
let hidden = exchange(
|
||||
&mut p,
|
||||
103,
|
||||
"Filtered archive acknowledgement",
|
||||
monitor,
|
||||
archive,
|
||||
);
|
||||
let RelationBody::FunctionalExchange {
|
||||
source_port,
|
||||
target_port,
|
||||
..
|
||||
} = p.relations[&authored].body
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
for (node, port, side, offset) in [
|
||||
(id(21), source_port, PortAnchorSide::Right, 0.25),
|
||||
(id(23), target_port, PortAnchorSide::Left, 0.75),
|
||||
] {
|
||||
p = prepare_command(
|
||||
&p,
|
||||
&Command::SetPortAnchor {
|
||||
diagram: main,
|
||||
node,
|
||||
anchor: PortAnchor { port, side, offset },
|
||||
},
|
||||
)
|
||||
.unwrap()
|
||||
.project;
|
||||
}
|
||||
let mut visible = route(
|
||||
authored,
|
||||
vec![at(610., 210.), at(610., 420.)],
|
||||
0.5,
|
||||
at(0., -32.),
|
||||
);
|
||||
// Real semantic ports remain authoritative; these stored preferences are dormant.
|
||||
visible.source_attachment = Some(EdgeAttachment {
|
||||
side: PortAnchorSide::Bottom,
|
||||
offset: 0.5,
|
||||
});
|
||||
visible.target_attachment = Some(EdgeAttachment {
|
||||
side: PortAnchorSide::Top,
|
||||
offset: 0.25,
|
||||
});
|
||||
let filtered = route(hidden, vec![at(1020., 270.)], 0.75, at(16., -16.));
|
||||
p.diagrams.get_mut(&main).unwrap().routes = vec![visible, filtered];
|
||||
p.diagrams.get_mut(&main).unwrap().view_options.hidden_edges = vec![hidden];
|
||||
let second = id(2);
|
||||
p.diagrams.insert(
|
||||
second,
|
||||
Diagram {
|
||||
id: second,
|
||||
alias: "review_sentinel".into(),
|
||||
name: "Review sentinel".into(),
|
||||
description:
|
||||
"Independent other-view route; Arrange Main dispatch must retain this entire view"
|
||||
.into(),
|
||||
layer: Layer::Sa,
|
||||
projection: ProjectionKind::FunctionalDataflow,
|
||||
view_options: Default::default(),
|
||||
nodes: BTreeMap::new(),
|
||||
layout: None,
|
||||
routes: vec![route(authored, vec![at(420., 40.)], 0.25, at(8., -8.))],
|
||||
},
|
||||
);
|
||||
place(&mut p, second, 30, send, at(40., 120.), card);
|
||||
place(&mut p, second, 31, monitor, at(660., 120.), card);
|
||||
validate_candidate(&p).unwrap();
|
||||
(
|
||||
p,
|
||||
main,
|
||||
second,
|
||||
json!({"authored_edge":authored,"authored_edge_name":"Verified report",
|
||||
"filtered_edge":hidden,"container_placement":id(20),"source_port":source_port,"target_port":target_port,
|
||||
"main_diagram":main,"second_diagram":second,"authored_pin_count":2,
|
||||
"source_anchor":{"side":"Right","offset":0.25},"target_anchor":{"side":"Left","offset":0.75},
|
||||
"dormant_source_attachment":{"side":"Bottom","offset":0.5},
|
||||
"dormant_target_attachment":{"side":"Top","offset":0.25}}),
|
||||
)
|
||||
}
|
||||
|
||||
fn json_bytes(p: &Project) -> Vec<u8> {
|
||||
let mut bytes = serde_json::to_vec_pretty(p).unwrap();
|
||||
if !bytes.ends_with(b"\n") {
|
||||
bytes.push(b'\n');
|
||||
}
|
||||
assert_eq!(persistence::decode(&bytes).unwrap(), *p);
|
||||
bytes
|
||||
}
|
||||
|
||||
fn source_bytes(p: &Project) -> Vec<u8> {
|
||||
let source = language::format(p);
|
||||
assert_eq!(language::parse(&source).unwrap(), *p);
|
||||
let mut bytes = source.into_bytes();
|
||||
if !bytes.ends_with(b"\n") {
|
||||
bytes.push(b'\n');
|
||||
}
|
||||
bytes
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let mut args = std::env::args_os().skip(1);
|
||||
let first = args
|
||||
.next()
|
||||
.ok_or("Usage: arrange_reset_native_probe NEW_DIRECTORY | --compare ACTUAL EXPECTED")?;
|
||||
if first == "--compare" {
|
||||
let actual_path = args.next().ok_or("Missing ACTUAL path")?;
|
||||
let expected_path = args.next().ok_or("Missing EXPECTED path")?;
|
||||
if args.next().is_some() {
|
||||
return Err("Unexpected comparison argument".into());
|
||||
}
|
||||
let actual = persistence::decode(&fs::read(actual_path)?)?;
|
||||
let expected = persistence::decode(&fs::read(expected_path)?)?;
|
||||
let equal = actual == expected;
|
||||
println!(
|
||||
"{}",
|
||||
json!({"complete_project_equal": equal,
|
||||
"actual_diagrams": actual.diagrams.len(),
|
||||
"expected_diagrams": expected.diagrams.len()})
|
||||
);
|
||||
if !equal {
|
||||
return Err("Complete typed Projects differ".into());
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
let output = PathBuf::from(first);
|
||||
if args.next().is_some() || output.exists() {
|
||||
return Err("Require exactly one output directory that does not already exist".into());
|
||||
}
|
||||
let (before, main, second, identities) = original();
|
||||
let mut folded = before.clone();
|
||||
folded
|
||||
.diagrams
|
||||
.get_mut(&main)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&id(20))
|
||||
.unwrap()
|
||||
.collapsed = true;
|
||||
let mut files = Vec::new();
|
||||
let mut cases = Vec::new();
|
||||
for (name, project) in [("expanded", before), ("folded", folded)] {
|
||||
validate_candidate(&project)?;
|
||||
// Explicitly candidate-derived UI oracle: this is not an independent layout proof.
|
||||
let expected = execute_batch(&project, &[Command::Layout(main)])?;
|
||||
assert_eq!(project.elements, expected.elements);
|
||||
assert_eq!(project.relations, expected.relations);
|
||||
assert_eq!(project.diagrams[&second], expected.diagrams[&second]);
|
||||
for (stage, p) in [("original", &project), ("expected-after", &expected)] {
|
||||
files.push((format!("{name}-{stage}.json"), json_bytes(p)));
|
||||
files.push((format!("{name}-{stage}.arcadia"), source_bytes(p)));
|
||||
}
|
||||
cases.push(json!({"name":name,"diagram":main,
|
||||
"original_json":format!("{name}-original.json"),
|
||||
"expected_json":format!("{name}-expected-after.json"),
|
||||
"original_source":format!("{name}-original.arcadia"),
|
||||
"expected_source":format!("{name}-expected-after.arcadia"),
|
||||
"original_routes":project.diagrams[&main].routes,"expected_routes":expected.diagrams[&main].routes,
|
||||
"expected_origin":"Candidate Command::Layout; UI-wiring oracle only"}));
|
||||
}
|
||||
let manifest = json!({"schema":1,"cases":cases,"identities":identities,
|
||||
"definition":"Source-authored five-Function/two-view fixture; original geometry never generated by Layout.",
|
||||
"comparison":"Decode saved JSON with production persistence and compare complete Project to immutable original/expected file. Prefer direct serializer bytes for byte checks; do not reserialize Project through generic JSON Value as a float oracle.",
|
||||
"performed":"Generation and core checks only when this program is actually run; no native UI results recorded here."});
|
||||
files.push((
|
||||
"manifest.json".into(),
|
||||
serde_json::to_vec_pretty(&manifest)?,
|
||||
));
|
||||
fs::create_dir(&output)?;
|
||||
for (name, bytes) in files {
|
||||
fs::write(output.join(name), bytes)?;
|
||||
}
|
||||
println!("{}", output.join("manifest.json").display());
|
||||
Ok(())
|
||||
}
|
||||
@@ -952,6 +952,21 @@ fn layout(p: &mut Project, diagram: Id) -> Result<(), ModelError> {
|
||||
"Scenario layout requires the Scenario geometry operation.".into(),
|
||||
));
|
||||
}
|
||||
// Freeze reset eligibility before this operation changes geometry. In a
|
||||
// Batch this is the staged input, not an independently validated prefix.
|
||||
let view = crate::projection::project_view(p, diagram);
|
||||
let visible_nodes: BTreeSet<_> = view.nodes.iter().map(|node| node.id).collect();
|
||||
let reset_routes: BTreeSet<_> = view
|
||||
.edges
|
||||
.iter()
|
||||
.filter_map(|edge| {
|
||||
let owners = crate::projection::edge_endpoint_placements(p, d, edge.id)?;
|
||||
(owners == (edge.source, edge.target)
|
||||
&& visible_nodes.contains(&edge.source)
|
||||
&& visible_nodes.contains(&edge.target))
|
||||
.then_some(edge.id)
|
||||
})
|
||||
.collect();
|
||||
let parents = crate::projection::parents(p, d);
|
||||
let mut children: BTreeMap<Option<Id>, Vec<Id>> = BTreeMap::new();
|
||||
for (id, parent) in &parents {
|
||||
@@ -1044,7 +1059,7 @@ fn layout(p: &mut Project, diagram: Id) -> Result<(), ModelError> {
|
||||
}
|
||||
node
|
||||
};
|
||||
let edges: BTreeSet<_> = crate::projection::project_view(p, diagram)
|
||||
let edges: BTreeSet<_> = view
|
||||
.edges
|
||||
.iter()
|
||||
.filter_map(|edge| {
|
||||
@@ -1138,5 +1153,11 @@ fn layout(p: &mut Project, diagram: Id) -> Result<(), ModelError> {
|
||||
*existing = route;
|
||||
}
|
||||
}
|
||||
for route in &mut d.routes {
|
||||
if reset_routes.contains(&route.edge) {
|
||||
route.waypoints.clear();
|
||||
route.label = None;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -649,7 +649,7 @@ fn group_translation_beyond_coordinate_limit_rolls_back_nodes_and_routes() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_preserves_authored_route_count_order_labels_and_undo() {
|
||||
fn layout_resets_visible_pins_labels_preserving_route_count_order_and_undo() {
|
||||
let f = routed(fixture());
|
||||
let mut session = Session::new(f.project.clone());
|
||||
session.execute(Command::Layout(f.diagram)).unwrap();
|
||||
@@ -657,10 +657,10 @@ fn layout_preserves_authored_route_count_order_labels_and_undo() {
|
||||
assert_eq!(routes.len(), 2);
|
||||
assert_eq!(routes[0].edge, f.edge);
|
||||
assert_eq!(routes[1].edge, f.external);
|
||||
assert_eq!(routes[0].waypoints.len(), 2);
|
||||
assert_eq!(routes[1].waypoints.len(), 2);
|
||||
assert_eq!(routes[0].label, route(f.edge).label);
|
||||
assert_eq!(routes[1].label, route(f.external).label);
|
||||
assert!(routes[0].waypoints.is_empty());
|
||||
assert!(routes[1].waypoints.is_empty());
|
||||
assert_eq!(routes[0].label, None);
|
||||
assert_eq!(routes[1].label, None);
|
||||
assert_eq!(
|
||||
session.project().diagrams[&f.second],
|
||||
f.project.diagrams[&f.second]
|
||||
@@ -669,6 +669,452 @@ fn layout_preserves_authored_route_count_order_labels_and_undo() {
|
||||
assert_eq!(session.project(), &f.project);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_keeps_attachments_port_anchors_empty_entries_and_exact_history() {
|
||||
let mut f = routed(fixture());
|
||||
let extra = f.project.relate(
|
||||
RelationKind::FunctionalExchange,
|
||||
"Attachment only",
|
||||
f.a,
|
||||
f.b,
|
||||
);
|
||||
let empty = f
|
||||
.project
|
||||
.relate(RelationKind::FunctionalExchange, "Explicit empty", f.a, f.b);
|
||||
let attachment = EdgeAttachment {
|
||||
side: PortAnchorSide::Bottom,
|
||||
offset: 0.23,
|
||||
};
|
||||
let opposite = EdgeAttachment {
|
||||
side: PortAnchorSide::Top,
|
||||
offset: 0.81,
|
||||
};
|
||||
let mut fraction_only = route(f.edge);
|
||||
fraction_only.waypoints.clear();
|
||||
fraction_only.label = Some(EdgeLabel {
|
||||
fraction: 0.73,
|
||||
offset: Position { x: 0., y: 0. },
|
||||
});
|
||||
fraction_only.source_attachment = Some(attachment);
|
||||
fraction_only.target_attachment = Some(opposite);
|
||||
let attachment_only = EdgeRoute {
|
||||
edge: extra,
|
||||
source_attachment: Some(attachment),
|
||||
target_attachment: None,
|
||||
waypoints: vec![],
|
||||
label: None,
|
||||
};
|
||||
let empty_route = EdgeRoute {
|
||||
edge: empty,
|
||||
source_attachment: None,
|
||||
target_attachment: None,
|
||||
waypoints: vec![],
|
||||
label: None,
|
||||
};
|
||||
f.project.diagrams.get_mut(&f.diagram).unwrap().routes = vec![
|
||||
route(f.external),
|
||||
fraction_only.clone(),
|
||||
attachment_only.clone(),
|
||||
empty_route.clone(),
|
||||
];
|
||||
let RelationBody::FunctionalExchange { source_port, .. } = f.project.relations[&f.edge].body
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
let owner = node(&f.project, f.diagram, f.a);
|
||||
let anchor = PortAnchor {
|
||||
port: source_port,
|
||||
side: PortAnchorSide::Left,
|
||||
offset: 0.37,
|
||||
};
|
||||
f.project
|
||||
.diagrams
|
||||
.get_mut(&f.diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&owner)
|
||||
.unwrap()
|
||||
.port_anchors
|
||||
.push(anchor.clone());
|
||||
validate_candidate(&f.project).unwrap();
|
||||
let original_source = language::format(&f.project);
|
||||
let mut session = Session::new(f.project.clone());
|
||||
session.execute(Command::Layout(f.diagram)).unwrap();
|
||||
let after = session.project().clone();
|
||||
let mut expected_external = route(f.external);
|
||||
expected_external.waypoints.clear();
|
||||
expected_external.label = None;
|
||||
fraction_only.label = None;
|
||||
assert_eq!(
|
||||
after.diagrams[&f.diagram].routes,
|
||||
vec![
|
||||
expected_external,
|
||||
fraction_only,
|
||||
attachment_only,
|
||||
empty_route
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
after.diagrams[&f.diagram].nodes[&owner].port_anchors,
|
||||
vec![anchor]
|
||||
);
|
||||
assert_eq!(after.elements, f.project.elements);
|
||||
assert_eq!(after.relations, f.project.relations);
|
||||
assert_eq!(after.diagrams[&f.second], f.project.diagrams[&f.second]);
|
||||
assert_eq!(language::parse(&language::format(&after)).unwrap(), after);
|
||||
let command: Command =
|
||||
serde_json::from_value(serde_json::json!({"Layout": f.diagram})).unwrap();
|
||||
assert_eq!(execute_batch(&f.project, &[command]).unwrap(), after);
|
||||
session.undo();
|
||||
assert_eq!(session.project(), &f.project);
|
||||
assert_eq!(language::format(session.project()), original_source);
|
||||
assert!(!session.can_undo());
|
||||
session.redo();
|
||||
assert_eq!(session.project(), &after);
|
||||
}
|
||||
|
||||
fn shift_all(p: &mut Project, d: Id, x: f32, y: f32) {
|
||||
for placement in p.diagrams.get_mut(&d).unwrap().nodes.values_mut() {
|
||||
placement.position.x += x;
|
||||
placement.position.y += y;
|
||||
}
|
||||
}
|
||||
|
||||
fn translated_expected(mut route: EdgeRoute, x: f32, y: f32) -> EdgeRoute {
|
||||
for point in &mut route.waypoints {
|
||||
point.x += x;
|
||||
point.y += y;
|
||||
}
|
||||
route
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_resets_only_visible_actual_owners_and_retains_filtered_and_missing_routes() {
|
||||
let mut f = fixture();
|
||||
let outside = f.project.relations[&f.external].target();
|
||||
let visible = f.project.relate(
|
||||
RelationKind::FunctionalExchange,
|
||||
"Visible overview",
|
||||
f.group,
|
||||
outside,
|
||||
);
|
||||
let absent = f
|
||||
.project
|
||||
.add_element(Kind::Function, Layer::Sa, "Not placed", None)
|
||||
.unwrap();
|
||||
let missing = f.project.relate(
|
||||
RelationKind::FunctionalExchange,
|
||||
"Dormant missing owner",
|
||||
f.b,
|
||||
absent,
|
||||
);
|
||||
f.project
|
||||
.diagrams
|
||||
.get_mut(&f.diagram)
|
||||
.unwrap()
|
||||
.view_options
|
||||
.hidden_edges = vec![f.edge, f.external];
|
||||
// Freeze a geometry-only starting point before adding sentinels. The oracle
|
||||
// below names the eligible edge and exact authored deltas independently.
|
||||
let base = execute_batch(&f.project, &[Command::Layout(f.diagram)]).unwrap();
|
||||
f.project = base.clone();
|
||||
shift_all(&mut f.project, f.diagram, 40., 60.);
|
||||
let outside_node = node(&f.project, f.diagram, outside);
|
||||
f.project
|
||||
.diagrams
|
||||
.get_mut(&f.diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&outside_node)
|
||||
.unwrap()
|
||||
.position
|
||||
.x += 7.;
|
||||
f.project.diagrams.get_mut(&f.diagram).unwrap().routes = vec![
|
||||
route(visible),
|
||||
route(f.edge),
|
||||
route(f.external),
|
||||
route(missing),
|
||||
];
|
||||
let view = projection::project_view(&f.project, f.diagram);
|
||||
assert_eq!(
|
||||
view.edges.iter().map(|edge| edge.id).collect::<Vec<_>>(),
|
||||
vec![visible]
|
||||
);
|
||||
let after = execute_batch(&f.project, &[Command::Layout(f.diagram)]).unwrap();
|
||||
assert_eq!(
|
||||
after.diagrams[&f.diagram].nodes,
|
||||
base.diagrams[&f.diagram].nodes
|
||||
);
|
||||
let mut reset = route(visible);
|
||||
reset.waypoints.clear();
|
||||
reset.label = None;
|
||||
assert_eq!(
|
||||
after.diagrams[&f.diagram].routes,
|
||||
vec![
|
||||
reset,
|
||||
translated_expected(route(f.edge), -40., -60.),
|
||||
route(f.external),
|
||||
route(missing)
|
||||
]
|
||||
);
|
||||
assert_eq!(after.diagrams[&f.second], f.project.diagrams[&f.second]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_folded_routes_use_actual_owner_deltas_without_resetting_promoted_edges() {
|
||||
let mut f = routed(fixture());
|
||||
let group = node(&f.project, f.diagram, f.group);
|
||||
let outside = f.project.relations[&f.external].target();
|
||||
let outside_node = node(&f.project, f.diagram, outside);
|
||||
f.project
|
||||
.diagrams
|
||||
.get_mut(&f.diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&group)
|
||||
.unwrap()
|
||||
.collapsed = true;
|
||||
f.project
|
||||
.diagrams
|
||||
.get_mut(&f.diagram)
|
||||
.unwrap()
|
||||
.routes
|
||||
.clear();
|
||||
let base = execute_batch(&f.project, &[Command::Layout(f.diagram)]).unwrap();
|
||||
f.project = base.clone();
|
||||
shift_all(&mut f.project, f.diagram, 40., 60.);
|
||||
// The visible container's displacement differs from its hidden children.
|
||||
f.project
|
||||
.diagrams
|
||||
.get_mut(&f.diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&group)
|
||||
.unwrap()
|
||||
.position
|
||||
.x += 13.;
|
||||
f.project
|
||||
.diagrams
|
||||
.get_mut(&f.diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&outside_node)
|
||||
.unwrap()
|
||||
.position
|
||||
.x += 7.;
|
||||
f.project.diagrams.get_mut(&f.diagram).unwrap().routes = vec![route(f.edge), route(f.external)];
|
||||
let view = projection::project_view(&f.project, f.diagram);
|
||||
assert!(!view.edges.iter().any(|edge| edge.id == f.edge));
|
||||
let promoted = view
|
||||
.edges
|
||||
.iter()
|
||||
.find(|edge| edge.id == f.external)
|
||||
.unwrap();
|
||||
assert_eq!(promoted.source, group);
|
||||
let after = execute_batch(&f.project, &[Command::Layout(f.diagram)]).unwrap();
|
||||
assert_eq!(
|
||||
after.diagrams[&f.diagram].nodes,
|
||||
base.diagrams[&f.diagram].nodes
|
||||
);
|
||||
assert_eq!(
|
||||
after.diagrams[&f.diagram].routes,
|
||||
vec![
|
||||
translated_expected(route(f.edge), -40., -60.),
|
||||
route(f.external)
|
||||
]
|
||||
);
|
||||
assert_eq!(after.diagrams[&f.second], f.project.diagrams[&f.second]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_batch_uses_staged_visibility_and_one_final_validation_history_boundary() {
|
||||
let f = routed(fixture());
|
||||
let mut hidden = f.project.diagrams[&f.diagram].clone();
|
||||
hidden.view_options.hidden_edges = vec![f.edge, f.external];
|
||||
let mut invalid = f.project.elements[&f.a].clone();
|
||||
invalid.alias = f.project.elements[&f.b].alias.clone();
|
||||
// Duplicate alias is repaired later in the same transaction. Layout must
|
||||
// not add prefix validation, nor reset routes hidden by the preceding edit.
|
||||
let mut session = Session::new(f.project.clone());
|
||||
session
|
||||
.execute(Command::Batch(vec![
|
||||
Command::EditElement(f.a, invalid),
|
||||
Command::EditDiagram(f.diagram, hidden),
|
||||
Command::Layout(f.diagram),
|
||||
Command::EditElement(f.a, f.project.elements[&f.a].clone()),
|
||||
]))
|
||||
.unwrap();
|
||||
let after = session.project().clone();
|
||||
assert_eq!(
|
||||
after.diagrams[&f.diagram]
|
||||
.routes
|
||||
.iter()
|
||||
.map(|r| (r.edge, r.waypoints.len(), r.label.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
(f.edge, 2, route(f.edge).label),
|
||||
(f.external, 2, route(f.external).label)
|
||||
]
|
||||
);
|
||||
session.undo();
|
||||
assert_eq!(session.project(), &f.project);
|
||||
assert!(!session.can_undo());
|
||||
session.redo();
|
||||
assert_eq!(session.project(), &after);
|
||||
let mut bad = after.elements[&f.a].clone();
|
||||
bad.alias = after.elements[&f.b].alias.clone();
|
||||
let revision = session.revision();
|
||||
assert!(
|
||||
session
|
||||
.execute(Command::Batch(vec![
|
||||
Command::Layout(f.diagram),
|
||||
Command::EditElement(f.a, bad)
|
||||
]))
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(session.project(), &after);
|
||||
assert_eq!(session.revision(), revision);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_outside_projection_routes_keep_overrides_and_inactive_owner_positions() {
|
||||
let mut f = fixture();
|
||||
// Decomposition admits ordinary relations, including FunctionalExchange.
|
||||
// Capabilities explicitly excludes those edges and their Function owners;
|
||||
// retain their authored placements while arranging a real visible node.
|
||||
let capability = f
|
||||
.project
|
||||
.add_element(
|
||||
Kind::SystemCapability,
|
||||
Layer::Sa,
|
||||
"Visible capability",
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let capability_node = f
|
||||
.project
|
||||
.place(f.diagram, capability, Position { x: 100., y: 100. });
|
||||
f.project.diagrams.get_mut(&f.diagram).unwrap().projection = ProjectionKind::Capabilities;
|
||||
let base = execute_batch(&f.project, &[Command::Layout(f.diagram)]).unwrap();
|
||||
f.project = base.clone();
|
||||
shift_all(&mut f.project, f.diagram, 40., 60.);
|
||||
f.project.diagrams.get_mut(&f.diagram).unwrap().routes = vec![route(f.edge), route(f.external)];
|
||||
validate_candidate(&f.project).unwrap();
|
||||
let projected = projection::project_view(&f.project, f.diagram);
|
||||
assert_eq!(projected.nodes.len(), 1);
|
||||
assert_eq!(projected.nodes[0].id, capability_node);
|
||||
assert!(
|
||||
projected
|
||||
.edges
|
||||
.iter()
|
||||
.all(|edge| edge.id != f.edge && edge.id != f.external)
|
||||
);
|
||||
let mut expected = f.project.clone();
|
||||
expected.diagrams.get_mut(&f.diagram).unwrap().nodes.insert(
|
||||
capability_node,
|
||||
base.diagrams[&f.diagram].nodes[&capability_node].clone(),
|
||||
);
|
||||
let after = execute_batch(&f.project, &[Command::Layout(f.diagram)]).unwrap();
|
||||
assert_ne!(
|
||||
after.diagrams[&f.diagram].nodes[&capability_node].position,
|
||||
f.project.diagrams[&f.diagram].nodes[&capability_node].position
|
||||
);
|
||||
assert_eq!(
|
||||
after.diagrams[&f.diagram].routes,
|
||||
vec![route(f.edge), route(f.external)]
|
||||
);
|
||||
assert_eq!(
|
||||
after, expected,
|
||||
"Owners excluded by the projection receive no Layout delta; their pins stay fixed"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identical_layout_transaction_keeps_redo() {
|
||||
let mut p = Project::empty();
|
||||
let element = p
|
||||
.add_element(Kind::Function, Layer::Sa, "Unplaced", None)
|
||||
.unwrap();
|
||||
let diagram = p.ensure_diagram(Layer::Sa);
|
||||
let mut changed = p.elements[&element].clone();
|
||||
changed.name = "Renamed unplaced".into();
|
||||
let mut session = Session::new(p.clone());
|
||||
session
|
||||
.execute(Command::EditElement(element, changed))
|
||||
.unwrap();
|
||||
session.undo();
|
||||
let revision = session.revision();
|
||||
session
|
||||
.execute(Command::Batch(vec![
|
||||
Command::Layout(diagram),
|
||||
Command::Layout(diagram),
|
||||
]))
|
||||
.unwrap();
|
||||
assert_eq!(session.project(), &p);
|
||||
assert_eq!(session.revision(), revision);
|
||||
assert!(session.can_redo());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_route_reset_alone_is_one_edit_and_repeated_layout_keeps_redo() {
|
||||
let f = fixture();
|
||||
let mut before = execute_batch(&f.project, &[Command::Layout(f.diagram)]).unwrap();
|
||||
assert!(!before.diagrams[&f.diagram].nodes.is_empty());
|
||||
before.diagrams.get_mut(&f.diagram).unwrap().routes = vec![route(f.edge), route(f.external)];
|
||||
// Node geometry is already arranged. The complete expected Project differs
|
||||
// only in the two independently named eligible route overrides.
|
||||
let mut expected = before.clone();
|
||||
for route in &mut expected.diagrams.get_mut(&f.diagram).unwrap().routes {
|
||||
route.waypoints.clear();
|
||||
route.label = None;
|
||||
}
|
||||
let mut session = Session::new(before.clone());
|
||||
session.execute(Command::Layout(f.diagram)).unwrap();
|
||||
assert_eq!(
|
||||
session.project().diagrams[&f.diagram].nodes,
|
||||
before.diagrams[&f.diagram].nodes
|
||||
);
|
||||
assert_eq!(session.project(), &expected);
|
||||
assert_eq!(
|
||||
session.revision(),
|
||||
1,
|
||||
"Clearing overrides is a real edit without any node movement"
|
||||
);
|
||||
session.execute(Command::Layout(f.diagram)).unwrap();
|
||||
assert_eq!(session.project(), &expected);
|
||||
assert_eq!(
|
||||
session.revision(),
|
||||
1,
|
||||
"A repeated identical Layout adds no history"
|
||||
);
|
||||
session.undo();
|
||||
assert_eq!(session.project(), &before);
|
||||
assert!(
|
||||
!session.can_undo(),
|
||||
"The route-only reset was exactly one history entry"
|
||||
);
|
||||
session.redo();
|
||||
assert_eq!(session.project(), &expected);
|
||||
|
||||
let mut renamed = expected.elements[&f.a].clone();
|
||||
renamed.name = "Pending redo of rename".into();
|
||||
session.execute(Command::EditElement(f.a, renamed)).unwrap();
|
||||
let after_rename = session.project().clone();
|
||||
session.undo();
|
||||
assert_eq!(session.project(), &expected);
|
||||
assert!(session.can_redo());
|
||||
let revision = session.revision();
|
||||
session.execute(Command::Layout(f.diagram)).unwrap();
|
||||
assert_eq!(session.project(), &expected);
|
||||
assert_eq!(session.revision(), revision);
|
||||
assert!(
|
||||
session.can_redo(),
|
||||
"No-op Layout retains an unrelated undone edit"
|
||||
);
|
||||
session.redo();
|
||||
assert_eq!(session.project(), &after_rename);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn old_models_get_empty_routes_and_route_json_is_strict() {
|
||||
for bytes in [
|
||||
|
||||
@@ -704,6 +704,98 @@ fn moving_a_region_does_not_translate_pins_attached_to_a_stationary_state_bounda
|
||||
assert_eq!(after.diagrams[&f.d].routes[0].waypoints, pins);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_retains_hidden_state_boundary_pins_and_internal_routes() {
|
||||
let mut f = fixture();
|
||||
f.p.diagrams
|
||||
.get_mut(&f.d)
|
||||
.unwrap()
|
||||
.view_options
|
||||
.hidden_edges = vec![f.local];
|
||||
let baseline = execute_batch(&f.p, &[Command::Layout(f.d)]).unwrap();
|
||||
f.p = baseline.clone();
|
||||
let entry = node(&f.p, f.d, f.entry);
|
||||
let leaf = node(&f.p, f.d, f.leaf);
|
||||
let boundary = node(&f.p, f.d, f.a);
|
||||
for id in [entry, leaf] {
|
||||
let n =
|
||||
f.p.diagrams
|
||||
.get_mut(&f.d)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&id)
|
||||
.unwrap();
|
||||
n.position.x += 10.;
|
||||
n.position.y += 10.;
|
||||
}
|
||||
let hidden = EdgeRoute {
|
||||
edge: f.local,
|
||||
source_attachment: None,
|
||||
target_attachment: None,
|
||||
waypoints: vec![point(280., 390.), point(280., 470.)],
|
||||
label: Some(EdgeLabel {
|
||||
fraction: 0.4,
|
||||
offset: point(3., -7.),
|
||||
}),
|
||||
};
|
||||
let internal = EdgeRoute {
|
||||
edge: f.internal,
|
||||
source_attachment: None,
|
||||
target_attachment: None,
|
||||
waypoints: vec![point(320., 440.)],
|
||||
label: Some(EdgeLabel {
|
||||
fraction: 0.7,
|
||||
offset: point(0., 0.),
|
||||
}),
|
||||
};
|
||||
let visible = EdgeRoute {
|
||||
edge: f.self_edge,
|
||||
source_attachment: Some(EdgeAttachment {
|
||||
side: PortAnchorSide::Left,
|
||||
offset: 0.25,
|
||||
}),
|
||||
target_attachment: Some(EdgeAttachment {
|
||||
side: PortAnchorSide::Right,
|
||||
offset: 0.75,
|
||||
}),
|
||||
waypoints: vec![point(700., 100.)],
|
||||
label: Some(EdgeLabel {
|
||||
fraction: 0.6,
|
||||
offset: point(0., 0.),
|
||||
}),
|
||||
};
|
||||
f.p.diagrams.get_mut(&f.d).unwrap().routes =
|
||||
vec![hidden.clone(), internal.clone(), visible.clone()];
|
||||
let projected = project_view(&f.p, f.d);
|
||||
assert!(
|
||||
!projected
|
||||
.edges
|
||||
.iter()
|
||||
.any(|e| e.id == f.local || e.id == f.internal)
|
||||
);
|
||||
assert!(projected.edges.iter().any(|e| e.id == f.self_edge));
|
||||
let after = execute_batch(&f.p, &[Command::Layout(f.d)]).unwrap();
|
||||
for id in [entry, leaf, boundary] {
|
||||
assert_eq!(
|
||||
after.diagrams[&f.d].nodes[&id].position,
|
||||
baseline.diagrams[&f.d].nodes[&id].position
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
after.diagrams[&f.d].nodes[&boundary].position,
|
||||
f.p.diagrams[&f.d].nodes[&boundary].position,
|
||||
"The endpoint's owning State boundary stayed stationary"
|
||||
);
|
||||
let mut reset = visible;
|
||||
reset.waypoints.clear();
|
||||
reset.label = None;
|
||||
assert_eq!(
|
||||
after.diagrams[&f.d].routes,
|
||||
vec![hidden, internal, reset],
|
||||
"Equal entry/leaf deltas must not translate a pin on their stationary State boundary"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initial_transition_attaches_to_the_compact_symbol_and_preview_keeps_settled_geometry() {
|
||||
let mut f = fixture();
|
||||
|
||||
@@ -210,6 +210,7 @@ impl SemanticPlan {
|
||||
pub fn project(&self) -> &Project {
|
||||
&self.candidate
|
||||
}
|
||||
#[cfg(test)]
|
||||
pub fn index(&self) -> &ViewIndex {
|
||||
&self.index
|
||||
}
|
||||
@@ -484,57 +485,6 @@ pub struct PresentationContext {
|
||||
pub target: Option<TargetPresentation>,
|
||||
}
|
||||
|
||||
impl PresentationContext {
|
||||
fn header_visible(self, header: Rect, outgoing: bool) -> bool {
|
||||
if !outgoing {
|
||||
return self.viewport.contains(header);
|
||||
}
|
||||
// Leaving a performer must not require fitting the entire work card
|
||||
// inside the camera. Retain a 24px grab region so the result stays
|
||||
// directly reachable; geometry/model admission is independent.
|
||||
let width = (header.position.x + header.size.width)
|
||||
.min(self.viewport.position.x + self.viewport.size.width)
|
||||
- header.position.x.max(self.viewport.position.x);
|
||||
let height = (header.position.y + header.size.height)
|
||||
.min(self.viewport.position.y + self.viewport.size.height)
|
||||
- header.position.y.max(self.viewport.position.y);
|
||||
width * self.zoom >= 24. && height * self.zoom >= 24.
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outgoing_header_retains_a_grab_region_without_requiring_full_camera_containment() {
|
||||
let context = PresentationContext {
|
||||
viewport: Rect {
|
||||
position: Position { x: 0., y: 0. },
|
||||
size: Size {
|
||||
width: 800.,
|
||||
height: 400.,
|
||||
},
|
||||
},
|
||||
zoom: 0.75,
|
||||
target: None,
|
||||
};
|
||||
let mut header = Rect {
|
||||
position: Position { x: -100., y: 30. },
|
||||
size: Size {
|
||||
width: 224.,
|
||||
height: 62.,
|
||||
},
|
||||
};
|
||||
assert!(context.header_visible(header, true));
|
||||
assert!(!context.header_visible(header, false));
|
||||
header.position.x = -192.; // exactly 32 world pixels = 24 logical pixels
|
||||
assert!(context.header_visible(header, true));
|
||||
header.position.x -= 1.;
|
||||
assert!(!context.header_visible(header, true));
|
||||
header.position.x = 0.;
|
||||
header.position.y = 368.;
|
||||
assert!(context.header_visible(header, true));
|
||||
header.position.y += 1.;
|
||||
assert!(!context.header_visible(header, true));
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PlacementPlan {
|
||||
pub diagram: Diagram,
|
||||
@@ -546,8 +496,8 @@ pub struct PlacementPlan {
|
||||
commands: Vec<Command>,
|
||||
}
|
||||
|
||||
/// `requested` preserves the frozen pointer offset unless the caller proposes
|
||||
/// bounded outward clearance for an outgoing drop. Snap only
|
||||
/// `requested` is the original selected placement position plus pointer motion
|
||||
/// in the frozen transform (therefore preserving the press offset). Snap only
|
||||
/// this root; apply the resulting identical delta to captured Q and its routes.
|
||||
/// No whole Project clone, validator or semantic ViewIndex rebuild occurs here.
|
||||
pub fn place(
|
||||
@@ -627,7 +577,7 @@ pub fn place(
|
||||
height: selected.size.height.min(62.),
|
||||
},
|
||||
};
|
||||
if !presentation.header_visible(header, plan.destination == Destination::OutsidePerformers) {
|
||||
if !presentation.viewport.contains(header) {
|
||||
return Err(Refusal::LayoutRequired(
|
||||
"Make room or adjust the view, then drag again.".into(),
|
||||
));
|
||||
|
||||
@@ -188,7 +188,7 @@ pub fn place(
|
||||
};
|
||||
geometry::check_rigid(&plan.before_expanded, &expanded, &plan.placements, delta)?;
|
||||
scenes?;
|
||||
geometry::check_headers(&view, &plan.selected, presentation, false)?;
|
||||
geometry::check_headers(&view, &plan.selected, presentation.viewport)?;
|
||||
Ok(PlacementPlan {
|
||||
diagram,
|
||||
view,
|
||||
|
||||
@@ -398,12 +398,7 @@ pub fn place(
|
||||
&expanded,
|
||||
)?;
|
||||
geometry::check_rigid(&scope.0.before_expanded, &expanded, scope.affected(), delta)?;
|
||||
geometry::check_headers(
|
||||
&view,
|
||||
scope.selected(),
|
||||
presentation,
|
||||
plan.destination == Destination::OutsidePerformers,
|
||||
)?;
|
||||
geometry::check_headers(&view, scope.selected(), presentation.viewport)?;
|
||||
Ok(PlacementPlan {
|
||||
diagram,
|
||||
view,
|
||||
|
||||
@@ -170,8 +170,7 @@ pub(super) fn check_rigid(
|
||||
pub(super) fn check_headers(
|
||||
view: &ProjectedView,
|
||||
selected: &BTreeMap<Id, Id>,
|
||||
presentation: PresentationContext,
|
||||
outgoing: bool,
|
||||
viewport: Rect,
|
||||
) -> Result<(), Refusal> {
|
||||
for (id, element) in selected {
|
||||
let node = view
|
||||
@@ -186,7 +185,7 @@ pub(super) fn check_headers(
|
||||
height: node.size.height.min(62.),
|
||||
},
|
||||
};
|
||||
if !presentation.header_visible(header, outgoing) {
|
||||
if !viewport.contains(header) {
|
||||
return Err(Refusal::LayoutRequired(
|
||||
"Make room or adjust the view, then drag again.".into(),
|
||||
));
|
||||
|
||||
@@ -14,8 +14,6 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
mod clearance;
|
||||
|
||||
pub const FINISH_EDIT: &str =
|
||||
"Finish or cancel the current edit before selecting or moving elements.";
|
||||
|
||||
@@ -58,7 +56,6 @@ pub struct Target {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Frozen {
|
||||
pub targets: Vec<Target>,
|
||||
pub moving: Vec<Rectangle>,
|
||||
cards: Vec<Rectangle>,
|
||||
controls: Vec<Rectangle>,
|
||||
edges: Vec<(Point, Point)>,
|
||||
@@ -85,16 +82,14 @@ impl Frozen {
|
||||
) -> Self {
|
||||
let mut targets = Vec::new();
|
||||
let mut cards = Vec::new();
|
||||
let mut moving = Vec::new();
|
||||
for n in &view.nodes {
|
||||
if affected.contains_key(&n.id) {
|
||||
continue;
|
||||
}
|
||||
let region = Rectangle::new(
|
||||
Point::new(n.position.x, n.position.y),
|
||||
iced::Size::new(n.size.width, n.size.height),
|
||||
);
|
||||
if affected.contains_key(&n.id) {
|
||||
moving.push(region);
|
||||
continue;
|
||||
}
|
||||
let Some(e) = project.elements.get(&n.element) else {
|
||||
cards.push(region);
|
||||
continue;
|
||||
@@ -177,7 +172,6 @@ impl Frozen {
|
||||
}
|
||||
Self {
|
||||
targets,
|
||||
moving,
|
||||
cards,
|
||||
controls,
|
||||
edges,
|
||||
@@ -413,6 +407,7 @@ impl Candidate {
|
||||
Self::Performers(p) => &p.accepted,
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
pub fn index(&self) -> &view_options::ViewIndex {
|
||||
match self {
|
||||
Self::Singleton(p) => p.index(),
|
||||
@@ -530,8 +525,6 @@ pub(crate) struct Feedback {
|
||||
pub inspection_targets: Vec<usize>,
|
||||
pub text: String,
|
||||
pub refused: bool,
|
||||
/// Pointer-following outline only; it never authorizes a model change.
|
||||
pub ghost_delta: Option<Vector>,
|
||||
}
|
||||
impl Feedback {
|
||||
pub fn inspection_identity(&self) -> String {
|
||||
@@ -1062,26 +1055,7 @@ impl App {
|
||||
viewport: stamp.viewport(), zoom: stamp.zoom,
|
||||
target: target.map(|i| plan::TargetPresentation { placement: active.frozen.as_ref().unwrap().targets[i].placement }),
|
||||
};
|
||||
let exact = candidate.place(requested, presentation);
|
||||
let placed = if destination == plan::Destination::OutsidePerformers
|
||||
&& matches!(&exact, Err(plan::Refusal::LayoutRequired(_)))
|
||||
{
|
||||
let affected = match &active.prepared {
|
||||
Prepared::Singleton(scope) => Some(&scope.placements),
|
||||
Prepared::Assignment(scope) => Some(scope.affected()),
|
||||
_ => None,
|
||||
};
|
||||
affected.and_then(|affected| {
|
||||
clearance::requests(
|
||||
(self.project(), self.document.view_index()),
|
||||
(candidate.project(), candidate.index()), stamp.context.diagram,
|
||||
affected, *anchor, requested,
|
||||
(stamp.world(press.start), stamp.world(pointer.unwrap())),
|
||||
).into_iter().find_map(|position| candidate.place(position, presentation).ok())
|
||||
}).map_or(exact, Ok)
|
||||
} else {
|
||||
exact
|
||||
}.map_err(|r| refusal(&r))?;
|
||||
let placed = candidate.place(requested, presentation).map_err(|r| refusal(&r))?;
|
||||
let label = candidate.label();
|
||||
if release {
|
||||
if let Some(command) = candidate.finish(self.project(), &placed).map_err(|r| refusal(&r))? {
|
||||
@@ -1108,14 +1082,6 @@ impl App {
|
||||
} else {
|
||||
let inspection_targets = active.inspection_targets.clone();
|
||||
let frozen = active.frozen.clone();
|
||||
// A rejected intermediate position is not a return gesture. Keep the
|
||||
// last admitted scene and show current motion separately; release
|
||||
// always recomputes admission and never commits this retained view.
|
||||
if refused && in_bounds && preview.is_none() {
|
||||
preview = self.allocation_drag.preview.take();
|
||||
}
|
||||
let ghost_delta = (refused && in_bounds)
|
||||
.then(|| (pointer.unwrap() - press.start) * (1. / stamp.zoom));
|
||||
self.allocation_drag.active = Some(active);
|
||||
self.allocation_drag.preview = preview;
|
||||
self.allocation_drag.feedback = Some(Feedback {
|
||||
@@ -1126,7 +1092,6 @@ impl App {
|
||||
inspection_targets,
|
||||
text,
|
||||
refused,
|
||||
ghost_delta,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,595 +0,0 @@
|
||||
//! Bounded outgoing clearance proposals. The ordinary placement and semantic
|
||||
//! validators still decide whether any proposal may be previewed or committed.
|
||||
use iced::Point;
|
||||
use oa_core::{Id, NodePlacement, Position, Project, projection, view_options::ViewIndex};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
const GRID: f32 = 10.;
|
||||
const GAP: f32 = 10.;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct Bounds {
|
||||
left: f32,
|
||||
top: f32,
|
||||
right: f32,
|
||||
bottom: f32,
|
||||
}
|
||||
|
||||
impl Bounds {
|
||||
fn node(node: &projection::ProjectedNode) -> Option<Self> {
|
||||
let result = Self {
|
||||
left: node.position.x,
|
||||
top: node.position.y,
|
||||
right: node.position.x + node.size.width,
|
||||
bottom: node.position.y + node.size.height,
|
||||
};
|
||||
([result.left, result.top, result.right, result.bottom]
|
||||
.iter()
|
||||
.all(|value| value.is_finite())
|
||||
&& result.right > result.left
|
||||
&& result.bottom > result.top)
|
||||
.then_some(result)
|
||||
}
|
||||
|
||||
fn union(self, other: Self) -> Self {
|
||||
Self {
|
||||
left: self.left.min(other.left),
|
||||
top: self.top.min(other.top),
|
||||
right: self.right.max(other.right),
|
||||
bottom: self.bottom.max(other.bottom),
|
||||
}
|
||||
}
|
||||
|
||||
fn overlaps(self, other: Self) -> bool {
|
||||
self.left < other.right
|
||||
&& other.left < self.right
|
||||
&& self.top < other.bottom
|
||||
&& other.top < self.bottom
|
||||
}
|
||||
}
|
||||
|
||||
/// Called only after an OutsidePerformers candidate fails ordinary placement.
|
||||
/// Coordinates use the frozen world transform. The entire affected footprint
|
||||
/// receives one anchor translation; this never moves or packs other cards.
|
||||
pub(super) fn requests(
|
||||
accepted: (&Project, &ViewIndex),
|
||||
candidate: (&Project, &ViewIndex),
|
||||
diagram: Id,
|
||||
affected: &BTreeMap<Id, NodePlacement>,
|
||||
anchor: Id,
|
||||
requested: Position,
|
||||
motion: (Point, Point),
|
||||
) -> Vec<Position> {
|
||||
let (accepted, accepted_index) = accepted;
|
||||
let (candidate, candidate_index) = candidate;
|
||||
let (start, pointer) = motion;
|
||||
if ![
|
||||
requested.x,
|
||||
requested.y,
|
||||
start.x,
|
||||
start.y,
|
||||
pointer.x,
|
||||
pointer.y,
|
||||
]
|
||||
.iter()
|
||||
.all(|value| value.is_finite())
|
||||
{
|
||||
return Vec::new();
|
||||
}
|
||||
let (Some(original), Some(base), Some(anchor)) = (
|
||||
accepted.diagrams.get(&diagram),
|
||||
candidate.diagrams.get(&diagram),
|
||||
affected.get(&anchor),
|
||||
) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let snapped = Position {
|
||||
x: (requested.x / GRID).round() * GRID,
|
||||
y: (requested.y / GRID).round() * GRID,
|
||||
};
|
||||
let delta = Position {
|
||||
x: snapped.x - anchor.position.x,
|
||||
y: snapped.y - anchor.position.y,
|
||||
};
|
||||
let before = projection::project_diagram_with_index(accepted, original, accepted_index);
|
||||
let before_nodes: BTreeMap<_, _> = before.nodes.iter().map(|node| (node.id, node)).collect();
|
||||
let mut sources = BTreeSet::new();
|
||||
for node in before
|
||||
.nodes
|
||||
.iter()
|
||||
.filter(|node| affected.contains_key(&node.id))
|
||||
{
|
||||
let mut parent = node.visual_parent;
|
||||
let mut seen = BTreeSet::new();
|
||||
while let Some(id) = parent {
|
||||
if !seen.insert(id) {
|
||||
return Vec::new();
|
||||
}
|
||||
let Some(node) = before_nodes.get(&id) else {
|
||||
break;
|
||||
};
|
||||
if !affected.contains_key(&id)
|
||||
&& accepted
|
||||
.elements
|
||||
.get(&node.element)
|
||||
.is_some_and(|element| element.kind().is_performer())
|
||||
{
|
||||
sources.insert(id);
|
||||
}
|
||||
parent = node.visual_parent;
|
||||
}
|
||||
}
|
||||
if sources.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
// Projection after responsibility removal supplies the former performer's
|
||||
// remaining enclosure, including other allocated work. Projecting the moved
|
||||
// block also includes the full visible descendant enclosure of every root.
|
||||
let mut moved = base.clone();
|
||||
for (id, captured) in affected {
|
||||
let Some(node) = moved.nodes.get_mut(id) else {
|
||||
return Vec::new();
|
||||
};
|
||||
node.position = Position {
|
||||
x: captured.position.x + delta.x,
|
||||
y: captured.position.y + delta.y,
|
||||
};
|
||||
if ![
|
||||
node.position.x,
|
||||
node.position.y,
|
||||
node.size.width,
|
||||
node.size.height,
|
||||
]
|
||||
.iter()
|
||||
.all(|value| value.is_finite())
|
||||
{
|
||||
return Vec::new();
|
||||
}
|
||||
}
|
||||
let after = projection::project_diagram_with_index(candidate, &moved, candidate_index);
|
||||
let mut footprint: Option<Bounds> = None;
|
||||
for node in after
|
||||
.nodes
|
||||
.iter()
|
||||
.filter(|node| affected.contains_key(&node.id))
|
||||
{
|
||||
let Some(bounds) = Bounds::node(node) else {
|
||||
return Vec::new();
|
||||
};
|
||||
footprint = Some(footprint.map_or(bounds, |previous| previous.union(bounds)));
|
||||
}
|
||||
let Some(footprint) = footprint else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut overlaps = Vec::new();
|
||||
for node in after.nodes.iter().filter(|node| sources.contains(&node.id)) {
|
||||
let (Some(old), Some(current)) = (Bounds::node(before_nodes[&node.id]), Bounds::node(node))
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
if footprint.overlaps(current) {
|
||||
overlaps.push((old, current));
|
||||
}
|
||||
}
|
||||
if overlaps.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
// Each entry is a half-plane actually crossed by the pointer. At a corner,
|
||||
// prefer the boundary reached first on the press-to-pointer segment. The
|
||||
// direction index is a stable tie-break, independent of placement UUIDs.
|
||||
let mut crossings = [None::<f32>; 4];
|
||||
for (old, _) in &overlaps {
|
||||
for (direction, initial, final_, boundary, outward) in [
|
||||
(
|
||||
0,
|
||||
start.x,
|
||||
pointer.x,
|
||||
old.left,
|
||||
pointer.x < old.left && start.x >= old.left,
|
||||
),
|
||||
(
|
||||
1,
|
||||
start.x,
|
||||
pointer.x,
|
||||
old.right,
|
||||
pointer.x >= old.right && start.x < old.right,
|
||||
),
|
||||
(
|
||||
2,
|
||||
start.y,
|
||||
pointer.y,
|
||||
old.top,
|
||||
pointer.y < old.top && start.y >= old.top,
|
||||
),
|
||||
(
|
||||
3,
|
||||
start.y,
|
||||
pointer.y,
|
||||
old.bottom,
|
||||
pointer.y >= old.bottom && start.y < old.bottom,
|
||||
),
|
||||
] {
|
||||
if outward {
|
||||
let fraction = (boundary - initial) / (final_ - initial);
|
||||
if fraction.is_finite() && (0. ..=1.).contains(&fraction) {
|
||||
crossings[direction] =
|
||||
Some(crossings[direction].map_or(fraction, |old| old.min(fraction)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut directions: Vec<_> = crossings
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.filter_map(|(direction, fraction)| fraction.map(|fraction| (direction, fraction)))
|
||||
.collect();
|
||||
directions.sort_by(|a, b| a.1.total_cmp(&b.1).then(a.0.cmp(&b.0)));
|
||||
let mut result = Vec::new();
|
||||
for (direction, _) in directions {
|
||||
let mut corrected = snapped;
|
||||
match direction {
|
||||
0 => {
|
||||
let edge = overlaps
|
||||
.iter()
|
||||
.map(|(_, current)| current.left)
|
||||
.fold(f32::INFINITY, f32::min);
|
||||
corrected.x = ((snapped.x + edge - GAP - footprint.right) / GRID).floor() * GRID;
|
||||
}
|
||||
1 => {
|
||||
let edge = overlaps
|
||||
.iter()
|
||||
.map(|(_, current)| current.right)
|
||||
.fold(f32::NEG_INFINITY, f32::max);
|
||||
corrected.x = ((snapped.x + edge + GAP - footprint.left) / GRID).ceil() * GRID;
|
||||
}
|
||||
2 => {
|
||||
let edge = overlaps
|
||||
.iter()
|
||||
.map(|(_, current)| current.top)
|
||||
.fold(f32::INFINITY, f32::min);
|
||||
corrected.y = ((snapped.y + edge - GAP - footprint.bottom) / GRID).floor() * GRID;
|
||||
}
|
||||
3 => {
|
||||
let edge = overlaps
|
||||
.iter()
|
||||
.map(|(_, current)| current.bottom)
|
||||
.fold(f32::NEG_INFINITY, f32::max);
|
||||
corrected.y = ((snapped.y + edge + GAP - footprint.top) / GRID).ceil() * GRID;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
if corrected.x.is_finite() && corrected.y.is_finite() && !result.contains(&corrected) {
|
||||
result.push(corrected);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::allocation_drag as plan;
|
||||
use oa_core::{Kind, Layer, ProjectionKind, RelationKind, Size};
|
||||
|
||||
struct Fixture {
|
||||
project: Project,
|
||||
diagram: Id,
|
||||
performer: Id,
|
||||
work: Id,
|
||||
node: Id,
|
||||
}
|
||||
|
||||
fn xy(x: f32, y: f32) -> Position {
|
||||
Position { x, y }
|
||||
}
|
||||
|
||||
fn fixture(layer: Layer) -> Fixture {
|
||||
let mut project = Project::empty();
|
||||
let (performer, work) = match layer {
|
||||
Layer::Oa => (Kind::Entity, Kind::Activity),
|
||||
Layer::Sa => (Kind::System, Kind::Function),
|
||||
};
|
||||
let performer = project
|
||||
.add_element(performer, layer, "Performer", None)
|
||||
.unwrap();
|
||||
let work = project.add_element(work, layer, "Work", None).unwrap();
|
||||
let diagram = project.ensure_diagram(layer);
|
||||
project.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Allocation;
|
||||
let performer = project.place(diagram, performer, xy(100., 100.));
|
||||
project
|
||||
.diagrams
|
||||
.get_mut(&diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&performer)
|
||||
.unwrap()
|
||||
.size = Size {
|
||||
width: 400.,
|
||||
height: 250.,
|
||||
};
|
||||
let node = project.place(diagram, work, xy(160., 180.));
|
||||
let owner = project.diagrams[&diagram].nodes[&performer].element;
|
||||
project.relate(RelationKind::Allocation, "Responsibility", owner, work);
|
||||
Fixture {
|
||||
project,
|
||||
diagram,
|
||||
performer,
|
||||
work,
|
||||
node,
|
||||
}
|
||||
}
|
||||
|
||||
fn semantic(f: &Fixture) -> (plan::Scope, plan::SemanticPlan) {
|
||||
let scope = plan::capture(&f.project, f.diagram, f.work, f.node).unwrap();
|
||||
let semantic = plan::plan_semantics(
|
||||
&f.project,
|
||||
&scope,
|
||||
plan::Destination::OutsidePerformers,
|
||||
&BTreeMap::new(),
|
||||
)
|
||||
.unwrap();
|
||||
(scope, semantic)
|
||||
}
|
||||
|
||||
fn presentation() -> plan::PresentationContext {
|
||||
plan::PresentationContext {
|
||||
viewport: plan::Rect {
|
||||
position: xy(-2000., -2000.),
|
||||
size: Size {
|
||||
width: 5000.,
|
||||
height: 5000.,
|
||||
},
|
||||
},
|
||||
zoom: 1.,
|
||||
target: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_outgoing_edge_clears_the_source_and_passes_ordinary_placement() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let f = fixture(layer);
|
||||
let (scope, semantic) = semantic(&f);
|
||||
for (start, pointer, requested, expected) in [
|
||||
(
|
||||
Point::new(170., 210.),
|
||||
Point::new(99., 210.),
|
||||
xy(89., 180.),
|
||||
xy(-140., 180.),
|
||||
),
|
||||
(
|
||||
Point::new(340., 210.),
|
||||
Point::new(501., 210.),
|
||||
xy(321., 180.),
|
||||
xy(510., 180.),
|
||||
),
|
||||
(
|
||||
Point::new(200., 190.),
|
||||
Point::new(200., 99.),
|
||||
xy(160., 89.),
|
||||
xy(160., -10.),
|
||||
),
|
||||
(
|
||||
Point::new(200., 260.),
|
||||
Point::new(200., 351.),
|
||||
xy(160., 271.),
|
||||
xy(160., 360.),
|
||||
),
|
||||
] {
|
||||
assert!(matches!(
|
||||
plan::place(&semantic, requested, presentation()),
|
||||
Err(plan::Refusal::LayoutRequired(_))
|
||||
));
|
||||
let proposed = requests(
|
||||
(&f.project, &ViewIndex::new(&f.project)),
|
||||
(semantic.project(), semantic.index()),
|
||||
f.diagram,
|
||||
&scope.placements,
|
||||
f.node,
|
||||
requested,
|
||||
(start, pointer),
|
||||
);
|
||||
assert_eq!(proposed, vec![expected]);
|
||||
let placed = plan::place(&semantic, proposed[0], presentation()).unwrap();
|
||||
assert!(
|
||||
plan::finish(&semantic, &f.project, &placed)
|
||||
.unwrap()
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_shrink_uses_candidate_enclosure_not_frozen_target() {
|
||||
let mut f = fixture(Layer::Oa);
|
||||
f.project
|
||||
.diagrams
|
||||
.get_mut(&f.diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&f.performer)
|
||||
.unwrap()
|
||||
.size = Size {
|
||||
width: 224.,
|
||||
height: 94.,
|
||||
};
|
||||
let (scope, semantic) = semantic(&f);
|
||||
let proposed = requests(
|
||||
(&f.project, &ViewIndex::new(&f.project)),
|
||||
(semantic.project(), semantic.index()),
|
||||
f.diagram,
|
||||
&scope.placements,
|
||||
f.node,
|
||||
xy(229., 180.),
|
||||
(Point::new(340., 210.), Point::new(409., 210.)),
|
||||
);
|
||||
assert_eq!(proposed, vec![xy(340., 180.)]);
|
||||
plan::place(&semantic, proposed[0], presentation()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corner_order_follows_boundary_crossing_and_is_deterministic() {
|
||||
let f = fixture(Layer::Sa);
|
||||
let (scope, semantic) = semantic(&f);
|
||||
// Right crossed at 160/161 of the segment; bottom at 90/91.
|
||||
let args = (
|
||||
xy(321., 271.),
|
||||
Point::new(340., 260.),
|
||||
Point::new(501., 351.),
|
||||
);
|
||||
let proposed = requests(
|
||||
(&f.project, &ViewIndex::new(&f.project)),
|
||||
(semantic.project(), semantic.index()),
|
||||
f.diagram,
|
||||
&scope.placements,
|
||||
f.node,
|
||||
args.0,
|
||||
(args.1, args.2),
|
||||
);
|
||||
assert_eq!(proposed, vec![xy(320., 360.), xy(510., 270.)]);
|
||||
assert_eq!(
|
||||
proposed,
|
||||
requests(
|
||||
(&f.project, &ViewIndex::new(&f.project)),
|
||||
(semantic.project(), semantic.index()),
|
||||
f.diagram,
|
||||
&scope.placements,
|
||||
f.node,
|
||||
args.0,
|
||||
(args.1, args.2)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_crossing_no_overlap_no_source_and_nonfinite_requests_are_empty() {
|
||||
let f = fixture(Layer::Oa);
|
||||
let (scope, semantic) = semantic(&f);
|
||||
for (requested, pointer) in [
|
||||
(xy(170., 180.), Point::new(210., 210.)),
|
||||
(xy(700., 180.), Point::new(740., 210.)),
|
||||
(xy(f32::NAN, 180.), Point::new(99., 210.)),
|
||||
(xy(89., 180.), Point::new(f32::INFINITY, 210.)),
|
||||
] {
|
||||
assert!(
|
||||
requests(
|
||||
(&f.project, &ViewIndex::new(&f.project)),
|
||||
(semantic.project(), semantic.index()),
|
||||
f.diagram,
|
||||
&scope.placements,
|
||||
f.node,
|
||||
requested,
|
||||
(Point::new(200., 210.), pointer)
|
||||
)
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
requests(
|
||||
(semantic.project(), semantic.index()),
|
||||
(semantic.project(), semantic.index()),
|
||||
f.diagram,
|
||||
&scope.placements,
|
||||
f.node,
|
||||
xy(89., 180.),
|
||||
(Point::new(200., 210.), Point::new(99., 210.))
|
||||
)
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn descendant_footprint_controls_clearance_without_mutating_projects() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = fixture(layer);
|
||||
let kind = f.project.elements[&f.work].kind();
|
||||
let child = f
|
||||
.project
|
||||
.add_element(kind, layer, "Child", Some(f.work))
|
||||
.unwrap();
|
||||
let child_node = f.project.place(f.diagram, child, xy(240., 270.));
|
||||
// First-child allocation transfer is normally performed by commands;
|
||||
// this fixture expresses the resulting leaf allocation directly.
|
||||
f.project.relations.clear();
|
||||
let owner = f.project.diagrams[&f.diagram].nodes[&f.performer].element;
|
||||
f.project.relate(
|
||||
RelationKind::Allocation,
|
||||
"Leaf responsibility",
|
||||
owner,
|
||||
child,
|
||||
);
|
||||
let (scope, semantic) = semantic(&f);
|
||||
assert!(scope.placements.contains_key(&child_node));
|
||||
let before = f.project.clone();
|
||||
let candidate = semantic.project().clone();
|
||||
let proposed = requests(
|
||||
(&f.project, &ViewIndex::new(&f.project)),
|
||||
(&candidate, semantic.index()),
|
||||
f.diagram,
|
||||
&scope.placements,
|
||||
f.node,
|
||||
xy(89., 180.),
|
||||
(Point::new(170., 210.), Point::new(99., 210.)),
|
||||
);
|
||||
assert_eq!(proposed, vec![xy(-240., 180.)]);
|
||||
let placed = plan::place(&semantic, proposed[0], presentation()).unwrap();
|
||||
let root = &placed.diagram.nodes[&f.node];
|
||||
let child = &placed.diagram.nodes[&child_node];
|
||||
assert_eq!(child.position.x - root.position.x, 80.);
|
||||
assert_eq!(child.position.y - root.position.y, 90.);
|
||||
assert_eq!(f.project, before);
|
||||
assert_eq!(semantic.project(), &candidate);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn independent_selected_roots_keep_one_rigid_group_translation() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = fixture(layer);
|
||||
let kind = f.project.elements[&f.work].kind();
|
||||
let second = f
|
||||
.project
|
||||
.add_element(kind, layer, "Second work", None)
|
||||
.unwrap();
|
||||
let second_node = f.project.place(f.diagram, second, xy(240., 300.));
|
||||
let owner = f.project.diagrams[&f.diagram].nodes[&f.performer].element;
|
||||
f.project.relate(
|
||||
RelationKind::Allocation,
|
||||
"Second responsibility",
|
||||
owner,
|
||||
second,
|
||||
);
|
||||
let selected = BTreeMap::from([(f.node, f.work), (second_node, second)]);
|
||||
let scope =
|
||||
plan::assignment::capture(&f.project, f.diagram, &selected, f.node).unwrap();
|
||||
let semantic = plan::assignment::plan_semantics(
|
||||
&scope,
|
||||
plan::Destination::OutsidePerformers,
|
||||
&BTreeMap::new(),
|
||||
)
|
||||
.unwrap();
|
||||
let proposed = requests(
|
||||
(&f.project, &ViewIndex::new(&f.project)),
|
||||
(semantic.project(), semantic.index()),
|
||||
f.diagram,
|
||||
scope.affected(),
|
||||
f.node,
|
||||
xy(89., 180.),
|
||||
(Point::new(170., 210.), Point::new(99., 210.)),
|
||||
);
|
||||
assert_eq!(proposed, vec![xy(-220., 180.)]);
|
||||
let placed = plan::assignment::place(&semantic, proposed[0], presentation()).unwrap();
|
||||
let root = &placed.diagram.nodes[&f.node];
|
||||
let second = &placed.diagram.nodes[&second_node];
|
||||
assert_eq!(second.position.x - root.position.x, 80.);
|
||||
assert_eq!(second.position.y - root.position.y, 120.);
|
||||
assert!(
|
||||
plan::assignment::finish(&semantic, &f.project, &placed)
|
||||
.unwrap()
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,117 +30,6 @@ async fn mount() -> (Fixture, Mounted) {
|
||||
mounted.settle(&mut f.app).await;
|
||||
(f, mounted)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn actual_tree_outgoing_boundary_motion_preview_release_and_cancel() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = outgoing_fixture(layer);
|
||||
f.app.layer = layer;
|
||||
f.app.window_size = iced::Size::new(1440., 900.);
|
||||
let _ = f.app.update(Message::Select(f.work));
|
||||
let mut mounted = Mounted::new(&mut f.app).await;
|
||||
f.app.zoom = 0.75;
|
||||
f.app.pan = Vector::new(20., 15.);
|
||||
f.app.fit_to_view = false;
|
||||
mounted.settle(&mut f.app).await;
|
||||
let original = f.app.project().clone();
|
||||
let source = f.app.document.bundle().clone();
|
||||
let from = at(&mut mounted, &f, xy(660., 225.));
|
||||
mounted
|
||||
.batch(
|
||||
&mut f.app,
|
||||
&[
|
||||
Input::Mouse(mouse::Event::CursorMoved { position: from }),
|
||||
Input::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
|
||||
],
|
||||
mouse::Cursor::Available(from),
|
||||
)
|
||||
.await;
|
||||
// Separate input/update/layout turns expose the held boundary transition
|
||||
// that a one-batch distant drop cannot exercise.
|
||||
for x in [620., 560., 510., 499., 490.] {
|
||||
let to = at(&mut mounted, &f, xy(x, 225.));
|
||||
mounted
|
||||
.batch(
|
||||
&mut f.app,
|
||||
&[Input::Mouse(mouse::Event::CursorMoved { position: to })],
|
||||
mouse::Cursor::Available(to),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(f.app.project(), &original);
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
assert!(
|
||||
!f.app.allocation_drag.feedback.as_ref().unwrap().refused,
|
||||
"{layer:?} x={x}: {}",
|
||||
f.app.status
|
||||
);
|
||||
assert!(f.app.allocation_drag.preview.is_some());
|
||||
let _ = mounted.pixels(&f.app);
|
||||
}
|
||||
let expected = f
|
||||
.app
|
||||
.allocation_drag
|
||||
.preview
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.placement
|
||||
.diagram
|
||||
.clone();
|
||||
let to = at(&mut mounted, &f, xy(490., 225.));
|
||||
mounted
|
||||
.batch(
|
||||
&mut f.app,
|
||||
&[Input::Mouse(mouse::Event::ButtonReleased(
|
||||
mouse::Button::Left,
|
||||
))],
|
||||
mouse::Cursor::Available(to),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(&f.app.project().diagrams[&f.diagram], &expected);
|
||||
assert!(
|
||||
!f.app
|
||||
.project()
|
||||
.relations
|
||||
.values()
|
||||
.any(|r| r.kind() == RelationKind::Allocation && r.target() == f.work)
|
||||
);
|
||||
let _ = f.app.update(Message::Undo);
|
||||
mounted.settle(&mut f.app).await;
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
let from = at(&mut mounted, &f, xy(660., 225.));
|
||||
let to = at(&mut mounted, &f, xy(490., 225.));
|
||||
mounted
|
||||
.batch(
|
||||
&mut f.app,
|
||||
&[
|
||||
Input::Mouse(mouse::Event::CursorMoved { position: from }),
|
||||
Input::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
|
||||
Input::Mouse(mouse::Event::CursorMoved { position: to }),
|
||||
],
|
||||
mouse::Cursor::Available(to),
|
||||
)
|
||||
.await;
|
||||
mounted
|
||||
.batch(
|
||||
&mut f.app,
|
||||
&[key(keyboard::key::Named::Escape)],
|
||||
mouse::Cursor::Available(to),
|
||||
)
|
||||
.await;
|
||||
mounted
|
||||
.batch(
|
||||
&mut f.app,
|
||||
&[Input::Mouse(mouse::Event::ButtonReleased(
|
||||
mouse::Button::Left,
|
||||
))],
|
||||
mouse::Cursor::Available(to),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
assert!(f.app.allocation_drag.preview.is_none());
|
||||
assert!(!f.app.document.can_undo());
|
||||
}
|
||||
}
|
||||
fn canvas_bounds(mounted: &mut Mounted, app: &App) -> Rectangle {
|
||||
mounted
|
||||
.geometry(app)
|
||||
@@ -592,7 +481,6 @@ async fn actual_app_tree_exact_incoming_drag_keeps_destination_visible_and_canva
|
||||
if pan.y == -100. {
|
||||
// A genuine clipping refusal in the same held gesture, then
|
||||
// recovery at the original exact point: no camera rebasing.
|
||||
let retained = f.app.allocation_drag.preview.clone().unwrap();
|
||||
let clipped = at(&mut mounted, &f, xy(160., 110.));
|
||||
mounted
|
||||
.batch(
|
||||
@@ -604,19 +492,7 @@ async fn actual_app_tree_exact_incoming_drag_keeps_destination_visible_and_canva
|
||||
)
|
||||
.await;
|
||||
assert!(f.app.allocation_drag.feedback.as_ref().unwrap().refused);
|
||||
assert!(Arc::ptr_eq(
|
||||
f.app.allocation_drag.preview.as_ref().unwrap(),
|
||||
&retained
|
||||
));
|
||||
assert!(
|
||||
f.app
|
||||
.allocation_drag
|
||||
.feedback
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.ghost_delta
|
||||
.is_some()
|
||||
);
|
||||
assert!(f.app.allocation_drag.preview.is_none());
|
||||
assert_eq!(canvas_bounds(&mut mounted, &f.app), canvas);
|
||||
assert_eq!(destination_bounds(&mut mounted, &f.app), slot);
|
||||
assert_eq!(f.app.project(), &before);
|
||||
|
||||
@@ -1239,7 +1239,6 @@ fn destination_identity_does_not_describe_blocked_or_outside_viewport_as_unalloc
|
||||
inspection_targets: vec![],
|
||||
text: "A card blocks this drop".into(),
|
||||
refused: true,
|
||||
ghost_delta: None,
|
||||
};
|
||||
assert_eq!(feedback.destination_identity(), "No eligible destination");
|
||||
feedback.text = "Release inside the captured canvas".into();
|
||||
@@ -1247,361 +1246,3 @@ fn destination_identity_does_not_describe_blocked_or_outside_viewport_as_unalloc
|
||||
feedback.refused = false;
|
||||
assert_eq!(feedback.destination_identity(), "Outside performers");
|
||||
}
|
||||
|
||||
fn outgoing_fixture(layer: Layer) -> Fixture {
|
||||
let mut f = fixture(layer);
|
||||
drag(&mut f, screen(xy(160., 190.)));
|
||||
let mut project = f.app.project().clone();
|
||||
let d = project.diagrams.get_mut(&f.diagram).unwrap();
|
||||
let performer = d.nodes.values_mut().find(|n| n.element == f.a).unwrap();
|
||||
performer.position = xy(500., 100.);
|
||||
performer.size = Size {
|
||||
width: 300.,
|
||||
height: 300.,
|
||||
};
|
||||
d.nodes.get_mut(&f.node).unwrap().position = xy(540., 190.);
|
||||
replace(&mut f, project);
|
||||
f
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outgoing_boundary_drop_clears_source_without_changing_other_placements() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = outgoing_fixture(layer);
|
||||
let before = f.app.project().clone();
|
||||
let source = f.app.document.bundle().clone();
|
||||
let from = screen(xy(660., 225.));
|
||||
let to = screen(xy(490., 225.));
|
||||
let mut events = batch(&f.app, from, to);
|
||||
let release = events.pop().unwrap();
|
||||
send(&mut f.app, events);
|
||||
let feedback = f.app.allocation_drag.feedback.as_ref().unwrap();
|
||||
assert!(!feedback.refused, "{}", feedback.text);
|
||||
let preview = f.app.allocation_drag.preview.as_ref().unwrap();
|
||||
let expected_diagram = preview.placement.diagram.clone();
|
||||
let work = &expected_diagram.nodes[&f.node];
|
||||
assert!(work.position.x + work.size.width <= 490.);
|
||||
assert_eq!(work.position.y, 190.);
|
||||
for (id, node) in &before.diagrams[&f.diagram].nodes {
|
||||
if *id != f.node {
|
||||
assert_eq!(&expected_diagram.nodes[id], node);
|
||||
}
|
||||
}
|
||||
assert_eq!(f.app.project(), &before);
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
let _ = f.app.update(release);
|
||||
assert_eq!(&f.app.project().diagrams[&f.diagram], &expected_diagram);
|
||||
assert!(
|
||||
!f.app
|
||||
.project()
|
||||
.relations
|
||||
.values()
|
||||
.any(|r| r.kind() == RelationKind::Allocation && r.target() == f.work)
|
||||
);
|
||||
let after = f.app.project().clone();
|
||||
let saved = f.app.document.bundle().clone();
|
||||
let _ = f.app.update(Message::Undo);
|
||||
assert_eq!(f.app.project(), &before);
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
assert!(!f.app.document.can_undo());
|
||||
let _ = f.app.update(Message::Redo);
|
||||
assert_eq!(f.app.project(), &after);
|
||||
assert_eq!(f.app.document.bundle(), &saved);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outgoing_clearance_collision_refuses_release_without_source_or_history_mutation() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = outgoing_fixture(layer);
|
||||
let mut project = f.app.project().clone();
|
||||
let obstacle = project
|
||||
.add_element(
|
||||
project.elements[&f.work].kind(),
|
||||
layer,
|
||||
"Unrelated work",
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let obstacle_node = project.place(f.diagram, obstacle, xy(240., 190.));
|
||||
replace(&mut f, project);
|
||||
// A refused release must preserve an existing redo branch, too.
|
||||
let _ = f.app.update(Message::Command(Command::Rename(
|
||||
f.work,
|
||||
"Temporary".into(),
|
||||
)));
|
||||
let _ = f.app.update(Message::Undo);
|
||||
let before = f.app.project().clone();
|
||||
let source = f.app.document.bundle().clone();
|
||||
let snapshot = f.app.document.snapshot();
|
||||
let revision = f.app.document.revision();
|
||||
let from = screen(xy(660., 225.));
|
||||
let to = screen(xy(490., 225.));
|
||||
let mut events = batch(&f.app, from, to);
|
||||
let release = events.pop().unwrap();
|
||||
send(&mut f.app, events);
|
||||
|
||||
let active = f.app.allocation_drag.active.as_ref().unwrap();
|
||||
let p = &active.press;
|
||||
let frozen = active.frozen.as_ref().unwrap();
|
||||
let owner = frozen.targets.iter().find(|t| t.element == f.a).unwrap();
|
||||
assert_eq!(p.start, from);
|
||||
assert!(contains(owner.region, p.stamp.world(from)));
|
||||
assert!(p.stamp.world(to).x < owner.region.x);
|
||||
assert_eq!(
|
||||
frozen.resolve(p.stamp, Some(to), false),
|
||||
Hit::Destination(plan::Destination::OutsidePerformers, None),
|
||||
"The pointer must reach empty canvas, not be occluded by the obstacle"
|
||||
);
|
||||
let candidate = active
|
||||
.cache
|
||||
.iter()
|
||||
.find(|(destination, _)| *destination == plan::Destination::OutsidePerformers)
|
||||
.unwrap()
|
||||
.1
|
||||
.as_ref()
|
||||
.unwrap();
|
||||
let affected = match &active.prepared {
|
||||
Prepared::Singleton(scope) => &scope.placements,
|
||||
Prepared::Assignment(scope) => scope.affected(),
|
||||
_ => panic!("expected assignment gesture"),
|
||||
};
|
||||
let requested = xy(370., 190.);
|
||||
let presentation = plan::PresentationContext {
|
||||
viewport: p.stamp.viewport(),
|
||||
zoom: p.stamp.zoom,
|
||||
target: None,
|
||||
};
|
||||
assert!(matches!(
|
||||
candidate.place(requested, presentation),
|
||||
Err(plan::Refusal::LayoutRequired(_))
|
||||
));
|
||||
let proposed = clearance::requests(
|
||||
(f.app.project(), f.app.document.view_index()),
|
||||
(candidate.project(), candidate.index()),
|
||||
f.diagram,
|
||||
affected,
|
||||
f.node,
|
||||
requested,
|
||||
(p.stamp.world(from), p.stamp.world(to)),
|
||||
);
|
||||
assert_eq!(
|
||||
proposed.len(),
|
||||
1,
|
||||
"a genuine left crossing must propose clearance"
|
||||
);
|
||||
assert!(proposed[0].x < requested.x);
|
||||
let work = &before.diagrams[&f.diagram].nodes[&f.node];
|
||||
let obstacle = &before.diagrams[&f.diagram].nodes[&obstacle_node];
|
||||
assert!(obstacle.position.x + obstacle.size.width < 490.);
|
||||
assert!(proposed[0].x + work.size.width <= owner.region.x - 10.);
|
||||
assert!(proposed[0].x < obstacle.position.x + obstacle.size.width);
|
||||
assert!(obstacle.position.x < proposed[0].x + work.size.width);
|
||||
assert_eq!(proposed[0].y, obstacle.position.y);
|
||||
assert!(matches!(
|
||||
candidate.place(proposed[0], presentation),
|
||||
Err(plan::Refusal::LayoutRequired(message)) if message.contains("overlapping cards")
|
||||
));
|
||||
let feedback = f.app.allocation_drag.feedback.as_ref().unwrap();
|
||||
assert!(feedback.refused, "{}", feedback.text);
|
||||
assert!(feedback.ghost_delta.is_some());
|
||||
assert!(f.app.allocation_drag.preview.is_none());
|
||||
assert_eq!(f.app.project(), &before);
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
assert!(Arc::ptr_eq(&snapshot, &f.app.document.snapshot()));
|
||||
let _ = f.app.update(release);
|
||||
assert!(
|
||||
f.app.status.contains("overlapping cards"),
|
||||
"{}",
|
||||
f.app.status
|
||||
);
|
||||
assert!(f.app.allocation_drag.active.is_none());
|
||||
assert!(f.app.allocation_drag.preview.is_none());
|
||||
assert_eq!(f.app.project(), &before);
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
assert_eq!(f.app.document.revision(), revision);
|
||||
assert!(Arc::ptr_eq(&snapshot, &f.app.document.snapshot()));
|
||||
assert!(!f.app.document.can_undo());
|
||||
assert!(f.app.document.can_redo());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outgoing_clearance_preserves_source_enclosure_and_allocated_sibling() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = outgoing_fixture(layer);
|
||||
let mut project = f.app.project().clone();
|
||||
let sibling = project
|
||||
.add_element(
|
||||
project.elements[&f.work].kind(),
|
||||
layer,
|
||||
"Remaining work",
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let sibling_node = project.place(f.diagram, sibling, xy(540., 450.));
|
||||
let allocation = project.relate(
|
||||
RelationKind::Allocation,
|
||||
"Keep responsibility",
|
||||
f.a,
|
||||
sibling,
|
||||
);
|
||||
replace(&mut f, project);
|
||||
let before = f.app.project().clone();
|
||||
let source = f.app.document.bundle().clone();
|
||||
let view = projection::project_view(&before, f.diagram);
|
||||
let owner = view.nodes.iter().find(|n| n.element == f.a).unwrap();
|
||||
let bottom = owner.position.y + owner.size.height;
|
||||
let stored_owner = &before.diagrams[&f.diagram].nodes[&owner.id];
|
||||
assert!(
|
||||
bottom > stored_owner.position.y + stored_owner.size.height,
|
||||
"The retained sibling must enlarge the projected source enclosure"
|
||||
);
|
||||
let from = screen(xy(660., 225.));
|
||||
let to = screen(xy(660., bottom + 1.));
|
||||
let mut events = batch(&f.app, from, to);
|
||||
let release = events.pop().unwrap();
|
||||
send(&mut f.app, events);
|
||||
let active = f.app.allocation_drag.active.as_ref().unwrap();
|
||||
let p = &active.press;
|
||||
let frozen = active.frozen.as_ref().unwrap();
|
||||
let target = frozen.targets.iter().find(|t| t.element == f.a).unwrap();
|
||||
assert_eq!(p.start, from);
|
||||
assert!(contains(target.region, p.stamp.world(from)));
|
||||
assert!(p.stamp.world(to).y >= target.region.y + target.region.height);
|
||||
assert_eq!(
|
||||
frozen.resolve(p.stamp, Some(to), false),
|
||||
Hit::Destination(plan::Destination::OutsidePerformers, None)
|
||||
);
|
||||
let preview = f.app.allocation_drag.preview.as_ref().expect(&f.app.status);
|
||||
assert!(!f.app.allocation_drag.feedback.as_ref().unwrap().refused);
|
||||
let requested = xy(540., 190. + to.y - from.y);
|
||||
assert!(matches!(
|
||||
preview.candidate.place(
|
||||
requested,
|
||||
plan::PresentationContext {
|
||||
viewport: p.stamp.viewport(),
|
||||
zoom: p.stamp.zoom,
|
||||
target: None,
|
||||
}
|
||||
),
|
||||
Err(plan::Refusal::LayoutRequired(_))
|
||||
));
|
||||
let moved = &preview.placement.diagram.nodes[&f.node];
|
||||
assert_eq!(moved.position.x, 540.);
|
||||
assert!(moved.position.y >= bottom + 10.);
|
||||
assert!(moved.position.y > requested.y);
|
||||
assert_eq!(
|
||||
preview
|
||||
.placement
|
||||
.view
|
||||
.nodes
|
||||
.iter()
|
||||
.find(|n| n.id == owner.id)
|
||||
.unwrap(),
|
||||
owner
|
||||
);
|
||||
let remaining = preview
|
||||
.placement
|
||||
.view
|
||||
.nodes
|
||||
.iter()
|
||||
.find(|n| n.id == sibling_node)
|
||||
.unwrap();
|
||||
assert_eq!(remaining.visual_parent, Some(owner.id));
|
||||
assert_eq!(
|
||||
remaining.position,
|
||||
before.diagrams[&f.diagram].nodes[&sibling_node].position
|
||||
);
|
||||
assert_eq!(
|
||||
&preview.project().relations[&allocation],
|
||||
&before.relations[&allocation]
|
||||
);
|
||||
for (id, node) in &before.diagrams[&f.diagram].nodes {
|
||||
if *id != f.node {
|
||||
assert_eq!(&preview.placement.diagram.nodes[id], node);
|
||||
}
|
||||
}
|
||||
let mut expected = preview.project().clone();
|
||||
expected
|
||||
.diagrams
|
||||
.insert(f.diagram, preview.placement.diagram.clone());
|
||||
assert_eq!(f.app.project(), &before);
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
let _ = f.app.update(release);
|
||||
assert_eq!(f.app.project(), &expected, "{}", f.app.status);
|
||||
assert_eq!(f.app.document.revision(), 1);
|
||||
assert_eq!(
|
||||
validation::effective_performer(f.app.project(), f.work),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
validation::effective_performer(f.app.project(), sibling),
|
||||
Some(f.a)
|
||||
);
|
||||
assert_eq!(
|
||||
&f.app.project().relations[&allocation],
|
||||
&before.relations[&allocation]
|
||||
);
|
||||
let after = f.app.document.bundle().clone();
|
||||
let _ = f.app.update(Message::Undo);
|
||||
assert_eq!(f.app.project(), &before);
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
assert!(!f.app.document.can_undo());
|
||||
let _ = f.app.update(Message::Redo);
|
||||
assert_eq!(f.app.project(), &expected);
|
||||
assert_eq!(f.app.document.bundle(), &after);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refused_motion_retains_last_scene_but_release_never_commits_it() {
|
||||
let mut f = outgoing_fixture(Layer::Sa);
|
||||
let mut project = f.app.project().clone();
|
||||
let obstacle = project
|
||||
.add_element(Kind::Function, Layer::Sa, "Unrelated work", None)
|
||||
.unwrap();
|
||||
project.place(f.diagram, obstacle, xy(1250., 120.));
|
||||
replace(&mut f, project);
|
||||
let original = f.app.project().clone();
|
||||
let source = f.app.document.bundle().clone();
|
||||
let p = admit_press(&mut f);
|
||||
let clear = screen(xy(450., 225.));
|
||||
let _ = f.app.update(Message::WorkDrag(Event::Preview(
|
||||
p.stamp,
|
||||
p.token,
|
||||
Some(clear),
|
||||
)));
|
||||
let last = f.app.allocation_drag.preview.clone().expect(&f.app.status);
|
||||
// Another work card blocks this location: the prior legal outgoing scene is display
|
||||
// state only, and must not become a fallback command on release.
|
||||
let blocked = screen(xy(1260., 130.));
|
||||
let _ = f.app.update(Message::WorkDrag(Event::Preview(
|
||||
p.stamp,
|
||||
p.token,
|
||||
Some(blocked),
|
||||
)));
|
||||
assert!(f.app.allocation_drag.feedback.as_ref().unwrap().refused);
|
||||
assert!(
|
||||
f.app
|
||||
.allocation_drag
|
||||
.feedback
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.ghost_delta
|
||||
.is_some()
|
||||
);
|
||||
assert!(Arc::ptr_eq(
|
||||
f.app.allocation_drag.preview.as_ref().unwrap(),
|
||||
&last
|
||||
));
|
||||
let _ = f.app.update(Message::WorkDrag(Event::Release(
|
||||
p.stamp,
|
||||
p.token,
|
||||
Some(blocked),
|
||||
)));
|
||||
assert_eq!(f.app.project(), &original);
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
assert!(!f.app.document.can_undo());
|
||||
assert!(f.app.allocation_drag.preview.is_none());
|
||||
}
|
||||
|
||||
@@ -42,7 +42,6 @@ mod port_labels;
|
||||
pub use port_labels::PortLabels;
|
||||
#[path = "diagram/focus.rs"]
|
||||
pub(crate) mod focus;
|
||||
mod grid;
|
||||
pub(crate) mod group_gesture;
|
||||
mod multiselect;
|
||||
#[cfg(test)]
|
||||
@@ -72,7 +71,6 @@ pub struct DiagramCanvas<'a> {
|
||||
}
|
||||
#[derive(Default)]
|
||||
pub struct State {
|
||||
grid: grid::Grid,
|
||||
modifiers: iced::keyboard::Modifiers,
|
||||
group: Option<multiselect::Press>,
|
||||
work: Option<Arc<crate::app::allocation_drag::Press>>,
|
||||
@@ -1253,10 +1251,23 @@ impl canvas::Program<Message> for DiagramCanvas<'_> {
|
||||
bounds: Rectangle,
|
||||
_: mouse::Cursor,
|
||||
) -> Vec<canvas::Geometry> {
|
||||
let background = state
|
||||
.grid
|
||||
.draw(renderer, bounds.size(), self.zoom, self.pan);
|
||||
let mut f = Frame::new(renderer, bounds.size());
|
||||
f.fill_rectangle(
|
||||
Point::ORIGIN,
|
||||
bounds.size(),
|
||||
Color::from_rgb8(246, 248, 251),
|
||||
);
|
||||
let step = (24. * self.zoom).max(14.);
|
||||
let ox = self.pan.x.rem_euclid(step);
|
||||
let oy = self.pan.y.rem_euclid(step);
|
||||
for x in 0..(bounds.width / step) as i32 + 1 {
|
||||
for y in 0..(bounds.height / step) as i32 + 1 {
|
||||
f.fill(
|
||||
&Path::circle(Point::new(ox + x as f32 * step, oy + y as f32 * step), 0.65),
|
||||
style::LINE,
|
||||
);
|
||||
}
|
||||
}
|
||||
f.translate(self.pan);
|
||||
f.scale(self.zoom);
|
||||
let scene = self.scene(state);
|
||||
@@ -1659,7 +1670,7 @@ impl canvas::Program<Message> for DiagramCanvas<'_> {
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
vec![background, f.into_geometry(), overlay.into_geometry()]
|
||||
vec![f.into_geometry(), overlay.into_geometry()]
|
||||
}
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
//! Screen-space background geometry, independent of the model and pointer.
|
||||
use super::*;
|
||||
use std::cell::Cell;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct Grid {
|
||||
geometry: canvas::Cache,
|
||||
camera: Cell<Option<(f32, Vector)>>,
|
||||
#[cfg(test)]
|
||||
builds: Cell<usize>,
|
||||
}
|
||||
|
||||
impl Grid {
|
||||
pub(super) fn draw(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
size: Size,
|
||||
zoom: f32,
|
||||
pan: Vector,
|
||||
) -> canvas::Geometry {
|
||||
// Cache::draw handles exact size changes. Raw camera equality is
|
||||
// deliberately conservative: do not quantize fractional antialiasing.
|
||||
if self.camera.replace(Some((zoom, pan))) != Some((zoom, pan)) {
|
||||
self.geometry.clear();
|
||||
}
|
||||
self.geometry.draw(renderer, size, |frame| {
|
||||
#[cfg(test)]
|
||||
self.builds.set(self.builds.get() + 1);
|
||||
paint(frame, size, zoom, pan);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(frame: &mut Frame, size: Size, zoom: f32, pan: Vector) {
|
||||
frame.fill_rectangle(Point::ORIGIN, size, Color::from_rgb8(246, 248, 251));
|
||||
let step = (24. * zoom).max(14.);
|
||||
let ox = pan.x.rem_euclid(step);
|
||||
let oy = pan.y.rem_euclid(step);
|
||||
for x in 0..(size.width / step) as i32 + 1 {
|
||||
for y in 0..(size.height / step) as i32 + 1 {
|
||||
frame.fill(
|
||||
&Path::circle(Point::new(ox + x as f32 * step, oy + y as f32 * step), 0.65),
|
||||
style::LINE,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -1,378 +0,0 @@
|
||||
use super::*;
|
||||
use iced::advanced::{
|
||||
graphics::geometry::Renderer as _,
|
||||
renderer::{Headless, Renderer as _},
|
||||
};
|
||||
|
||||
async fn renderer() -> Renderer {
|
||||
<Renderer as Headless>::new(iced::Font::DEFAULT, 16.into(), None)
|
||||
.await
|
||||
.expect("grid tests require the supported WGPU headless renderer")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cache_reuses_unchanged_inputs_and_rebuilds_for_each_camera_or_size_change() {
|
||||
let renderer = renderer().await;
|
||||
let grid = Grid::default();
|
||||
let original = Size::new(320., 240.);
|
||||
let threshold = 14. / 24.;
|
||||
// Each row changes exactly one dependency from its predecessor. Returning
|
||||
// to old dimensions must invalidate too; this cache holds one viewport.
|
||||
let cases = [
|
||||
("initial", original, 1., Vector::ZERO),
|
||||
("positive x pan", original, 1., Vector::new(24., 0.)),
|
||||
("negative x pan", original, 1., Vector::new(-24., 0.)),
|
||||
("fractional x pan", original, 1., Vector::new(-24.25, 0.)),
|
||||
("positive y pan", original, 1., Vector::new(-24.25, 18.)),
|
||||
("negative y pan", original, 1., Vector::new(-24.25, -18.)),
|
||||
("fractional y pan", original, 1., Vector::new(-24.25, -18.5)),
|
||||
("return to zero pan", original, 1., Vector::ZERO),
|
||||
("zoom at zero pan", original, 1.25, Vector::ZERO),
|
||||
(
|
||||
"below spacing threshold",
|
||||
original,
|
||||
threshold - 0.001,
|
||||
Vector::ZERO,
|
||||
),
|
||||
("at spacing threshold", original, threshold, Vector::ZERO),
|
||||
(
|
||||
"above spacing threshold",
|
||||
original,
|
||||
threshold + 0.001,
|
||||
Vector::ZERO,
|
||||
),
|
||||
(
|
||||
"width only",
|
||||
Size::new(321., 240.),
|
||||
threshold + 0.001,
|
||||
Vector::ZERO,
|
||||
),
|
||||
("return width", original, threshold + 0.001, Vector::ZERO),
|
||||
(
|
||||
"height only",
|
||||
Size::new(320., 241.),
|
||||
threshold + 0.001,
|
||||
Vector::ZERO,
|
||||
),
|
||||
("return height", original, threshold + 0.001, Vector::ZERO),
|
||||
(
|
||||
"fractional width",
|
||||
Size::new(320.25, 240.),
|
||||
threshold + 0.001,
|
||||
Vector::ZERO,
|
||||
),
|
||||
(
|
||||
"fractional height",
|
||||
Size::new(320.25, 240.5),
|
||||
threshold + 0.001,
|
||||
Vector::ZERO,
|
||||
),
|
||||
];
|
||||
for (index, (label, size, zoom, pan)) in cases.into_iter().enumerate() {
|
||||
let _ = grid.draw(&renderer, size, zoom, pan);
|
||||
assert_eq!(grid.builds.get(), index + 1, "{label} must rebuild");
|
||||
for _ in 0..3 {
|
||||
let _ = grid.draw(&renderer, size, zoom, pan);
|
||||
assert_eq!(grid.builds.get(), index + 1, "{label} must reuse");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Independent reference copied from the former DiagramCanvas::draw background
|
||||
// pass. Do not call grid::paint here: a shared implementation would hide changes
|
||||
// in spacing, phase, radius, color, or coverage from the parity comparison.
|
||||
fn legacy_background(frame: &mut Frame, size: Size, zoom: f32, pan: Vector) {
|
||||
frame.fill_rectangle(Point::ORIGIN, size, Color::from_rgb8(246, 248, 251));
|
||||
let spacing = (24. * zoom).max(14.);
|
||||
let offset_x = pan.x.rem_euclid(spacing);
|
||||
let offset_y = pan.y.rem_euclid(spacing);
|
||||
for column in 0..(size.width / spacing) as i32 + 1 {
|
||||
for row in 0..(size.height / spacing) as i32 + 1 {
|
||||
frame.fill(
|
||||
&Path::circle(
|
||||
Point::new(
|
||||
offset_x + column as f32 * spacing,
|
||||
offset_y + row as f32 * spacing,
|
||||
),
|
||||
0.65,
|
||||
),
|
||||
Color::from_rgb(0.86, 0.89, 0.91),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn foreground(frame: &mut Frame, zoom: f32, pan: Vector, changed: bool) {
|
||||
frame.translate(pan);
|
||||
frame.scale(zoom);
|
||||
// Crossing paths pass behind opaque cards. The long paths also cross the
|
||||
// screen-space warning, making geometry order observable.
|
||||
for (start, end, color) in [
|
||||
(Point::new(0., 20.), Point::new(300., 210.), style::TEAL),
|
||||
(Point::new(0., 210.), Point::new(300., 20.), style::BOUNDARY),
|
||||
] {
|
||||
frame.stroke(
|
||||
&Path::line(start, end),
|
||||
Stroke::default().with_color(color).with_width(3.),
|
||||
);
|
||||
}
|
||||
let shift = if changed { 19.5 } else { 0. };
|
||||
frame.fill_rectangle(
|
||||
Point::new(12., 92.),
|
||||
Size::new(270., 35.),
|
||||
Color::from_rgba(0.1, 0.6, 0.5, 0.25),
|
||||
);
|
||||
for (position, size, title) in [
|
||||
(Point::new(32. + shift, 58.), Size::new(116., 72.), "Sensor"),
|
||||
(Point::new(172., 139.), Size::new(118., 66.), "Controller"),
|
||||
] {
|
||||
frame.fill_rectangle(position, size, Color::WHITE);
|
||||
frame.stroke(
|
||||
&Path::rectangle(position, size),
|
||||
Stroke::default().with_color(style::BOUNDARY).with_width(1.),
|
||||
);
|
||||
frame.fill_text(Text {
|
||||
content: title.into(),
|
||||
position: position + Vector::new(8., 12.),
|
||||
color: style::INK,
|
||||
size: 13.into(),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
// Selection paint and handles remain live foreground data, including when
|
||||
// the underlying background cache is reused after an edit.
|
||||
frame.stroke(
|
||||
&Path::rectangle(Point::new(29. + shift, 55.), Size::new(122., 78.)),
|
||||
Stroke::default().with_color(style::TEAL).with_width(2.),
|
||||
);
|
||||
frame.fill_rectangle(Point::new(26. + shift, 52.), Size::new(6., 6.), style::TEAL);
|
||||
frame.fill_text(Text {
|
||||
content: if changed {
|
||||
"Edited connection"
|
||||
} else {
|
||||
"Connection"
|
||||
}
|
||||
.into(),
|
||||
position: Point::new(116., 113.),
|
||||
color: style::INK,
|
||||
size: 11.into(),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
fn warning(frame: &mut Frame, size: Size) {
|
||||
frame.fill_rectangle(
|
||||
Point::new(8., 8.),
|
||||
Size::new((size.width - 16.).max(0.), 30.),
|
||||
Color::WHITE,
|
||||
);
|
||||
frame.fill_text(Text {
|
||||
content: "1 connection route needs attention".into(),
|
||||
position: Point::new(16., 15.),
|
||||
color: style::MUTED,
|
||||
size: 12.into(),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
fn legacy_geometry(
|
||||
renderer: &Renderer,
|
||||
size: Size,
|
||||
zoom: f32,
|
||||
pan: Vector,
|
||||
changed: bool,
|
||||
) -> Vec<canvas::Geometry> {
|
||||
let mut combined = Frame::new(renderer, size);
|
||||
legacy_background(&mut combined, size, zoom, pan);
|
||||
foreground(&mut combined, zoom, pan, changed);
|
||||
let mut overlay = Frame::new(renderer, size);
|
||||
warning(&mut overlay, size);
|
||||
vec![combined.into_geometry(), overlay.into_geometry()]
|
||||
}
|
||||
|
||||
fn cached_geometry(
|
||||
grid: &Grid,
|
||||
renderer: &Renderer,
|
||||
size: Size,
|
||||
zoom: f32,
|
||||
pan: Vector,
|
||||
changed: bool,
|
||||
) -> Vec<canvas::Geometry> {
|
||||
let background = grid.draw(renderer, size, zoom, pan);
|
||||
let mut content = Frame::new(renderer, size);
|
||||
foreground(&mut content, zoom, pan, changed);
|
||||
let mut overlay = Frame::new(renderer, size);
|
||||
warning(&mut overlay, size);
|
||||
vec![background, content.into_geometry(), overlay.into_geometry()]
|
||||
}
|
||||
|
||||
fn pixels(renderer: &mut Renderer, size: Size, geometry: Vec<canvas::Geometry>) -> Vec<u8> {
|
||||
// Keep physical screenshot dimensions and renderer viewport consistent even
|
||||
// for fractional canvas bounds; the geometry itself keeps its exact size.
|
||||
let physical = Size::new(size.width.ceil() as u32, size.height.ceil() as u32);
|
||||
renderer.reset(Rectangle::with_size(Size::new(
|
||||
physical.width as f32,
|
||||
physical.height as f32,
|
||||
)));
|
||||
for layer in geometry {
|
||||
renderer.draw_geometry(layer);
|
||||
}
|
||||
let pixels = Headless::screenshot(renderer, physical, 1., style::BG);
|
||||
assert_eq!(
|
||||
pixels.len(),
|
||||
physical.width as usize * physical.height as usize * 4
|
||||
);
|
||||
pixels
|
||||
}
|
||||
|
||||
fn assert_same_pixels(expected: &[u8], actual: &[u8], label: &str) {
|
||||
assert_eq!(expected.len(), actual.len(), "{label}: dimensions");
|
||||
let differing = expected
|
||||
.as_chunks::<4>()
|
||||
.0
|
||||
.iter()
|
||||
.zip(actual.as_chunks::<4>().0)
|
||||
.filter(|(a, b)| a != b)
|
||||
.count();
|
||||
assert_eq!(differing, 0, "{label}: changed RGBA pixels");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cached_background_preserves_pixels_and_live_foreground_across_camera_and_size_changes() {
|
||||
let mut renderer = renderer().await;
|
||||
let grid = Grid::default();
|
||||
let original = Size::new(320., 240.);
|
||||
let threshold = 14. / 24.;
|
||||
for (label, size, zoom, pan) in [
|
||||
("origin", original, 1., Vector::ZERO),
|
||||
("positive pan", original, 1., Vector::new(17., 11.)),
|
||||
("negative pan", original, 1., Vector::new(-17., -11.)),
|
||||
("fractional pan", original, 1., Vector::new(-17.25, 11.5)),
|
||||
("zoom zero pan", original, 1.25, Vector::ZERO),
|
||||
("below threshold", original, threshold - 0.001, Vector::ZERO),
|
||||
("at threshold", original, threshold, Vector::ZERO),
|
||||
("above threshold", original, threshold + 0.001, Vector::ZERO),
|
||||
("width resize", Size::new(327., 240.), 1., Vector::ZERO),
|
||||
("height resize", Size::new(327., 247.), 1., Vector::ZERO),
|
||||
("return size", original, 1., Vector::ZERO),
|
||||
(
|
||||
"fractional bounds",
|
||||
Size::new(319.25, 239.5),
|
||||
1.,
|
||||
Vector::new(-0.5, 0.25),
|
||||
),
|
||||
] {
|
||||
let old = legacy_geometry(&renderer, size, zoom, pan, false);
|
||||
let expected = pixels(&mut renderer, size, old);
|
||||
let layers = cached_geometry(&grid, &renderer, size, zoom, pan, false);
|
||||
let actual = pixels(&mut renderer, size, layers);
|
||||
assert_same_pixels(&expected, &actual, label);
|
||||
let builds = grid.builds.get();
|
||||
let reused = cached_geometry(&grid, &renderer, size, zoom, pan, false);
|
||||
let reused = pixels(&mut renderer, size, reused);
|
||||
assert_same_pixels(&expected, &reused, label);
|
||||
assert_eq!(grid.builds.get(), builds, "{label}: screenshot reuse");
|
||||
|
||||
let edited_old = legacy_geometry(&renderer, size, zoom, pan, true);
|
||||
let edited_expected = pixels(&mut renderer, size, edited_old);
|
||||
assert_ne!(
|
||||
expected, edited_expected,
|
||||
"{label}: edit must visibly change foreground"
|
||||
);
|
||||
let edited = cached_geometry(&grid, &renderer, size, zoom, pan, true);
|
||||
let edited = pixels(&mut renderer, size, edited);
|
||||
assert_same_pixels(&edited_expected, &edited, label);
|
||||
assert_eq!(
|
||||
grid.builds.get(),
|
||||
builds,
|
||||
"{label}: foreground edit must reuse grid"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn actual_canvas_keeps_background_cached_while_selection_and_revision_change() {
|
||||
use super::super::{Context, DiagramCanvas, State};
|
||||
let mut renderer = renderer().await;
|
||||
let mut project = Project::empty();
|
||||
let a = project
|
||||
.add_element(Kind::Entity, Layer::Oa, "Sensor", None)
|
||||
.unwrap();
|
||||
let b = project
|
||||
.add_element(Kind::Actor, Layer::Oa, "Controller", None)
|
||||
.unwrap();
|
||||
let diagram = project.ensure_diagram(Layer::Oa);
|
||||
project.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Overview;
|
||||
let node = project.place(diagram, a, Position { x: 20., y: 50. });
|
||||
project.place(diagram, b, Position { x: 100., y: 160. });
|
||||
project.relate(RelationKind::CommunicationMean, "Link", a, b);
|
||||
let mut canvas = DiagramCanvas {
|
||||
selection: None,
|
||||
allocation: None,
|
||||
project: &project,
|
||||
view_index: None,
|
||||
diagram,
|
||||
selected: a,
|
||||
navigation_focus: None,
|
||||
zoom: 0.75,
|
||||
pan: Vector::ZERO,
|
||||
context: Context {
|
||||
document: project.id,
|
||||
diagram,
|
||||
revision: 0,
|
||||
epoch: 0,
|
||||
},
|
||||
selected_bend: None,
|
||||
selected_attachment: None,
|
||||
editable: true,
|
||||
placing: false,
|
||||
connection: None,
|
||||
};
|
||||
let state = State::default();
|
||||
let size = Size::new(360., 280.);
|
||||
let bounds = Rectangle::with_size(size);
|
||||
let draw = |canvas: &DiagramCanvas<'_>, renderer: &Renderer, bounds| {
|
||||
canvas::Program::draw(
|
||||
canvas,
|
||||
&state,
|
||||
renderer,
|
||||
&crate::style::theme(),
|
||||
bounds,
|
||||
mouse::Cursor::Unavailable,
|
||||
)
|
||||
};
|
||||
let layers = draw(&canvas, &renderer, bounds);
|
||||
assert_eq!(layers.len(), 3, "background, scene, warning overlay");
|
||||
let original = pixels(&mut renderer, size, layers);
|
||||
assert_eq!(state.grid.builds.get(), 1);
|
||||
canvas.selected = b;
|
||||
let layers = draw(&canvas, &renderer, bounds);
|
||||
let selected = pixels(&mut renderer, size, layers);
|
||||
assert_ne!(original, selected, "selection must remain live");
|
||||
assert_eq!(state.grid.builds.get(), 1);
|
||||
let mut changed = project.clone();
|
||||
changed
|
||||
.diagrams
|
||||
.get_mut(&diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&node)
|
||||
.unwrap()
|
||||
.position
|
||||
.x += 30.;
|
||||
canvas.project = &changed;
|
||||
canvas.context.revision += 1;
|
||||
let layers = draw(&canvas, &renderer, bounds);
|
||||
let moved = pixels(&mut renderer, size, layers);
|
||||
assert_ne!(selected, moved, "new model revision must remain live");
|
||||
assert_eq!(state.grid.builds.get(), 1);
|
||||
canvas.pan.x = -0.25;
|
||||
let _ = draw(&canvas, &renderer, bounds);
|
||||
assert_eq!(state.grid.builds.get(), 2);
|
||||
let _ = draw(
|
||||
&canvas,
|
||||
&renderer,
|
||||
Rectangle::with_size(Size::new(361., 280.)),
|
||||
);
|
||||
assert_eq!(state.grid.builds.get(), 3);
|
||||
}
|
||||
@@ -243,21 +243,6 @@ impl canvas::Program<Message> for WorkFeedback<'_> {
|
||||
let Some(frozen) = &feedback.frozen else {
|
||||
return vec![];
|
||||
};
|
||||
if let Some(delta) = feedback.ghost_delta {
|
||||
for region in &frozen.moving {
|
||||
let top = Point::new(
|
||||
(region.x + delta.x) * self.0.zoom + self.0.pan.x,
|
||||
(region.y + delta.y) * self.0.zoom + self.0.pan.y,
|
||||
);
|
||||
frame.stroke(
|
||||
&Path::rectangle(
|
||||
top,
|
||||
iced::Size::new(region.width * self.0.zoom, region.height * self.0.zoom),
|
||||
),
|
||||
Stroke::default().with_color(color).with_width(2.),
|
||||
);
|
||||
}
|
||||
}
|
||||
for index in &feedback.targets {
|
||||
let target = &frozen.targets[*index];
|
||||
let region = target.region;
|
||||
|
||||
@@ -310,7 +310,7 @@ fn article(topic: Topic) -> Article {
|
||||
"Use the diagram selector or + View to work with a focused diagram. View properties edits its name, perspective, projection and Saved view visibility. Apply changes commits the view settings.",
|
||||
"Choose Containment for semantic children, Architecture · allocated work for responsibilities, Functional dataflow for work and exchanges, or Missions & capabilities for outcomes. All relationships explores one layer; OA ↔ SA traceability can show placed elements from both layers and their correspondence lines alongside ordinary relationships.",
|
||||
"Select an element and edit Semantic parent to change ownership. Use the Allocation palette tool to connect a performer to existing unallocated leaf work in Containment. The relationship can appear as a line there; Architecture · allocated work shows responsibility through nesting instead. Saved visibility filters can hide a line without removing the relationship.",
|
||||
"Scroll to zoom and use Fit to frame the current view. In allocated-work views, dragging work changes its assignment and placement together. Choose Arrange next drag or press Alt+A to move work without changing responsibility for that one drag; Escape cancels the armed action. Auto-layout arranges the whole view. Export SVG produces a shareable view.",
|
||||
"Scroll to zoom and use Fit to frame the current view. In allocated-work views, dragging work changes its assignment and placement together. Choose Arrange next drag or press Alt+A to move work without changing responsibility for that one drag; Escape cancels the armed action. Auto-layout arranges the view; see Route connections around the model for its bend and label reset rules. Export SVG produces a shareable view.",
|
||||
"Select a diagram node to access Placement, Fold / unfold and Hide. With the canvas focused, Delete hides its placement; Shift+Delete asks to remove the model element. Other views retain a hidden element. Undo restores either change.",
|
||||
],
|
||||
),
|
||||
@@ -357,7 +357,9 @@ fn article(topic: Topic) -> Article {
|
||||
&[
|
||||
"Select an architecture connection and drag a segment to add a bend. Right-click a line for Add bend here; right-click a circle for Remove bend. With the canvas focused, Insert adds a bend and Delete removes the selected bend. Drag the label independently. Escape cancels a gesture; Undo reverses one completed drag. Shift+F10 opens the selected diagram item's context menu; use arrow keys and Enter to choose an action.",
|
||||
"Automatic orthogonal routing avoids cards and container headers where space permits. Amber connections need attention: move an obstructing element or a bend. In Select / move, drag a port to any border of its owner. Manual positions are per diagram; right-click Reset automatic position restores connection-aware placement. Ports remain attached to their actual owners. Manual routes are saved separately for each diagram and appear in source, API and SVG export.",
|
||||
"Select a connection and drag a square endpoint handle along its element boundary to move its attachment in this view without changing what it connects. If handles overlap, right-click the selected connection or use its Actions menu and choose Move source attachment or Move target attachment, then drag the handle. Connections to semantic ports retain their port attachment; move the port instead. Reset source attachment or Reset target attachment restores that end's automatic placement while preserving the other end, pins and label. Reset routing clears all these route overrides in this view. Auto-layout preserves pins. Moving one endpoint keeps pins fixed; moving a container carrying both endpoints translates their pins. When folding promotes an endpoint, expand it again to resume and edit its saved route.",
|
||||
"Select a connection and drag a square endpoint handle along its element boundary to move its attachment in this view without changing what it connects. If handles overlap, right-click the selected connection or use its Actions menu and choose Move source attachment or Move target attachment, then drag the handle. Connections to semantic ports retain their port attachment; move the port instead. Reset source attachment or Reset target attachment restores that end's automatic placement while preserving the other end, pins and label. Reset routing clears all these route overrides, including both attachments, in this view.",
|
||||
"Auto-layout (Arrange elements) replaces saved bends and label positions on visible connections with their actual endpoints shown; attachment preferences and port anchors stay saved. Hidden or folded routes keep their overrides, though pins can move with their endpoint owners during arrangement. Undo restores the prior arrangement.",
|
||||
"During ordinary movement, moving one endpoint keeps pins fixed; moving a container carrying both endpoints translates their pins. When folding promotes an endpoint, expand it again to resume and edit its saved route.",
|
||||
],
|
||||
),
|
||||
explain(
|
||||
|
||||
@@ -1692,3 +1692,161 @@ fn adding_bend_between_close_ports_stays_outside_owner_clearance() {
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
fn arrange_reset_fixture() -> (Project, Id, Id) {
|
||||
let mut project = Project::empty();
|
||||
let source = project.add(Kind::Activity, "Send report", 20., 70.);
|
||||
let target = project.add(Kind::Activity, "Receive report", 650., 200.);
|
||||
let edge = project.relate(RelationKind::Interaction, "Report", source, target);
|
||||
let diagram = project.ensure_diagram(Layer::Oa);
|
||||
project
|
||||
.diagrams
|
||||
.get_mut(&diagram)
|
||||
.unwrap()
|
||||
.routes
|
||||
.push(EdgeRoute {
|
||||
edge,
|
||||
source_attachment: Some(EdgeAttachment {
|
||||
side: PortAnchorSide::Bottom,
|
||||
offset: 0.25,
|
||||
}),
|
||||
target_attachment: Some(EdgeAttachment {
|
||||
side: PortAnchorSide::Top,
|
||||
offset: 0.75,
|
||||
}),
|
||||
waypoints: vec![Position { x: 400., y: 500. }],
|
||||
label: Some(EdgeLabel {
|
||||
fraction: 0.25,
|
||||
offset: Position { x: 11., y: 7. },
|
||||
}),
|
||||
});
|
||||
(project, diagram, source)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arrange_reset_document_preserves_multifile_comments_and_exact_undo_redo_reopen() {
|
||||
use crate::document::Document;
|
||||
use oa_core::source_project::{self, ProjectLimits};
|
||||
let (project, diagram, _) = arrange_reset_fixture();
|
||||
let mut bundle = source_project::format_bundle(&project);
|
||||
assert!(
|
||||
bundle.files.len() > 1,
|
||||
"This must exercise a real include closure"
|
||||
);
|
||||
for (path, text) in &mut bundle.files {
|
||||
*text = format!("// Keep exact authored module: {path}\r\n{text}");
|
||||
}
|
||||
let mut document = Document::from_bundle(bundle, ProjectLimits::default()).unwrap();
|
||||
document.mark_saved(document.snapshot());
|
||||
let before = document.snapshot();
|
||||
let original_route = before.decoded.project.diagrams[&diagram].routes[0].clone();
|
||||
document.execute(Command::Layout(diagram)).unwrap();
|
||||
let after = document.snapshot();
|
||||
let routes = &after.decoded.project.diagrams[&diagram].routes;
|
||||
let mut expected_route = original_route;
|
||||
expected_route.waypoints.clear();
|
||||
expected_route.label = None;
|
||||
assert_eq!(routes, &vec![expected_route]);
|
||||
let mut outside = after.decoded.project.clone();
|
||||
outside
|
||||
.diagrams
|
||||
.insert(diagram, before.decoded.project.diagrams[&diagram].clone());
|
||||
assert_eq!(outside, before.decoded.project);
|
||||
let affected: std::collections::BTreeSet<_> = std::iter::once(diagram)
|
||||
.chain(
|
||||
before.decoded.project.diagrams[&diagram]
|
||||
.nodes
|
||||
.keys()
|
||||
.copied(),
|
||||
)
|
||||
.map(|id| document.compiled().index.declarations[&id].path.clone())
|
||||
.collect();
|
||||
for (path, text) in &before.bundle.files {
|
||||
assert!(
|
||||
after.bundle.files[path].contains(&format!("// Keep exact authored module: {path}"))
|
||||
);
|
||||
if !affected.contains(path) {
|
||||
assert_eq!(
|
||||
&after.bundle.files[path], text,
|
||||
"Unrelated accepted bytes: {path}"
|
||||
);
|
||||
}
|
||||
}
|
||||
assert!(document.undo());
|
||||
assert_eq!(document.bundle(), &before.bundle);
|
||||
assert_eq!(document.project(), &before.decoded.project);
|
||||
assert!(!document.dirty());
|
||||
assert!(document.redo());
|
||||
assert_eq!(document.bundle(), &after.bundle);
|
||||
assert_eq!(document.project(), &after.decoded.project);
|
||||
let reopened = Document::from_bundle(after.bundle.clone(), ProjectLimits::default()).unwrap();
|
||||
assert_eq!(reopened.bundle(), &after.bundle);
|
||||
assert_eq!(reopened.project(), &after.decoded.project);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arrange_reset_pending_valid_property_has_separate_apply_and_layout_history() {
|
||||
let (project, diagram, source) = arrange_reset_fixture();
|
||||
let mut control = App::with_project(project.clone());
|
||||
send(&mut control, Message::Select(source));
|
||||
send(
|
||||
&mut control,
|
||||
Message::Field("/name".into(), "Revised sender".into()),
|
||||
);
|
||||
send(&mut control, Message::Apply);
|
||||
let applied_project = control.project().clone();
|
||||
let applied_bundle = control.document.bundle().clone();
|
||||
assert_eq!(applied_project.elements[&source].name, "Revised sender");
|
||||
assert_eq!(
|
||||
applied_project.diagrams[&diagram].routes,
|
||||
project.diagrams[&diagram].routes
|
||||
);
|
||||
let mut app = App::with_project(project);
|
||||
let before_project = app.project().clone();
|
||||
let before_bundle = app.document.bundle().clone();
|
||||
send(&mut app, Message::Select(source));
|
||||
send(
|
||||
&mut app,
|
||||
Message::Field("/name".into(), "Revised sender".into()),
|
||||
);
|
||||
send(&mut app, Message::Command(Command::Layout(diagram)));
|
||||
assert_eq!(app.project().elements[&source].name, "Revised sender");
|
||||
assert!(
|
||||
app.project().diagrams[&diagram].routes[0]
|
||||
.waypoints
|
||||
.is_empty()
|
||||
);
|
||||
assert!(app.project().diagrams[&diagram].routes[0].label.is_none());
|
||||
let arranged_project = app.project().clone();
|
||||
let arranged_bundle = app.document.bundle().clone();
|
||||
send(&mut app, Message::Undo);
|
||||
assert_eq!(app.project(), &applied_project);
|
||||
assert_eq!(app.document.bundle(), &applied_bundle);
|
||||
send(&mut app, Message::Undo);
|
||||
assert_eq!(app.project(), &before_project);
|
||||
assert_eq!(app.document.bundle(), &before_bundle);
|
||||
send(&mut app, Message::Redo);
|
||||
assert_eq!(app.project(), &applied_project);
|
||||
assert_eq!(app.document.bundle(), &applied_bundle);
|
||||
send(&mut app, Message::Redo);
|
||||
assert_eq!(app.project(), &arranged_project);
|
||||
assert_eq!(app.document.bundle(), &arranged_bundle);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arrange_reset_invalid_property_blocks_layout_without_losing_draft_or_accepted_bytes() {
|
||||
let (project, diagram, _) = arrange_reset_fixture();
|
||||
let node = *project.diagrams[&diagram].nodes.keys().next().unwrap();
|
||||
let mut app = App::with_project(project);
|
||||
send(&mut app, Message::Select(node));
|
||||
let before_project = app.project().clone();
|
||||
let before_bundle = app.document.bundle().clone();
|
||||
let revision = app.document.revision();
|
||||
send(&mut app, Message::Field("/position/x".into(), "-".into()));
|
||||
send(&mut app, Message::Command(Command::Layout(diagram)));
|
||||
assert_eq!(app.project(), &before_project);
|
||||
assert_eq!(app.document.bundle(), &before_bundle);
|
||||
assert_eq!(app.document.revision(), revision);
|
||||
assert!(!app.document.can_undo());
|
||||
assert_eq!(app.draft.as_ref().unwrap().inputs["/position/x"].raw, "-");
|
||||
}
|
||||
|
||||
+1
-13
@@ -2702,19 +2702,7 @@ performer change with diagram placement. The preview MUST identify the
|
||||
destination and preserve readable work and destination identity. Destination
|
||||
identity is shown outside the canvas, independently of explanatory or refusal
|
||||
text. The outlined target's hit region remains fixed throughout the gesture;
|
||||
feedback MUST NOT obscure the work. Preserve pointer-relative placement when
|
||||
the requested position is valid. When an outgoing drop would leave work
|
||||
overlapping its former performer, the editor MAY propose a minimal grid-aligned
|
||||
translation through a crossed performer boundary, with one grid unit of
|
||||
clearance. It MUST preview that exact position, translate the complete affected
|
||||
work group rigidly, and recheck all semantic and collision constraints on release.
|
||||
It MUST NOT move unrelated placements or search arbitrary free space. If no
|
||||
proposal is valid, a pointer-following outline MAY accompany the last valid
|
||||
preview; that retained preview MUST NOT authorize a refused release.
|
||||
Outgoing work headers MAY be partly outside the viewport provided each retains
|
||||
at least a 24-by-24 logical-pixel grab region. Releasing the pointer outside the
|
||||
viewport still cancels. Other assignment and Arrange gestures retain their
|
||||
complete-header visibility requirement.
|
||||
feedback MUST NOT obscure the work or change its pointer-relative placement.
|
||||
A clean click selects without applying unrelated drafts. A refused work press
|
||||
with an unapplied property draft MUST preserve its text, caret and focused
|
||||
editor. An accepted drag is one atomic model/source/history transaction:
|
||||
|
||||
+50
-13
@@ -1255,6 +1255,55 @@ translates pins when both actual endpoint owners move by the same delta; label
|
||||
offsets remain relative. Raw ``EditNode`` and source position edits are exact
|
||||
record edits and do not implicitly translate pins.
|
||||
|
||||
Explicit ``Layout(diagram)`` (Auto-layout / Arrange elements) is an exception to
|
||||
route-override preservation. Immediately before this operation changes geometry,
|
||||
it MUST determine eligibility from the staged Project's ordinary filtered
|
||||
projection. An edge is eligible only if it is visible, both actual endpoint-owner
|
||||
placements exist, both projected owner nodes exist, and its projected source and
|
||||
target equal those actual owners in stored orientation. A semantic Port belongs
|
||||
to its actual owning placement; hiding its marker does not make the edge
|
||||
ineligible. An endpoint promoted to a folded ancestor does. Missing owners MUST
|
||||
NOT be inferred. In a Batch, this input includes preceding staged operations;
|
||||
determining eligibility MUST NOT introduce separate validation of a batch prefix.
|
||||
|
||||
For each existing eligible route entry, Layout MUST clear ``waypoints`` and the
|
||||
entire ``label`` override (both fraction and offset). It MUST retain the entry,
|
||||
its list position, edge identity, and both attachment values, including dormant
|
||||
attachments at semantic ports. Empty and attachments-only entries MUST NOT be
|
||||
removed. Layout MUST NOT create route entries merely to record generated elbows
|
||||
or automatic label placement. Node port-anchor records remain exact, although
|
||||
moving or resizing their owners can change their physical positions.
|
||||
|
||||
Ineligible routes, including hidden, filtered and promoted edges, MUST NOT be
|
||||
reset. Their labels, attachments, identities and list order remain exact. Their
|
||||
waypoints follow the existing movement rule: translate only when both actual
|
||||
endpoint-owner placements receive the same nonzero original-to-final delta,
|
||||
subject also to the existing State boundary movement rule. For a State Entry/Exit
|
||||
endpoint whose owning State boundary has a placement, that boundary placement
|
||||
must move with the same delta before the stored pins translate. Otherwise retain
|
||||
the waypoint values.
|
||||
Promotion to a visible ancestor alone does not authorize translation. Eligibility
|
||||
limits route reset, not placement scope: ordinary Layout may still reposition or
|
||||
resize hidden and folded descendant placements. Other diagrams remain unchanged.
|
||||
|
||||
Placement changes and eligible resets form one atomic Layout mutation. A changed
|
||||
standalone Layout creates one history entry; Layout within Batch or execute_many
|
||||
shares the enclosing transaction's entry, even when that transaction contains
|
||||
several Layout operations. Validate the complete candidate through the ordinary
|
||||
transaction path. Rejection leaves the accepted Project and history unchanged;
|
||||
an identical enclosing transaction creates no entry and preserves Redo. Undo
|
||||
restores the pre-transaction placement and route records. A separately applied
|
||||
desktop property draft retains its own history entry; Layout does not merge it.
|
||||
|
||||
This exception does not change ordinary dragging, group movement, reconnect,
|
||||
folding, filtering, raw ``EditNode`` or source position edits. Reset routing
|
||||
remains the distinct whole-entry removal operation, including both attachments.
|
||||
Internal State transitions have no external edge eligible for reset. Process
|
||||
generated topology is unchanged; saved Scenario layout retains its dedicated
|
||||
operation and rejects this Layout command. Automatic elbows remain derived;
|
||||
resetting constraints does not guarantee an intersection-free or collision-free
|
||||
arrangement.
|
||||
|
||||
This is an additive schema-3 field. Earlier schema-3 JSON/source that omits
|
||||
``routes`` loads as an empty list. Frozen schema-1 and schema-2 readers do not
|
||||
accept this new field; migration creates empty route lists. Source projects keep
|
||||
@@ -2128,19 +2177,7 @@ performer change with diagram placement. The preview MUST identify the
|
||||
destination and preserve readable work and destination identity. Destination
|
||||
identity is shown outside the canvas, independently of explanatory or refusal
|
||||
text. The outlined target's hit region remains fixed throughout the gesture;
|
||||
feedback MUST NOT obscure the work. Preserve pointer-relative placement when
|
||||
the requested position is valid. When an outgoing drop would leave work
|
||||
overlapping its former performer, the editor MAY propose a minimal grid-aligned
|
||||
translation through a crossed performer boundary, with one grid unit of
|
||||
clearance. It MUST preview that exact position, translate the complete affected
|
||||
work group rigidly, and recheck all semantic and collision constraints on release.
|
||||
It MUST NOT move unrelated placements or search arbitrary free space. If no
|
||||
proposal is valid, a pointer-following outline MAY accompany the last valid
|
||||
preview; that retained preview MUST NOT authorize a refused release.
|
||||
Outgoing work headers MAY be partly outside the viewport provided each retains
|
||||
at least a 24-by-24 logical-pixel grab region. Releasing the pointer outside the
|
||||
viewport still cancels. Other assignment and Arrange gestures retain their
|
||||
complete-header visibility requirement.
|
||||
feedback MUST NOT obscure the work or change its pointer-relative placement.
|
||||
A clean click selects without applying unrelated drafts. A refused work press
|
||||
with an unapplied property draft MUST preserve its text, caret and focused
|
||||
editor. An accepted drag is one atomic model/source/history transaction:
|
||||
|
||||
@@ -498,3 +498,26 @@ editing remains separate from semantic reconnect. The [decision and evidence
|
||||
map](edge-attachments-20260922.md) records alternatives, lifecycle rules,
|
||||
independent falsifiers, evaluators and the Process/Scenario applicability limits.
|
||||
This is branch-specific work; it does not establish an integrated release.
|
||||
|
||||
## Explicit Arrange resets visible authored geometry — 2026-09-25
|
||||
|
||||
Branch-specific candidate for issue #32, based on released715036e; not an integrated
|
||||
release checkpoint. Explicit Layout clears pins and the complete label override
|
||||
for existing projected routes whose displayed owners equal their actual placed
|
||||
owners. Freeze that eligibility before geometry changes, including at the operation's
|
||||
position in a Batch. Preserve route entries, order, attachments and semantic/manual
|
||||
port choices. Preserve existing equal-actual-owner translation for ineligible
|
||||
routes and retain ordinary movement behavior. Keep whole Reset routing distinct.
|
||||
|
||||
This narrowly amends the former pin-preserving Layout contract so an explicit
|
||||
arrangement can recover automatic route/label placement without deleting endpoint
|
||||
choices. Clearing the whole label record avoids retaining a stale authored fraction;
|
||||
it does not guarantee better geometry. The routing and node-placement algorithms
|
||||
remain unchanged. More aggressive experimental routing candidates were not adopted
|
||||
because independent scenes exposed tradeoffs and regressions.
|
||||
|
||||
[Results and evidence](experiments/arrange-reset-20260925/results.md) bind the
|
||||
contract, source/history tests, strict Clippy and expanded/folded native save/reopen
|
||||
acceptance to their actual code pins. Residual crossing/layout quality and viewport/
|
||||
guide discoverability remain outside this bounded acceptance. Gitea owns live
|
||||
publication and integration status; this record does not close issue #32.
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
# Outgoing allocation dragging
|
||||
|
||||
Scope: [issue 40](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/40),
|
||||
based on published Windows source `715036e132677dd2bc2fb37ed316d93dc9987aec`.
|
||||
This contribution is not integrated release acceptance. User requirements remain
|
||||
in `PROJECT_GOAL.md`; this note records a revision to derived interaction policy.
|
||||
|
||||
## Reproduction and cause
|
||||
|
||||
The released executable SHA256
|
||||
`2c03bd6cc3f1110c219e4f57ab94874a121590f83779239a93289b7170664399`
|
||||
opened a copied Allocation/Process companion fixture on Windows. At 1440x900,
|
||||
72% Fit, the OA assign view's X-receive was dragged from window (486,546) to
|
||||
(365,546), just beyond A's left edge. It remained assigned at its original
|
||||
position, reporting enlarged card overlap. A control drag to (625,572) removed
|
||||
assignment; Undo restored the original scene. These are actual native end-state
|
||||
observations; intermediate jitter was diagnosed from source, not frame capture.
|
||||
|
||||
Frozen targets resolve from the pointer, while placement retains its grab
|
||||
offset. The pointer therefore leaves its performer before the full work card
|
||||
does. Former parent/child intersection becomes unrelated overlap after removing
|
||||
allocation. Refused motion previously cleared the preview and redrew the saved
|
||||
scene. Additionally, whole-header viewport containment rejects a useful outgoing
|
||||
position in the fitted reproduction even when its model geometry is valid.
|
||||
|
||||
## Selected policy
|
||||
|
||||
Try exact snapped pointer placement first. Only an outgoing assignment candidate
|
||||
with a placement refusal may propose bounded source clearance: move the entire
|
||||
captured work footprint through a crossed frozen source boundary, clearing the
|
||||
post-removal source enclosure by one 10-world-unit grid cell. Consider at most
|
||||
four directions, ordered by press-to-pointer boundary-crossing fraction and a
|
||||
fixed direction tie-break. Existing placement, collision, semantic, exact-command
|
||||
and source/history admission remain authoritative. No neighbor is moved, no
|
||||
general packing/search occurs, and no semantic refusal is overridden.
|
||||
|
||||
The accepted/candidate semantic indexes are reused during fallback projections.
|
||||
Outgoing headers may be partly clipped but each must retain a 24x24 logical-pixel
|
||||
grab region. Other assignment and Arrange visibility rules remain unchanged.
|
||||
The exact corrected candidate is previewed; release recomputes it. Blocked
|
||||
in-bounds motion retains its last admitted scene and adds an orange pointer
|
||||
outline. This display state confers no permission to commit on refused release.
|
||||
Cancel, outside release and gesture invalidation retire it normally.
|
||||
|
||||
Rejected alternatives: retaining a moving outline alone leaves the boundary
|
||||
dead band; allowing unrelated overlap makes unassigned work look allocated;
|
||||
general auto-layout changes unrelated placement and makes drag intent harder to
|
||||
predict. The previous unconditional pointer-offset rule is narrowed in
|
||||
`docs/language.rst` and its HTML copy. Real neighboring-card collisions and
|
||||
insufficient visible grab area can still refuse a drop.
|
||||
|
||||
## Verification status
|
||||
|
||||
Independent source diagnosis and repair critique found no semantic/stale/cancel
|
||||
bypass. Review exposed the remaining whole-header camera restriction and an
|
||||
obsolete retained-preview assertion; both were revised. Added checks cover
|
||||
OA/SA boundary clearance, corners, source shrinkage, rigid descendants/groups,
|
||||
real App frame-by-frame input, exact source/history, cancellation, blocked release
|
||||
and outgoing grab-area boundaries. At clean source
|
||||
`1a669130fb6d2b65279aab3847e966a1a282c191`, the Windows allocation-drag
|
||||
suite passes 121 tests (none failed or ignored; 1,424 filtered out), strict
|
||||
desktop all-target Clippy passes, and the normal desktop development build
|
||||
passes. [Raw results and reproduction commands](experiments/drag-out-20260925/README.md)
|
||||
preserve the source, platform, failed attempts and exact scope. These checks do
|
||||
not prove native usability. Computer Use was stopped with the physical Escape
|
||||
key before the changed executable could be inspected. Fitted-view dragging,
|
||||
clipped-card identification/regrabbing, native Undo/Redo and Save/reopen remain
|
||||
unverified. The issue remains open; no integration or release acceptance follows
|
||||
from the targeted gates.
|
||||
|
||||
Test-only successor `24b1eab07d306d8cb091bb46f7daf9f027a8f1c0` additionally
|
||||
checks an actual fallback blocked by unrelated work, including preservation of
|
||||
an existing redo branch, and an outgoing move while an allocated sibling keeps
|
||||
the source enclosure expanded. Both run for OA and SA. All 123 allocation-drag
|
||||
tests and strict desktop Clippy pass at that successor. Fresh independent review
|
||||
confirms those tests exercise their claimed invariants, with no material source
|
||||
finding in this scope. The same linked evidence includes the new logs and review;
|
||||
production code and native-acceptance limits remain unchanged.
|
||||
|
||||
The first build attempt failed before tests because the sparse checkout omitted
|
||||
`build/windows_manifest.rs`; restoring the tracked build directory corrected the
|
||||
checkout. The local drive initially had about 22 MB free. Git sparse checkout and
|
||||
Cargo's package-scoped cleanup of inactive desktop artifacts recovered capacity.
|
||||
Automatic review rejected recursive deletion of the extracted preview; it was
|
||||
left intact. No published package or unique evidence was deleted.
|
||||
The second attempt also exposed omitted tracked harness fixtures and a `Self`
|
||||
typo in a standalone test helper; both were corrected before the passing run.
|
||||
That failed compiler process was terminated after its errors were captured.
|
||||
@@ -1 +0,0 @@
|
||||
*.log -text
|
||||
@@ -1,91 +0,0 @@
|
||||
# Small architecture diagram CPU investigation
|
||||
|
||||
Question: does core projection/routing explain [issue 44's low FPS](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/44)
|
||||
with roughly eight architecture cards? This experiment narrows that question;
|
||||
it neither reproduces native frame rate nor establishes a complete cause.
|
||||
|
||||
## Workload and source
|
||||
|
||||
The screenshot uses names from `examples/release1`. Its exact model, projection,
|
||||
camera and authored geometry are unavailable. The probe loads the unchanged
|
||||
`examples/release1/model.json`, measures its five saved views, and constructs a
|
||||
clearly labeled eight-card SA Overview approximation using those semantic
|
||||
elements. Approximate coordinates and stable placement IDs are in the probe;
|
||||
it has 8 projected nodes, 6 ports and 13 projected edges. It is not a recovered
|
||||
copy of the user's diagram. All data changes occur in memory.
|
||||
|
||||
Source: `24b1eab07d306d8cb091bb46f7daf9f027a8f1c0`; core, manifest and lockfile
|
||||
are unchanged from the drag contribution's `1a669130` production source.
|
||||
Windows 10.0.26200, Rust 1.98.1, x86_64-pc-windows-gnullvm, LLVM-MinGW
|
||||
20260908 UCRT, reported CPU Intel Core i7-10870H with 4 exposed cores/threads.
|
||||
The probe was optimized with `-O` and linked to the existing **unoptimized
|
||||
development-profile core library** used by the completed desktop test build.
|
||||
This distinction matters: optimizing the driver does not optimize that library.
|
||||
The desktop test process had completed before measurement; no Cargo compilation
|
||||
was started until after measurement. Other operating-system workloads were not
|
||||
controlled, so outliers are retained rather than interpreted as stable capacity.
|
||||
|
||||
## Method and results
|
||||
|
||||
Each phase gets 3 warmups and 40 timed samples, with returned values passed to
|
||||
`black_box`. Reported p50/p95 select sorted index `ceil((n-1)*p)` as implemented
|
||||
in the source. Returned output destruction is outside each timed phase. Diagram
|
||||
movement uses a deterministic small delta sequence and the existing descendant
|
||||
and authored-route translation APIs. Index creation, projection, routing, and
|
||||
combined movement preparation are measured separately. The probe does not model
|
||||
allocation transaction admission or an Iced frame.
|
||||
|
||||
For the eight-card approximation, `performance-01.log` records:
|
||||
|
||||
| Phase | p50 ms | p95 ms | max ms |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| Reused-index projection | 0.3885 | 0.7550 | 1.2965 |
|
||||
| Fresh-index projection | 1.7135 | 2.7022 | 4.2204 |
|
||||
| Settled routing | 2.4525 | 4.4617 | 4.7735 |
|
||||
| Moved routing | 3.1907 | 4.4781 | 4.5672 |
|
||||
| Movement preparation + projection + routing | 4.2640 | 6.8259 | 8.8052 |
|
||||
|
||||
This workload does not expose severe core latency sufficient by itself to
|
||||
explain very low FPS. It does not rule out different authored routes, allocation
|
||||
admission, hardware/rendering, or the user's exact scene. Repeated fresh-index
|
||||
projection has a measurable cost, but no change is justified solely by this
|
||||
small sample. No FPS or performance-fix claim follows.
|
||||
|
||||
## Source observations and next discriminating measurement
|
||||
|
||||
Independent read-only hot-path review found that `DiagramCanvas::scene` already
|
||||
caches projection and routing by semantic context and transient geometry.
|
||||
Ordinary node movement changes preview geometry and routes the full view;
|
||||
route gestures use `route_edge_preview`. Ordinary hover need not reroute.
|
||||
|
||||
`diagram/port_labels.rs` requests redraw on every mouse event, including when
|
||||
no tooltip is visible. `DiagramCanvas::draw` rebuilds geometry and each dot-grid
|
||||
circle on every draw. At a 1900x660 canvas this yields about 2,240 circles at
|
||||
zoom 1 and 6,528 at minimum spacing. Scene caching does not cache that geometry.
|
||||
`views.rs` also performs an unindexed `project_view` during app view rebuilding;
|
||||
legacy node-motion redraw does not itself require that full UI rebuild.
|
||||
|
||||
Next, measure actual canvas draw/frame costs independently for idle hover, pan,
|
||||
ordinary node drag and allocation drag, with grid generation separated from
|
||||
node/edge/text rendering and frame presentation. Compare the same scene/camera
|
||||
and renderer before changing production code. Native automation was stopped
|
||||
with Escape; no GPU/frame measurement was attempted in this experiment. Live
|
||||
ownership and subsequent results belong to issue 44.
|
||||
|
||||
## Reproduction
|
||||
|
||||
Build `oa-core` at the identified source with the stated Windows toolchain, then
|
||||
compile this standalone probe against that exact library and matching dependency
|
||||
directory. For a fresh standard Cargo development build:
|
||||
|
||||
```powershell
|
||||
cargo build --locked -p oa-core
|
||||
rustc --edition=2024 -O harness/experiments/architecture-perf-20260925/architecture_perf_probe.rs --extern oa_core=target/debug/liboa_core.rlib -L dependency=target/debug/deps -C linker=clang.exe -o architecture_perf_probe.exe
|
||||
./architecture_perf_probe.exe examples/release1/model.json 40
|
||||
```
|
||||
|
||||
The actual run linked `target/debug/deps/liboa_core-2f3c5174cb6398fd.rlib`
|
||||
and used the same dependency directory. Set the linker/runtime PATH as required
|
||||
by the Windows toolchain. Compiler flags, core profile, machine load and fixture
|
||||
all affect timings; preserve them when comparing candidates. This standalone
|
||||
probe is experimental evidence, not a production dependency or a timing test.
|
||||
@@ -1,166 +0,0 @@
|
||||
//! Local-only CPU diagnosis. Does not measure native FPS, canvas redraw, or GPU.
|
||||
//! Usage: architecture_perf_probe.exe PATH_TO_RELEASE1_MODEL_JSON [SAMPLES=40]
|
||||
use oa_core::{persistence, projection, routing, view_options::ViewIndex, *};
|
||||
use std::{collections::BTreeMap, hint::black_box, time::Instant};
|
||||
|
||||
fn measure<T>(label: &str, samples: usize, mut f: impl FnMut(usize) -> T) {
|
||||
for i in 0..3 {
|
||||
black_box(f(i));
|
||||
}
|
||||
let mut ms = Vec::with_capacity(samples);
|
||||
for i in 0..samples {
|
||||
let start = Instant::now();
|
||||
let output = black_box(f(i));
|
||||
ms.push(start.elapsed().as_secs_f64() * 1000.0);
|
||||
drop(output); // Exclude destruction consistently from each timed phase.
|
||||
}
|
||||
ms.sort_by(f64::total_cmp);
|
||||
let percentile = |p: f64| ms[((samples - 1) as f64 * p).ceil() as usize];
|
||||
println!(
|
||||
"{label}: n={samples} p50_ms={:.4} p95_ms={:.4} max_ms={:.4}",
|
||||
percentile(0.5),
|
||||
percentile(0.95),
|
||||
ms[samples - 1]
|
||||
);
|
||||
}
|
||||
|
||||
fn probe(p: &Project, d: &Diagram, samples: usize) {
|
||||
let index = ViewIndex::new(p);
|
||||
let view = projection::project_diagram_with_index(p, d, &index);
|
||||
println!(
|
||||
"\nview={} projection={:?} nodes={} ports={} edges={}",
|
||||
d.name,
|
||||
d.projection,
|
||||
view.nodes.len(),
|
||||
view.ports.len(),
|
||||
view.edges.len()
|
||||
);
|
||||
measure("index_new", samples, |_| ViewIndex::new(black_box(p)));
|
||||
measure("projection_reused_index", samples, |_| {
|
||||
projection::project_diagram_with_index(black_box(p), black_box(d), black_box(&index))
|
||||
});
|
||||
measure("projection_fresh_index", samples, |_| {
|
||||
projection::project_diagram(black_box(p), black_box(d))
|
||||
});
|
||||
measure("routing_settled", samples, |_| {
|
||||
routing::route_diagram(black_box(p), black_box(d), black_box(&view))
|
||||
});
|
||||
// Deterministic slight motion of the first root card. Includes the same
|
||||
// descendant/route translation as legacy single-card canvas drag. This is
|
||||
// presentation-only, not the allocation transaction pipeline.
|
||||
let Some(root) = view.nodes.iter().find(|n| n.visual_parent.is_none()) else {
|
||||
return;
|
||||
};
|
||||
println!("moving_root={} element={}", root.id, p.named(root.element));
|
||||
let mut moved = projection::descendant_nodes(p, d.id, root.id);
|
||||
moved.push(root.id);
|
||||
let make_moved = |i: usize| {
|
||||
let delta = Position {
|
||||
x: (i % 21) as f32 * 3.0,
|
||||
y: (i % 11) as f32 * 2.0,
|
||||
};
|
||||
let mut preview = d.clone();
|
||||
let deltas = moved
|
||||
.iter()
|
||||
.map(|id| (*id, delta))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
for route in projection::translated_routes(p, d, &deltas) {
|
||||
if let Some(stored) = preview.routes.iter_mut().find(|r| r.edge == route.edge) {
|
||||
*stored = route;
|
||||
} else {
|
||||
preview.routes.push(route);
|
||||
}
|
||||
}
|
||||
for id in &moved {
|
||||
if let Some(n) = preview.nodes.get_mut(id) {
|
||||
n.position.x += delta.x;
|
||||
n.position.y += delta.y;
|
||||
}
|
||||
}
|
||||
preview
|
||||
};
|
||||
let previews = (0..samples.max(3)).map(make_moved).collect::<Vec<_>>();
|
||||
let projected = previews
|
||||
.iter()
|
||||
.map(|d| projection::project_diagram_with_index(p, d, &index))
|
||||
.collect::<Vec<_>>();
|
||||
measure("moved_projection", samples, |i| {
|
||||
projection::project_diagram_with_index(p, black_box(&previews[i]), &index)
|
||||
});
|
||||
measure("moved_routing", samples, |i| {
|
||||
routing::route_diagram(p, black_box(&previews[i]), black_box(&projected[i]))
|
||||
});
|
||||
measure("moved_prepare_projection_routing", samples, |i| {
|
||||
let preview = make_moved(i);
|
||||
let view = projection::project_diagram_with_index(p, &preview, &index);
|
||||
routing::route_diagram(p, &preview, &view)
|
||||
});
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args = std::env::args().collect::<Vec<_>>();
|
||||
let path = args
|
||||
.get(1)
|
||||
.ok_or("usage: architecture_perf_probe PATH [SAMPLES]")?;
|
||||
let samples = args
|
||||
.get(2)
|
||||
.map(|s| s.parse::<usize>())
|
||||
.transpose()?
|
||||
.unwrap_or(40)
|
||||
.max(1);
|
||||
let p = persistence::decode(&std::fs::read(path)?)?;
|
||||
println!(
|
||||
"CPU-only probe; samples={samples}; fixture={path}; elements={} relations={}",
|
||||
p.elements.len(),
|
||||
p.relations.len()
|
||||
);
|
||||
for d in p.diagrams.values() {
|
||||
probe(&p, d, samples);
|
||||
}
|
||||
// Approximation of the reported eight-card screenshot, not an exact repro:
|
||||
// fixture semantics, Overview projection, synthetic positions/card sizes.
|
||||
let mut approx = p.clone();
|
||||
let id: Id = "f0440000-0000-4000-8000-000000000001".parse()?;
|
||||
approx.diagrams.insert(
|
||||
id,
|
||||
Diagram {
|
||||
id,
|
||||
alias: "fps44_approx_overview".into(),
|
||||
name: "APPROXIMATION eight-card SA Overview".into(),
|
||||
description: "Synthetic positions, not user geometry".into(),
|
||||
layer: Layer::Sa,
|
||||
projection: ProjectionKind::Overview,
|
||||
view_options: Default::default(),
|
||||
nodes: BTreeMap::new(),
|
||||
routes: vec![],
|
||||
layout: None,
|
||||
},
|
||||
);
|
||||
for (ordinal, (alias, x, y)) in [
|
||||
("sa_assessment_procedure", 30., 240.),
|
||||
("sa_triage", 510., 240.),
|
||||
("sa_control", 990., 240.),
|
||||
("sa_technician", 530., 440.),
|
||||
("sa_assess", 1490., 240.),
|
||||
("sa_repair", 1030., 440.),
|
||||
("sa_confirm", 1530., 440.),
|
||||
("sa_reset", 30., 440.),
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
let element = approx
|
||||
.elements
|
||||
.values()
|
||||
.find(|e| e.alias == alias)
|
||||
.ok_or_else(|| format!("missing alias {alias}"))?
|
||||
.id;
|
||||
let generated = approx.place(id, element, Position { x, y });
|
||||
let diagram = approx.diagrams.get_mut(&id).unwrap();
|
||||
let mut node = diagram.nodes.remove(&generated).unwrap();
|
||||
node.id = format!("f0440000-0000-4000-8000-{:012}", ordinal + 2).parse()?;
|
||||
diagram.nodes.insert(node.id, node);
|
||||
}
|
||||
probe(&approx, &approx.diagrams[&id], samples);
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"source": "24b1eab07d306d8cb091bb46f7daf9f027a8f1c0",
|
||||
"core_profile": "dev unoptimized",
|
||||
"driver_optimization": "rustc -O",
|
||||
"samples_per_phase": 40,
|
||||
"warmups_per_phase": 3,
|
||||
"rlib_sha256": "E10C81BF28A33CD0FB6C4C26884CFFA480EF05328E78BCDB138DD4B635B2531C",
|
||||
"fixture_sha256": "EF0EF4006C3E09D5DB6E1B621256B3F7D6642F3A7B09CC34607CFBCCA830C727",
|
||||
"driver_exe_sha256": "4C66E9CDE4F08D6DFD2A0DC3DBF1A80B97E340F3F22CA001A76301F06F47C2E2",
|
||||
"native_fps_measured": false
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
CPU-only probe; samples=40; fixture=.local/drag-out-01a0d6fd/examples/release1/model.json; elements=94 relations=62
|
||||
|
||||
view=1 · OA responsibility and repair traffic projection=Allocation nodes=6 ports=0 edges=3
|
||||
index_new: n=40 p50_ms=1.0106 p95_ms=2.0583 max_ms=2.0910
|
||||
projection_reused_index: n=40 p50_ms=0.5702 p95_ms=1.2320 max_ms=1.3490
|
||||
projection_fresh_index: n=40 p50_ms=1.5505 p95_ms=3.6681 max_ms=5.1533
|
||||
routing_settled: n=40 p50_ms=0.1944 p95_ms=0.3391 max_ms=0.4246
|
||||
moving_root=3191e05c-1c7f-5f0e-beb0-5f3bded54c48 element=Control room
|
||||
moved_projection: n=40 p50_ms=0.4848 p95_ms=1.2745 max_ms=1.5797
|
||||
moved_routing: n=40 p50_ms=0.1994 p95_ms=0.7971 max_ms=1.1414
|
||||
moved_prepare_projection_routing: n=40 p50_ms=0.9250 p95_ms=2.6847 max_ms=10.4908
|
||||
|
||||
view=5 · SA pressure supervision projection=StateMachine { machine: 132b3f28-3f88-5327-9868-8c8b645ed412 } nodes=5 ports=0 edges=3
|
||||
index_new: n=40 p50_ms=0.9602 p95_ms=2.1394 max_ms=2.4512
|
||||
projection_reused_index: n=40 p50_ms=0.1697 p95_ms=0.7629 max_ms=1.1780
|
||||
projection_fresh_index: n=40 p50_ms=1.1189 p95_ms=2.6324 max_ms=3.3912
|
||||
routing_settled: n=40 p50_ms=0.7063 p95_ms=1.0812 max_ms=1.1149
|
||||
moving_root=a676fbb9-0833-59a8-98cf-a10d8f4b7434 element=Pressure supervision
|
||||
moved_projection: n=40 p50_ms=0.1565 p95_ms=0.3445 max_ms=0.3790
|
||||
moved_routing: n=40 p50_ms=0.6113 p95_ms=1.3199 max_ms=1.3733
|
||||
moved_prepare_projection_routing: n=40 p50_ms=0.8259 p95_ms=1.7640 max_ms=2.1752
|
||||
|
||||
view=2 · SA service boundary and functions projection=Allocation nodes=6 ports=6 edges=3
|
||||
index_new: n=40 p50_ms=0.7820 p95_ms=3.4180 max_ms=6.8439
|
||||
projection_reused_index: n=40 p50_ms=0.7938 p95_ms=1.9946 max_ms=2.1411
|
||||
projection_fresh_index: n=40 p50_ms=2.0597 p95_ms=3.4554 max_ms=3.6616
|
||||
routing_settled: n=40 p50_ms=0.1478 p95_ms=0.7886 max_ms=1.0081
|
||||
moving_root=8345c76a-8d85-5c31-902e-ec1cd33ed4a3 element=Pressure coordination service
|
||||
moved_projection: n=40 p50_ms=0.6914 p95_ms=1.5286 max_ms=1.8658
|
||||
moved_routing: n=40 p50_ms=0.1862 p95_ms=0.5658 max_ms=2.8924
|
||||
moved_prepare_projection_routing: n=40 p50_ms=1.1724 p95_ms=3.1806 max_ms=4.2620
|
||||
|
||||
view=4 · SA mission and included capability projection=Capabilities nodes=4 ports=1 edges=3
|
||||
index_new: n=40 p50_ms=0.6879 p95_ms=1.3582 max_ms=1.5266
|
||||
projection_reused_index: n=40 p50_ms=0.1562 p95_ms=0.6767 max_ms=1.0102
|
||||
projection_fresh_index: n=40 p50_ms=1.2929 p95_ms=2.3543 max_ms=4.0762
|
||||
routing_settled: n=40 p50_ms=0.2553 p95_ms=1.4389 max_ms=1.6677
|
||||
moving_root=069a3fb3-077c-59d8-91da-27b0665143c7 element=Assess an alarm
|
||||
moved_projection: n=40 p50_ms=0.1243 p95_ms=0.1928 max_ms=0.2212
|
||||
moved_routing: n=40 p50_ms=0.2271 p95_ms=0.6733 max_ms=1.2408
|
||||
moved_prepare_projection_routing: n=40 p50_ms=0.5542 p95_ms=1.3344 max_ms=1.8957
|
||||
|
||||
view=3 · SA service conversation only projection=Allocation nodes=6 ports=6 edges=1
|
||||
index_new: n=40 p50_ms=0.9618 p95_ms=1.6890 max_ms=2.0995
|
||||
projection_reused_index: n=40 p50_ms=0.6661 p95_ms=2.3388 max_ms=2.6429
|
||||
projection_fresh_index: n=40 p50_ms=2.3913 p95_ms=4.1941 max_ms=4.8120
|
||||
routing_settled: n=40 p50_ms=0.0660 p95_ms=0.2156 max_ms=0.6289
|
||||
moving_root=40210589-69f7-5209-b329-10bb83cf615c element=Field technician
|
||||
moved_projection: n=40 p50_ms=0.5527 p95_ms=1.1239 max_ms=1.1362
|
||||
moved_routing: n=40 p50_ms=0.0495 p95_ms=0.1771 max_ms=0.2209
|
||||
moved_prepare_projection_routing: n=40 p50_ms=0.7963 p95_ms=4.1835 max_ms=6.7551
|
||||
|
||||
view=APPROXIMATION eight-card SA Overview projection=Overview nodes=8 ports=6 edges=13
|
||||
index_new: n=40 p50_ms=0.7487 p95_ms=1.9597 max_ms=2.2005
|
||||
projection_reused_index: n=40 p50_ms=0.3885 p95_ms=0.7550 max_ms=1.2965
|
||||
projection_fresh_index: n=40 p50_ms=1.7135 p95_ms=2.7022 max_ms=4.2204
|
||||
routing_settled: n=40 p50_ms=2.4525 p95_ms=4.4617 max_ms=4.7735
|
||||
moving_root=f0440000-0000-4000-8000-000000000002 element=Assess an alarm procedure
|
||||
moved_projection: n=40 p50_ms=0.4691 p95_ms=0.8354 max_ms=1.0408
|
||||
moved_routing: n=40 p50_ms=3.1907 p95_ms=4.4781 max_ms=4.5672
|
||||
moved_prepare_projection_routing: n=40 p50_ms=4.2640 p95_ms=6.8259 max_ms=8.8052
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
||||
# Supplemental export checkpoint: source review
|
||||
|
||||
Prepared `.local/arrange-reset-export-checkpoint-20260925.py`, SHA256 `5bcd8a568d5318dd62217c1e0b35538b22e34403c9727eee8814a5d0ec5a899f`. AST parsing passed; script/oracle not executed. This is an author source check, not independent runtime acceptance.
|
||||
|
||||
Example after the parent observes JSON export to a new destination:
|
||||
|
||||
```text
|
||||
python .local/arrange-reset-export-checkpoint-20260925.py --root TRIAL --actual TRIAL/working/expanded-after.json --name expanded-after-export --case expanded --expected after
|
||||
```
|
||||
|
||||
Use fresh names/files for Undo (`--expected original`) and Redo (`--expected after`). The script requires an existing JSON directly inside the admitted working directory and refuses the two original imported filenames. It does not create an export or establish when/how that file was created; the parent's GUI observation supplies that evidence.
|
||||
|
||||
It loads only the exact reviewed staged common helper, then reuses admitted stage/source/package/baseline checks. The supplemental script is explicitly **not stage-admitted**: its own exact file identity appears in the checkpoint receipt and oracle invocation bindings. It snapshots the export without rewriting it, invokes the preserved same-candidate `--compare` oracle, and checks the export, entire working inventory, supplemental script and admitted inputs afterward. Fresh checkpoint directories and receipts preserve attempts; failure quarantines the trial through the existing sentinel convention. No staged tool, manifest, original trial file or source checkout is changed.
|
||||
|
||||
An accepted receipt proves complete typed Project equality for the captured JSON. It does not establish native canonical-project Save, clean-document state, UI action cause, source-bundle byte equality or fresh-process reopening. JSON Save As is export-only here; parent must separately observe canonical Save and reopening. The earlier refused overwrite remains a recorded UI observation, not a successful save.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Arrange reset native outcome independent review
|
||||
|
||||
**Accept the bounded expanded/folded Arrange-reset workflow demonstrated here. No material blocker found in the reviewed outcome.** This is Windows native wiring, preservation, history, source-save and reopen evidence at `d6f45a13a7b8ab8dc9b84ee432a832609f5af6ed`, not full issue32 layout-quality acceptance or J/L router adoption.
|
||||
|
||||
Reviewed all18 observation JSON notes and independently viewed all18 corresponding full-window PNGs with view_image. Read all eight checkpoint receipts/oracle outputs and both final close receipts. No GUI action, compiler, model helper or checkpoint rerun by reviewer.
|
||||
|
||||
## Model and lifecycle evidence
|
||||
|
||||
Each case has after/Undo/Redo/reopened exports: eight checkpoints accepted, failure null, immutable inputs unchanged, oracle exit0 and complete_project_equal=true. Independently read the captured exported JSON and expected baseline files: all eight pairs are byte-identical, stronger than the reported typed-model equality for these particular artifacts. Verified snapshot/expected/supplemental-script/comparison-receipt hashes. Expected after-Projects remain candidate-derived UI-wiring oracles; independent core tests establish the reset contract separately.
|
||||
|
||||
Additional independent model inspection confirms expanded Verified report resets its two pins `(610,210),(610,420)` and full label override; folded Verified report retains both pins and label fraction0.5/offset(0,-32). Both cases retain route entry order, attachments, semantic port anchor records, semantic elements/relations and the entire Review sentinel diagram. Filtered edge's pin `(1020,270)` remains exact in this fixture; this is not a promise that all filtered routes remain at fixed world coordinates. Original working JSON imports remain byte-identical to immutable originals.
|
||||
|
||||
Run01 PID7812 and run02 PID4328 both have final close receipts: exit0, failure null, observed exit, immutable inputs unchanged and empty stdout/stderr. Parent observations establish normal AltF4 causes; receipts establish actual exits. Run02 close is now complete, not merely window disappearance. Both saved source projects were reopened in fresh process run02 after run01 closure, then exported and compared against the same frozen after-models.
|
||||
|
||||
## Native observations
|
||||
|
||||
Expanded before/Undo images show two authored circular handles and manual label; arranged/Redo images show the selected route without those handles while automatic elbows remain. Folded images preserve collapsed presentation and visible promoted exchanges. Source-save images report all source files saved; reopen images report source project opened and retain expanded/folded state. Both sentinel-view images show retained route/label geometry, including the selected pin in expanded sentinel. Whole-model comparison verifies hidden fields that screenshots alone cannot.
|
||||
|
||||
The arranged view required Fit and displays71% zoom; Undo retained viewport zoom, so folded Archive partly leaves the viewport until refit. These are recorded viewing limitations, not model-history failures. Guide card13 is readable and correctly distinguishes explicit Layout's visible actual-endpoint pin/label reset from full Reset routing, retained port/attachment choices, dormant hidden/folded overrides and Undo. Search filters topics but required six scrolls to the relevant card; retain this existing discoverability limitation.
|
||||
|
||||
## Protocol deviation and adapter incidents
|
||||
|
||||
The original plan assumed overwriteable JSON checkpoints. Observation03 visibly reports that the JSON destination already exists and asks for a new file. The original remained intact. The actual workflow instead used fresh JSON exports for model checkpoints, then saved expanded and folded multi-file source projects in separate locations and reopened those sources. Export did not mark source saved or consume model history; source Save supplied the persistence/reopen evidence. This is a justified workflow adjustment, not a falsely reported successful overwrite.
|
||||
|
||||
The supplemental export checker is explicitly marked not stage-admitted in its receipts, with its own hash and narrow equality scope; original staged tools remained the existing set. Preserve that provenance rather than describing it as original staging. A rejected click targeted a16×16 icon screenshot; full-window screenshot1 corrected targeting. Short filename typing/set_value failures and successful absolute-path typing are adapter interaction observations, not demonstrated application defects. None authorizes editing expected model bytes; no such repair is evidenced.
|
||||
|
||||
## Binding and limits
|
||||
|
||||
Native manifest SHA256 `eb208a3a1fa0843be4087e1797d109dc0e3dcddb3255f3ad9f536f3d98292b65`.
|
||||
Recorded GUI SHA256 `133375d57bdecf3f3effe5ecc13ec121e1257b90838f2cdb64fb91aa9a530d1b`; oracle `0792aff4cf8460c58fa2bf2f5ddbfa478523b7c9fe9b45e19ec93090191c37c1`.
|
||||
Supplemental checker `5bcd8a568d5318dd62217c1e0b35538b22e34403c9727eee8814a5d0ec5a899f`.
|
||||
Run01 close SHA256 `ffe8a3cab307ecc4ad45caf027787efdb6e41449fb304fc2cadd242a7fb5daf8`; run02 `6abce58644e8bf2c85e133aa479f44d66eeeb0a6d59361afb9db1d996b4ed745`.
|
||||
|
||||
No claim of general crossing/label-aware arrangement quality, arbitrary State-boundary native behavior, Process/Scenario changes, performance, Linux, full workspace/regression-suite completion, or remote publication. This trial saves newly generated multi-file sources; original authored multi-file comment/draft preservation is separately covered by desktop tests, not inferred from this UI sequence. Remote disk-full blockage still separates local acceptance from delivery.
|
||||
@@ -0,0 +1,61 @@
|
||||
# Explicit Arrange route reset — branch evidence
|
||||
|
||||
**Bounded reset candidate verified on Windows; local-only pending publication.** Branch `codex/arrange-reset-ytr83r-20260925` is based on released `715036e132677dd2bc2fb37ed316d93dc9987aec`. Final native/build/Clippy source is `d6f45a13a7b8ab8dc9b84ee432a832609f5af6ed`. This result covers the reset capability within issue #32, not a replacement node-layout algorithm or completion of intersection/label-aware automatic arrangement. No routing algorithm change is included.
|
||||
|
||||
Publication remains blocked by the [recorded remote disk-space failure](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/32#issuecomment-1330). This branch/evidence is local-only; no successful push, PR, merge or release is claimed. Existing published prereleases are unaffected.
|
||||
|
||||
## Behavior
|
||||
|
||||
Explicit `Command::Layout` freezes reset eligibility from the staged pre-operation filtered projection. It clears existing pins and the complete label override only for projected edges whose displayed endpoint owners equal both actual placed owners and whose projected owner nodes exist. It retains route entries/order, edge identities, both endpoint attachments, semantic ports and manual port anchors. Ordinary placement movement and the routing engine are unchanged.
|
||||
|
||||
Filtered, promoted, missing-owner and outside-projection routes are not reset. Retained pins follow the existing actual-owner equal-delta translation rule, including the State boundary exception; a retained record is not a promise of fixed world coordinates. Projection-excluded owners receive no movement when outside the existing Layout placement scope. Other views remain exact. Whole **Reset routing** remains a distinct operation that removes all overrides. Documentation now describes this deliberate behavior amendment instead of promising that Auto-layout always preserves pins.
|
||||
|
||||
Standalone changes form one history entry, including a route-only reset when nodes are already arranged. Nested Layout participates in its enclosing Batch's final validation and history. No-op Layout preserves Redo. A valid pending property draft remains a separate Apply before Layout; an invalid draft blocks Layout without losing its input.
|
||||
|
||||
## Recorded checks
|
||||
|
||||
All successful receipts below record exit code 0, unchanged pinned source, and no runner failure. Environment: local Windows `x86_64-pc-windows-gnullvm`, frozen offline toolchain, one Cargo build job.
|
||||
|
||||
| Source | Check | Actual result |
|
||||
| --- | --- | --- |
|
||||
| `6c44a2d6a16d8c80320d35a9c8d2eee3b77e8047` | Core library + `release_routing`, `state_diagrams`, `routing_geometry`, `routing_labels`, `route_source`, `group_movement` | **195 passed**, 0 failed/ignored across seven harnesses |
|
||||
| `6c44a2d6a16d8c80320d35a9c8d2eee3b77e8047` | `cargo clippy --locked --offline -p oa-core -p oa-desktop --all-targets -- -D warnings` | Passed |
|
||||
| `9eb4642e50564165aa0db38d8e91f4c14b818bdc` | `cargo test --locked --offline -p oa-desktop arrange_reset_` | **3 passed**, 1535 filtered out; not a full desktop-suite run |
|
||||
| `d6f45a13a7b8ab8dc9b84ee432a832609f5af6ed` | `cargo build --locked --offline -p oa-core --example arrange_reset_native_probe` | Passed |
|
||||
| `d6f45a13a7b8ab8dc9b84ee432a832609f5af6ed` | `cargo build --locked --offline -p oa-desktop --bin arcadia-studio` | Passed |
|
||||
| `d6f45a13a7b8ab8dc9b84ee432a832609f5af6ed` | Strict core/desktop all-target Clippy command above | Passed |
|
||||
| `d6f45a13a7b8ab8dc9b84ee432a832609f5af6ed` | Native expanded/folded Arrange, Undo, Redo, source-save and fresh-process reopen | Eight complete-Project comparisons passed; two normal closes, exit 0, empty stdout/stderr |
|
||||
|
||||
The three desktop tests cover exact multi-file comment/source preservation through Undo/Redo/reopen, an invalid property draft blocking Layout, and separate valid-property Apply/Layout history. Core cases include explicit reset-only change history, preserved attachments/anchors and empty entries, filtered equal/different-owner deltas, folded actual-owner versus displayed-owner movement, missing owners, excluded projections, State boundary retention, temporary invalid Batch prefix repaired before final validation, and no-op Redo.
|
||||
|
||||
The difference from the core-test source `6c44a2d6` to `d6f45a13` is the added native oracle example and desktop history tests. The production reset implementation, production routing and node arrangement did not change in that interval. Each gate is reported at its actual source pin; the 195-test run was not repeated against `d6f45a13`.
|
||||
|
||||
Receipts, raw logs and before/after source inventories for all eight gates, including both failures, are preserved in the accompanying evidence archive under `gates/`. Commands run from the identified commit's repository root. Environment was Rust 1.98.1, Windows gnullvm, LLVM-MinGW 20260908 UCRT, dev builds and one Cargo job. `--offline` requires dependencies already cached. The core command is:
|
||||
|
||||
```text
|
||||
cargo test --locked --offline -p oa-core --lib --test release_routing --test state_diagrams --test routing_geometry --test routing_labels --test route_source --test group_movement
|
||||
```
|
||||
|
||||
Preserved failures: `01-core-contracts` at `f0d64b531addcea584083e35296d1e5a4cb1fa8d` failed compilation with E0507 because a test moved `Option<EdgeLabel>` through a shared reference; the assertion now clones it. `02-core-contracts-fixed` at `3dfbe1959d8b5717bf8b8c1a76f31afc5ca7072a` compiled, then stopped with 19/20 routing tests passing: the new fixture incorrectly assumed Decomposition excludes FunctionalExchanges. It now uses Capabilities, includes one genuinely visible capability and retains inactive Function placements, with full independently cloned expected Project equality. No production behavior was weakened to accommodate that fixture. The corrected complete selected run is the 195-pass result above.
|
||||
|
||||
## Native acceptance
|
||||
|
||||
The native fixture supplies expanded/folded originals, two authored visible pins and a label override, fixed semantic anchors, dormant attachments, a filtered-route sentinel and an independent second view. Its expected after-Projects are produced by the candidate `Command::Layout`: they verify UI wiring/save/history against the candidate, not independently prove geometric quality. Independent contract tests and source review supply separate checks. Generate a fresh fixture directory with `arrange_reset_native_probe NEW_DIRECTORY`; compare saved JSON using `arrange_reset_native_probe --compare ACTUAL EXPECTED`. Build commands are above. Run the desktop executable with its matching LLVM-MinGW `libunwind.dll`, open the generated original JSON, and use fresh destinations for every JSON export.
|
||||
|
||||
Executed for expanded and folded cases: Open → Arrange → export/compare expected → Undo/export/compare original → Redo/export/compare expected → save source project. After normal process closure, a fresh process reopened each saved source project, inspected its main and sentinel views, and exported another exact comparison. All eight typed-Project comparisons passed; an independent reviewer also confirmed byte equality for these particular JSON artifact pairs. Original imports remained intact. Saved source projects are multi-file bundles, not byte-identical to the generator's single canonical source file; their fresh-process model comparisons establish semantic persistence. Desktop tests separately cover authored multi-file comments and exact source history.
|
||||
|
||||
Protocol correction: the initial plan assumed JSON Save overwrites the opened file. The app instead imports JSON without adopting its path; JSON Save As exports to a new destination and refuses an existing destination. The first overwrite attempt was visibly refused and preserved. Subsequent checkpoints used new JSON exports, then actual source Save for clean closure and reopen. The original staged checkpoint helper was not used. A reviewed supplemental checker, explicitly recorded as not stage-admitted, snapshots exports and invokes exact production decoding/equality without mutation. Its SHA256 is `5bcd8a568d5318dd62217c1e0b35538b22e34403c9727eee8814a5d0ec5a899f`.
|
||||
|
||||
The launcher admission review found and corrected a package-inventory gap before staging. Captured GUI SHA256 is `133375d57bdecf3f3effe5ecc13ec121e1257b90838f2cdb64fb91aa9a530d1b`; oracle SHA256 is `0792aff4cf8460c58fa2bf2f5ddbfa478523b7c9fe9b45e19ec93090191c37c1`. Both came from reviewed same-pin builds under exclusive local compiler/target ownership and immediate stage capture. Ordinary Cargo receipts do not hash executables at compiler exit; the capture attribution depends on that ownership observation. The evidence archive excludes executables/private profiles but retains their manifests and inventories.
|
||||
|
||||
All 18 full-window native screenshots and eight comparisons received independent review. Both GUI processes closed normally, with exit 0 and empty logs. At maximized 1605×872 capture size, selected pins disappeared/reappeared with Arrange/Undo, folded metadata survived, and the guide correctly explained the narrower reset versus whole Reset routing. An icon-sized screenshot was initially selected by the automation helper; its rejected click was corrected using the full-window screenshot. Short filename/set-value helper failures were resolved by absolute-path typing. These observations do not establish application input defects or resolution of the separate restored-window issue.
|
||||
|
||||
Material existing usability limits: Arrange requires Fit to bring the wider arrangement into view; Undo retains the viewport and can clip earlier placements until refit. Guide search filters a topic but required six scroll actions to reach the routing card. Automatic shared contacts/crossings remain. These are follow-up work, not evidence of complete issue #32 acceptance.
|
||||
|
||||
No full-workspace regression, performance, Linux, general layout-quality, arbitrary State-boundary native or Process/Scenario acceptance is claimed. No source reset rule promises fewer automatic elbows, universal nonintersection or readable placement by itself. Linux remains excluded by the user. Live ownership and delivery status remain in Gitea; this document records branch-specific evidence, not an integrated-state checkpoint.
|
||||
|
||||
## Evidence navigation
|
||||
|
||||
[Source review](source-review.md), [native acceptance review](native-review.md), [staging review](tooling-review.md), [supplemental checker review](export-checker-review.md). Historical plans and rejected attempts are retained as historical inputs in the archive; the normative implemented contract is in `docs/language.rst` at the tested pin.
|
||||
|
||||
[Archive](evidence/arrange-reset-d6f45a13.zip) and [member manifest](evidence/manifest.json): 221 members, 6529778 bytes, SHA256 `219c6dde234f1f30c4af191a538c21eacf874f234af0b8506bb66251db412650`. Every member was reread and verified by length, SHA256 and CRC. Raw evidence retains original machine paths for provenance; peers should use the commit, repository-relative paths and commands above. Native binaries/private profiles are excluded.
|
||||
@@ -0,0 +1,52 @@
|
||||
# Arrange reset candidate source review
|
||||
|
||||
Reviewed exact commit `3923860ad2923ad2bcae93b2f0c3ccbcf02d7d75` in `.local/arrange-reset-20260925` against released `715036e132677dd2bc2fb37ed316d93dc9987aec`. Diff is confined to session.rs, release_routing.rs and state_diagrams.rs. No compiler/test execution, router adoption or source changes by reviewer.
|
||||
|
||||
No material implementation blocker found in this bounded core reset candidate. Approval here means ready for parent compilation/verification, not accepted product behavior or successful tests.
|
||||
|
||||
## Implementation
|
||||
|
||||
session::layout checks diagram existence and Scenario rejection before freezing the ordinary projected view. Eligibility requires actual endpoint-placement IDs to equal projected source/target and both projected owner nodes present. This excludes hidden, missing-owner, outside-projection and promoted edges without routing to infer suspension. The same input view is reused for root topology ordering; no post-layout eligibility recomputation occurs.
|
||||
|
||||
The existing node sizing/placement and translated_routes calls remain intact. Translation uses original stored positions and actual-owner deltas with State boundary_moves_with unchanged. Only afterward, existing eligible entries have waypoints cleared and label=None. No entry insertion/removal/reordering, attachment mutation, semantic edit, other-view mutation, new routing call or generated elbow persistence is introduced. Temporarily translating an eligible pin before clearing is redundant work, but leaves the specified final state and is not a correctness defect.
|
||||
|
||||
Batch execution still determines staged input and final validation/history through the existing session path; no new prefix validation or nested history entry. Borrow lifetimes are coherent: immutable Project/Diagram/view work precedes the existing final mutable diagram borrow. Added owner lookup is fallible and skips absent owners. No new indexing/unwrap panic path found in the added code. Existing layout assumptions about malformed transient candidates are unchanged, not newly certified as universally panic-free.
|
||||
|
||||
## Test-oracle assessment
|
||||
|
||||
The modified preservation test narrows the intentionally changed expectation to visible pins/labels, while still checking route count/order, other view and Undo. Additional tests cover full exact route vectors including attachments-only/empty entries, fraction-only label reset, port anchors, semantic records, canonical source roundtrip, JSON Layout command parity, Undo/Redo and one-entry history.
|
||||
|
||||
Filtered/missing-owner test explicitly identifies one eligible edge and uses authored40/60 offsets plus a7-unit unequal owner offset; folded test adds a13-unit visible-container offset distinct from hidden children. Their expected translations are computed arithmetically by a local helper, not by the production reset/translation helper. Baseline Layout supplies stable node geometry, but named eligibility and sentinel-route expectations remain independent and nonvacuous.
|
||||
|
||||
The staged Batch test introduces a duplicate alias, hides routes, runs Layout and repairs the alias afterward. This meaningfully detects accidental prefix validation and stale pre-batch visibility. Its failure branch checks model/revision rollback. The no-op test confirms retained Redo for an empty-view enclosing Batch. State test exercises equal entry/leaf displacement with a stationary owning boundary and retains internal-route metadata; visible external self-route clears overrides while preserving authored attachments.
|
||||
|
||||
Existing State `layout_reserves_internal_rows_and_state_routes_retain_hard_pins` checks router preservation BEFORE Layout and internal-row sizing afterward. It does not contradict the new reset policy, despite its broad name; no need to erase ordinary router pin-preservation coverage.
|
||||
|
||||
## Limits for parent verification/integration
|
||||
|
||||
- No new explicit hidden semantic-port-marker case; anchor preservation is covered, marker-hidden eligibility remains source reasoning pending focused verification.
|
||||
- No nonempty already-arranged diagram proving route-reset-only change/history, or a second Layout no-op on that same diagram. Current empty-view no-op test does not establish that particular scenario. A focused regression is useful if the parent expands verification, but the present reset loop visibly handles it.
|
||||
- Canonical language parse/format and JSON command parity are core evidence only. They do not prove HTTP transaction handling, desktop pending-draft history, multi-file comments/raw-byte Undo/Redo or native workflows. Those remain separate integration gates rather than grounds for claiming this candidate complete.
|
||||
- Eligibility tests use explicit hidden_edges and Decomposition/folding; not every saved category/coverage/filter combination is individually exercised. The implementation correctly reuses the ordinary projection, so no separate filter policy is introduced.
|
||||
|
||||
Keep ordinary movement, raw source edits, folded suspension and RemoveEdgeRoute tests unchanged except where the deliberate explicit Layout amendment applies. Documentation must carry the reviewed staged-Batch, enclosing-history and hidden-placement movement qualifications. Actual build/test outcomes remain pending and must be recorded independently.
|
||||
|
||||
## Desktop tests and native fixture follow-up at9eb4642e
|
||||
|
||||
Reviewed exact `9eb4642e50564165aa0db38d8e91f4c14b818bdc` above `6c44a2d6`, plus the native plan. No new source blocker found. Core production changes since3923860 are formatting only. No builds/tests/native actions executed by reviewer; parent-reported core195 pass and Clippy results remain separate receipts, and desktop tests were in progress at assignment.
|
||||
|
||||
Three desktop tests are meaningful at their actual layers:
|
||||
|
||||
- Document test creates a real include closure, inserts authored CRLF comment lines, independently clears the expected route's pins/label while retaining attachments, compares all non-target-diagram model state, preserves unrelated module bytes, and checks exact accepted bundle/model Undo/Redo plus Document reconstruction. This closes much of the earlier multi-file source evidence gap. Reconstructing Document from the bundle is not native disk-save/fresh-process reopening.
|
||||
- Valid pending-property test uses ordinary Select/Field/Apply as its control, explicitly checks the revised name and original routes, then proves Layout's separate history entry with complete Project and bundle comparisons through two Undo/Redo steps. This is an App message-wiring/history test, not mounted pointer/keyboard UI evidence or an independent layout-geometry oracle.
|
||||
- Invalid property test feeds incomplete numeric input, verifies accepted Project/bundle/revision/history unchanged after Layout, and retains exact raw draft. It does not simulate every invalid source draft, which is correctly outside this narrow scope.
|
||||
|
||||
The added nonempty already-arranged route-only core test independently names the two routes, asserts unchanged nodes and complete expected Project, one real edit, repeated no-op, Undo/Redo and retained unrelated Redo. It addresses the prior route-only history coverage gap.
|
||||
|
||||
Correction to earlier fixture reasoning: Decomposition admits FunctionalExchange, so it was not a valid excluded-projection sentinel. The corrected Capabilities test includes one visible capability, asserts real movement there, and exact retained inactive Function placements/routes. `projection::parents` filters out placements whose kinds are excluded by the projection; folded/filtered descendants within the admitted placement domain may still move, but arbitrary outside-projection owners receive no delta. Earlier broad wording about all stored d.nodes must be read with this admitted-domain qualification. The corrected test has a concrete oracle rather than weakening assertions to accommodate observed output.
|
||||
|
||||
Native fixture SHA256 `2781d2915d7fd4e428be119c36b8ffde5105e48e76f6e8f7843069f69e5899cf`; desktop tests file `935d09da577b5e2c7e4b65329e0d89010bde23b0199ad3072f82f99b5e060d26`. Original five-Function geometry is hand-authored; IDs and placement aliases are made explicit, port IDs are deterministic, and fractional witnesses are binary-exact. Both expanded/folded originals contain a filtered route and an entire other-view route sentinel. Expected after-Projects are intentionally generated by candidate Layout and asserted to preserve semantic records/other view. This is a full-model UI wiring/history oracle, not independent algorithm correctness; the source/plan label that limitation correctly. Full JSON and canonical source forms roundtrip through production parsers before writing a fresh directory.
|
||||
|
||||
The native plan separates immutable fixtures from writable originals, compares complete saved models, includes Undo/save/Redo/save and fresh-process reopening, and preserves normal close/log evidence. It correctly distinguishes generated elbows from authored pins and does not infer reset from fewer corners. Hidden/promoted route translation is checked against the frozen candidate expected model; independent core tests supply the separate translation contract oracle. Any inspection-only expansion must be a recorded edit outside the accepted checkpoint. No native outcome has yet been reviewed or asserted. GUI artifact/source hashes and fixture hashes must bind the eventual run; source-generated expected files alone are not acceptance.
|
||||
|
||||
These changes are local-only while remote publication is blocked; no pushed-code or peer-availability claim is made in this review.
|
||||
@@ -0,0 +1,24 @@
|
||||
# Arrange reset native tooling review
|
||||
|
||||
Source reviewed at `d6f45a13a7b8ab8dc9b84ee432a832609f5af6ed`; evaluator SHA256 `6a691d6605dd1da0d862ffa3da0cfca2f83547e47b26f9cb648b83061f8f7a39`. Static review plus AST parsing and isolated inventory dictionary checks only. No build, fixture/oracle execution, GUI, source edit or native acceptance.
|
||||
|
||||
**Found and corrected one prelaunch admission gap.** The copied launcher originally captured a new package inventory at launch and compared only against that baseline. It would accept an extra file added after stage, although a later checkpoint would reject it. With parent authorization, the draft launcher now converts the admitted stage inventory to its local row shape and requires exact equality before any process launch. Existing close verification preserves that same set. `common.py::LAUNCHER_SHA` and README are updated. Original `.local/state-label-native-20260924/launch.py` remains hash-identical. AST parsing of all four scripts and isolated equal/extra-file comparisons passed; no filesystem trial was performed.
|
||||
|
||||
No remaining blocker found within the declared evidence contract:
|
||||
|
||||
- Stage binds matching successful dev build/source/environment receipts, preserves exact GUI/oracle/runtime bytes, checks source before/after generation, and freezes package/baseline/tool inventories. It expressly depends on exclusive compiler/target ownership between build completion and capture: these Cargo receipts do not themselves hash emitted binaries. Parent must retain that ownership; this review does not upgrade that attribution to CargoJSON artifact provenance.
|
||||
- `--compare` decodes actual and expected via production persistence and compares complete typed Projects. Inequality/decode failure returns nonzero; it does not call Layout, routing or write input files. This avoids the generic-JSON float representation problem. Generation labels its expected Project as candidate-Layout-derived, so it tests GUI/save wiring, not independent arrangement quality.
|
||||
- Checkpoint snapshots one explicitly named working JSON, verifies exact captured input and expected/oracle/runtime bindings before/after comparison, and requires unchanged current working bytes, full source and immutable inventory. Its accepted flag asserts saved Project equality only. Source-bundle/comment restoration remains covered by separate desktop tests, not this single-JSON checkpoint.
|
||||
- Launcher records PID, argv, environment, private profile/cwd, working/package inventories and observed owned-process exit. Empty logs/exit0 cannot prove how it closed or what the user clicked. Parent observations must separately bind actual Open, Arrange, Save, Undo/Redo, reopening and normal-close actions to the correct process/case/checkpoint.
|
||||
|
||||
Failure evidence remains in fresh directories; stage/checkpoint failures quarantine the trial. Launch observer failures preserve their receipts and do not become a successful close. Parent must reject failed/missing process receipts rather than infer acceptance from a later model checkpoint. External supervision remains responsible for a stuck GUI/oracle; the scripts intentionally do not implement automated UI or automatic GUI termination.
|
||||
|
||||
Reviewed draft identities after correction:
|
||||
|
||||
| File | SHA256 |
|
||||
| --- | --- |
|
||||
| common.py | `1da64294c8b2380861a4fe2d715f18563a1a0bba1317572e4c3d0a516f04132d` |
|
||||
| stage.py | `2185bc1b20eee1eea3e1e4a7527318be14b76d81c3c16e67eff7104083c06d39` |
|
||||
| checkpoint.py | `d8b21c75c2c8f1d29f2a730f3125992575038a1353b87d967d53edd1d5d9caf0` |
|
||||
| launch.py | `9f25579451895334ca4c3c80137c4a47cd4256c720740fea70b6c6dc664255a7` |
|
||||
| README.md | `920c674e65ea6f1533e7f15fd9b7ec7ad1b501e7561561a7df93b92e91022409` |
|
||||
@@ -1 +0,0 @@
|
||||
*.log -text
|
||||
@@ -1,74 +0,0 @@
|
||||
# Outgoing allocation drag evidence
|
||||
|
||||
Contribution source: `1a669130fb6d2b65279aab3847e966a1a282c191`, based on
|
||||
published preview `715036e132677dd2bc2fb37ed316d93dc9987aec`.
|
||||
This is branch-specific Windows evidence for [issue 40](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/40).
|
||||
It is not acceptance of an integrated release or the complete application.
|
||||
The [decision and native baseline](../../drag-out-20260925.md) explain the repair.
|
||||
|
||||
## Environment and commands
|
||||
|
||||
Windows 10.0.26200, Rust/Cargo 1.98.1, host/target
|
||||
`x86_64-pc-windows-gnullvm`, LLVM 22.1.8, LLVM-MinGW 20260908 UCRT linker
|
||||
`clang.exe`, `CARGO_BUILD_JOBS=1`, `CARGO_INCREMENTAL=0`, default development
|
||||
profile. Run at the identified source with the Windows GNU LLVM toolchain and
|
||||
its linker/runtime available on PATH. A sparse checkout must include `build`
|
||||
and `harness/fixtures` as well as crates. The actual commands were:
|
||||
|
||||
```powershell
|
||||
cargo test --locked --offline -p oa-desktop allocation_drag -- --test-threads=1
|
||||
cargo clippy --locked --offline -p oa-desktop --all-targets -- -D warnings
|
||||
cargo build --locked --offline -p oa-desktop --bin arcadia-studio
|
||||
```
|
||||
|
||||
Offline commands require dependencies already cached. A fresh environment must
|
||||
fetch the locked dependencies first. Raw logs retain original absolute paths;
|
||||
these paths identify this run, not required directories on another machine.
|
||||
|
||||
## Results and boundaries
|
||||
|
||||
- Test-only successor `24b1eab07d306d8cb091bb46f7daf9f027a8f1c0` adds two
|
||||
OA/SA App regressions: a clearance proposal blocked by unrelated work and an
|
||||
outgoing move with a retained sibling enlarging the source enclosure.
|
||||
`tests-04.log` passes all 123 allocation-drag tests (zero failed/ignored,
|
||||
1,424 filtered) in 54.86s; `clippy-02.log` passes the same strict command in
|
||||
1m27s. Production inputs are unchanged from `1a669130`; its normal binary
|
||||
remains the native candidate. [Independent review](independent-review.md)
|
||||
covers the source, new test design and evidence limits.
|
||||
- `tests-03.log`: 121 passed, zero failed/ignored, 1,424 filtered; 35.33 seconds
|
||||
test execution. This is the allocation-drag subset, not the full desktop suite.
|
||||
- `clippy-01.log`: strict desktop all-target Clippy passed in 4m30s.
|
||||
- `build-01.log`: normal GUI build passed in 5m16s. The resulting local executable
|
||||
SHA256 is `9bda1a6a1d24fdc98c7a8822400c12fea9afe1de828a82ba9273f03b99e47c07`.
|
||||
It was staged with the published preview's `libunwind.dll` and started, but
|
||||
Computer Use was stopped before any changed-binary native observation.
|
||||
- `context.json`: recorded source/toolchain, passing test-log and baseline-image
|
||||
hashes, and clean source after the test run.
|
||||
- `tests-01.log`: compilation failed before tests because `build/windows_manifest.rs`
|
||||
was excluded from the initial sparse checkout.
|
||||
- `tests-02.log`: compilation failed before tests because harness fixture files
|
||||
were excluded and a standalone test helper used `Self` instead of `Fixture`.
|
||||
The compiler was terminated after reporting errors; its interrupted exit is
|
||||
preserved. The missing tracked files and typo were repaired before run 03.
|
||||
|
||||
The tests exercise source-clearance proposals, OA/SA App event sequences,
|
||||
preview/release agreement, exact source/history, cancellation, and retained
|
||||
preview without commit authority on refused release. Existing collision and
|
||||
semantic checks still govern all proposals. Independent source review found the
|
||||
whole-header viewport restriction and obsolete preview assertion; both were
|
||||
corrected before the passing run. This is source/test review, not native review.
|
||||
|
||||
`baseline-left-refusal.png` is an unmodified actual native capture from the
|
||||
published preview executable, SHA256
|
||||
`2c03bd6cc3f1110c219e4f57ab94874a121590f83779239a93289b7170664399`.
|
||||
In the copied Allocation companion fixture, OA assign view at 1440x900 content
|
||||
and Fit 72%, dragging X from window (486,546) to (365,546) leaves allocation and
|
||||
placement unchanged and reports overlap. A control drag to (625,572) removes
|
||||
allocation, and Undo restores it. Intermediate jitter was diagnosed from source;
|
||||
this image is end-state evidence only.
|
||||
|
||||
Changed-binary native acceptance is still required: repeat the short fitted-view
|
||||
drag, identify and regrab the partially clipped card, check refused-motion
|
||||
feedback, Cancel and Undo/Redo, save the full bundle, close and reopen in a fresh
|
||||
process, and verify identity/allocation/placement and unchanged sibling data for
|
||||
OA and SA. Linux/WSL remains deferred by the user. No published package changed.
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 129 KiB |
@@ -1,5 +0,0 @@
|
||||
Compiling iced_widget v0.14.2 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\vendor\iced_widget)
|
||||
Compiling iced v0.14.0
|
||||
Compiling oa-core v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-core)
|
||||
Compiling oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop)
|
||||
Finished `dev` profile [unoptimized] target(s) in 5m 16s
|
||||
@@ -1,6 +0,0 @@
|
||||
Compiling oa-core v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-core)
|
||||
Checking iced_widget v0.14.2 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\vendor\iced_widget)
|
||||
Compiling oa-server v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-server)
|
||||
Compiling oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop)
|
||||
Checking iced v0.14.0
|
||||
Finished `dev` profile [unoptimized] target(s) in 4m 30s
|
||||
@@ -1,2 +0,0 @@
|
||||
Checking oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop)
|
||||
Finished `dev` profile [unoptimized] target(s) in 1m 27s
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"source": "1a669130fb6d2b65279aab3847e966a1a282c191",
|
||||
"platform": "Microsoft Windows 10.0.26200",
|
||||
"compiler": [
|
||||
"rustc 1.98.1 (48a229cea 2026-09-01)",
|
||||
"binary: rustc",
|
||||
"commit-hash: 48a229ceaefd4985c50990b14116b6d856af0985",
|
||||
"commit-date: 2026-09-01",
|
||||
"host: x86_64-pc-windows-gnullvm",
|
||||
"release: 1.98.1",
|
||||
"LLVM version: 22.1.8"
|
||||
],
|
||||
"cargo": "cargo 1.98.1 (797e8a9bc 2026-08-05)",
|
||||
"test_log_sha256": "8EB57F522FA0DCCA327DC13C67A9C1B14F5FAB09C5599C108C0BA6D875C78886",
|
||||
"baseline_image_sha256": "AAF162619582018D1197D255B4BA1122747AA22703D8DEDFC7FCEFCB42AEFE38",
|
||||
"target": "x86_64-pc-windows-gnullvm",
|
||||
"incremental": "0",
|
||||
"jobs": "1",
|
||||
"working_tree_clean_after_test": true
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
# Independent source and evidence review
|
||||
|
||||
Fresh read-only GPT-6 Astra review, 2026-09-25. Production source
|
||||
`1a669130fb6d2b65279aab3847e966a1a282c191`; test-only successor
|
||||
`24b1eab07d306d8cb091bb46f7daf9f027a8f1c0`. No GUI actions or test execution
|
||||
were performed by this reviewer.
|
||||
|
||||
No material correctness finding in the bounded scope:
|
||||
|
||||
- The outgoing collision regression in `app/allocation_drag_tests.rs` proves a
|
||||
real source-boundary crossing and unobscured OutsidePerformers target, a
|
||||
nonempty clearance proposal, and intersection with unrelated work. Ordinary
|
||||
placement rejects that proposal. Release preserves project, source bundle,
|
||||
revision, snapshot identity and redo availability.
|
||||
- The retained-sibling regression first requires exact requested placement to
|
||||
fail. The admitted correction clears the enlarged source enclosure while
|
||||
preserving its projection, sibling allocation/placement and all other stored
|
||||
placements. Preview/release and source Undo/Redo comparisons are exact.
|
||||
- In `app/allocation_drag.rs`, clearance remains downstream of semantic
|
||||
admission and retries ordinary placement. Release recomputes placement and
|
||||
passes through `finish` and document execution. No collision, stale-state or
|
||||
retained-preview commit bypass was found.
|
||||
- The committed passing log's bytes and SHA256 match `context.json`; the
|
||||
baseline image hash also matches. Logs support 121 passing tests, strict
|
||||
Clippy and normal development build for the production source. They do not
|
||||
establish execution of the two successor tests; consult the linked raw logs
|
||||
and README for subsequent execution evidence.
|
||||
|
||||
The review supports the source policy and test design, not native usability.
|
||||
The 24x24 visible-header threshold alone does not prove readable identity or
|
||||
practical regrabbing. Changed-binary fitted-view dragging, feedback, recovery
|
||||
and Save/reopen remain unverified. This review is not the broad final
|
||||
requirements/domain/UX/release review required by `PROJECT_GOAL.md`.
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"source": "24b1eab07d306d8cb091bb46f7daf9f027a8f1c0",
|
||||
"production_source": "1a669130fb6d2b65279aab3847e966a1a282c191",
|
||||
"test_log": "tests-04.log",
|
||||
"test_log_sha256": "C98E0F70D60788B484146B3C8785891888BA696130ACC4BFD7D30DA570E6B1E1",
|
||||
"passed": 123,
|
||||
"failed": 0,
|
||||
"ignored": 0,
|
||||
"filtered": 1424,
|
||||
"native_candidate_sha256": "9BDA1A6A1D24FDC98C7A8822400C12FEA9AFE1DE828A82BA9273F03B99E47C07",
|
||||
"native_acceptance": "unverified"
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
Compiling oa-core v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-core)
|
||||
error: couldn't read `crates\oa-core\../../build/windows_manifest.rs`: The system cannot find the path specified. (os error 3)
|
||||
--> crates\oa-core\build.rs:1:1
|
||||
|
|
||||
1 | include!("../../build/windows_manifest.rs");
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: could not compile `oa-core` (build script) due to 1 previous error
|
||||
@@ -1,68 +0,0 @@
|
||||
Compiling oa-core v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-core)
|
||||
Compiling oa-server v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-server)
|
||||
Compiling iced_widget v0.14.2 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\vendor\iced_widget)
|
||||
Compiling oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop)
|
||||
Compiling iced v0.14.0
|
||||
error: couldn't read `C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop/../../harness/fixtures/interface-operation-probe/output/boundary-only.json`: The system cannot find the path specified. (os error 3)
|
||||
--> crates\oa-desktop\src\app\reference_authoring\tests\mounted.rs:755:37
|
||||
|
|
||||
755 | let mut p = persistence::decode(include_bytes!(concat!(
|
||||
| _____________________________________^
|
||||
756 | | env!("CARGO_MANIFEST_DIR"),
|
||||
757 | | "/../../harness/fixtures/interface-operation-probe/output/boundary-only.json"
|
||||
758 | | )))
|
||||
| |______^
|
||||
|
||||
error: couldn't read `C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop/../../harness/fixtures/interface-operation-probe/output/boundary-only.json`: The system cannot find the path specified. (os error 3)
|
||||
--> crates\oa-desktop\src\behavior\scenario\usage\tests.rs:235:37
|
||||
|
|
||||
235 | let mut p = persistence::decode(include_bytes!(concat!(
|
||||
| _____________________________________^
|
||||
236 | | env!("CARGO_MANIFEST_DIR"),
|
||||
237 | | "/../../harness/fixtures/interface-operation-probe/output/boundary-only.json"
|
||||
238 | | )))
|
||||
| |______^
|
||||
|
||||
error: couldn't read `C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop/../../harness/fixtures/interface-operation-probe/output/boundary-only.json`: The system cannot find the path specified. (os error 3)
|
||||
--> crates\oa-desktop\src\behavior\scenario_operation_tests.rs:6:25
|
||||
|
|
||||
6 | persistence::decode(include_bytes!(concat!(
|
||||
| _________________________^
|
||||
7 | | env!("CARGO_MANIFEST_DIR"),
|
||||
8 | | "/../../harness/fixtures/interface-operation-probe/output/boundary-only.json"
|
||||
9 | | )))
|
||||
| |______^
|
||||
|
||||
error: couldn't read `C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop/../../harness/fixtures/source7-native/base/expected-baseline.json`: The system cannot find the path specified. (os error 3)
|
||||
--> crates\oa-desktop\src\behavior\topology\contextual_routing.rs:61:29
|
||||
|
|
||||
61 | persistence::decode(include_bytes!(concat!(
|
||||
| _____________________________^
|
||||
62 | | env!("CARGO_MANIFEST_DIR"),
|
||||
63 | | "/../../harness/fixtures/source7-native/base/expected-baseline.json"
|
||||
64 | | )))
|
||||
| |__________^
|
||||
|
||||
error: couldn't read `C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop/../../harness/fixtures/interface-operation-probe/output/boundary-only.json`: The system cannot find the path specified. (os error 3)
|
||||
--> crates\oa-desktop\src\semantic_review\source7_tests.rs:5:25
|
||||
|
|
||||
5 | persistence::decode(include_bytes!(concat!(
|
||||
| _________________________^
|
||||
6 | | env!("CARGO_MANIFEST_DIR"),
|
||||
7 | | "/../../harness/fixtures/interface-operation-probe/output/boundary-only.json"
|
||||
8 | | )))
|
||||
| |______^
|
||||
|
||||
error[E0411]: cannot find struct, variant or union type `Self` in this scope
|
||||
--> crates\oa-desktop\src\app\allocation_drag\clearance.rs:314:9
|
||||
|
|
||||
287 | fn fixture(layer: Layer) -> Fixture {
|
||||
| ------- `Self` not allowed in a function
|
||||
...
|
||||
314 | Self {
|
||||
| ^^^^ `Self` is only available in impls, traits, and type definitions
|
||||
|
||||
error: could not compile `oa-desktop` (bin "arcadia-studio" test) due to 6 previous errors
|
||||
|
||||
Caused by:
|
||||
process didn't exit successfully: `C:\Users\User\Documents\Git\ARCADIA-Studio\.local\toolchain\rustup\toolchains\stable-x86_64-pc-windows-gnullvm\bin\rustc.exe --crate-name arcadia_studio --edition=2024 crates\oa-desktop\src\main.rs --error-format=json --json=diagnostic-rendered-ansi,artifacts,future-incompat --emit=dep-info,link -C embed-bitcode=no --test --check-cfg cfg(docsrs,test) --check-cfg "cfg(feature, values())" -C metadata=5e2096130f19ead2 -C extra-filename=-c5b42126ba36cd38 --out-dir C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps -C linker=clang.exe -C strip=debuginfo -L dependency=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps --extern axum=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\libaxum-ccc7aea466db9258.rlib --extern iced=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\libiced-594f7d417d69ed72.rlib --extern iced_runtime=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\libiced_runtime-4d31a36fe5cef0b0.rlib --extern oa_core=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\liboa_core-2f3c5174cb6398fd.rlib --extern oa_server=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\liboa_server-bb42169ca297f969.rlib --extern reqwest=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\libreqwest-31ac987472fa8038.rlib --extern rfd=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\librfd-17cc7ba2db223d7b.rlib --extern serde=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\libserde-60a32311146a42e4.rlib --extern serde_json=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\libserde_json-da2c4f333fc47818.rlib --extern tempfile=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\libtempfile-86aac28d6be16d2f.rlib --extern tokio=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\libtokio-5e0ca96735e3073a.rlib --extern unicode_segmentation=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\libunicode_segmentation-85698043f7780858.rlib -L native=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\build\ring-5f8d2d20440f09b4\out -L native=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\toolchain\cargo\registry\src\index.crates.io-1949cf8c6b5b557f\windows_x86_64_gnullvm-0.52.6\lib -C link-arg=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\build\oa-desktop-307d320bbc13ccd5\out\normal-user-manifest.o` (exit code: 0xffffffff)
|
||||
@@ -1,130 +0,0 @@
|
||||
Blocking waiting for file lock on build directory
|
||||
Compiling oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop)
|
||||
Finished `test` profile [unoptimized] target(s) in 8m 13s
|
||||
Running unittests src\main.rs (C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\arcadia_studio-c5b42126ba36cd38.exe)
|
||||
|
||||
running 121 tests
|
||||
test allocation_drag::outgoing_header_retains_a_grab_region_without_requiring_full_camera_containment ... ok
|
||||
test allocation_drag::tests::allocation_artifact_and_manual_trace_cascades_are_named_and_refused ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_detects_hidden_moved_child_and_stationary_enclosure_growth ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_distributed_oa_sa_exact_scope_routes_source_history_and_anchor ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_noop_stale_tamper_target_and_all_selected_headers ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_performer_closure_does_not_acquire_incidental_semantic_root ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_preserves_existing_overlap_but_refuses_its_growth ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_preserves_roles_and_does_not_invent_unplaced_descendants ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_preserves_unrelated_folded_interior_diagnostic_boundary ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_simultaneous_roots_may_enter_each_others_vacated_bounds ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_validates_all_original_members_before_normalizing ... ok
|
||||
test allocation_drag::tests::assignment_boundary_tests::group_assignment_checks_original_identity_and_visibility_before_normalization ... ok
|
||||
test allocation_drag::tests::assignment_boundary_tests::group_assignment_distributed_parent_refuses_but_its_independent_leaves_can_reassign ... ok
|
||||
test allocation_drag::tests::assignment_boundary_tests::group_assignment_finish_rejects_stale_project_and_each_tampered_result ... ok
|
||||
test allocation_drag::tests::assignment_boundary_tests::group_assignment_normalizes_parent_child_and_assigns_unplaced_leaf_once ... ok
|
||||
test allocation_drag::tests::assignment_boundary_tests::group_assignment_requires_non_anchor_selected_header_inside_viewport ... ok
|
||||
test allocation_drag::tests::assignment_boundary_tests::group_assignment_unresolved_oa_role_leaf_accepts_explicit_target_but_not_outside ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_accumulates_stable_ids_and_aliases_without_partial_reuse ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_collateral_surviving_constraint_refuses_whole_semantic_candidate ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_deletes_only_own_routes_across_views_and_keeps_internal_flow ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_independent_performers_preserves_already_correct_provenance ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_source_failure_preserves_existing_history_and_retry_ids ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_unfolds_only_changed_destination_and_refuses_newly_exposed_overlap ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_uses_vacated_space_and_one_complete_route_owner_map ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_zero_delta_changes_semantics_once_and_then_is_noop ... ok
|
||||
test allocation_drag::tests::cache_plan_uses_candidate_index_and_same_pixel_different_target_changes_preview ... ok
|
||||
test allocation_drag::tests::collapsed_target_unfold_is_explicit_and_no_other_fold_changes ... ok
|
||||
test allocation_drag::tests::direct_exit_revealing_another_role_performer_has_truthful_refusal ... ok
|
||||
test allocation_drag::tests::explicit_different_performer_retains_advisory_role_warning ... ok
|
||||
test allocation_drag::tests::folded_subtree_cannot_hide_new_collision ... ok
|
||||
test allocation_drag::tests::geometry_refuses_new_overlap_bounds_and_invisible_headers_and_preserves_existing_overlap ... ok
|
||||
test allocation_drag::tests::group_arrange_experiment::mixed_arrange_experiment_detects_moved_folded_child_collision ... ok
|
||||
test allocation_drag::tests::group_arrange_experiment::mixed_arrange_experiment_detects_stationary_enclosure_growth_collision ... ok
|
||||
test allocation_drag::tests::group_arrange_experiment::mixed_arrange_experiment_distributed_root_and_performer_can_translate_rigidly ... ok
|
||||
test allocation_drag::tests::group_arrange_experiment::mixed_arrange_experiment_unrelated_folded_interiors_are_outside_diagnostic_domain ... ok
|
||||
test allocation_drag::tests::malformed_scope_wrong_projection_duplicate_placement_and_stable_id_collision_refuse ... ok
|
||||
test allocation_drag::tests::mixed_p_x_y_refuses_semantic_and_arrange_instead_of_stretching_destination ... ok
|
||||
test allocation_drag::tests::oa_communication_incompleteness_remains_advisory_without_collateral_loss ... ok
|
||||
test allocation_drag::tests::oa_sa_incoming_between_outgoing_preserve_identity_source_history_and_other_views ... ok
|
||||
test allocation_drag::tests::oa_sa_one_leaf_move_refuses_when_ancestor_regrouping_introduces_unrelated_overlap ... ok
|
||||
test allocation_drag::tests::oa_sa_one_leaf_reassignment_and_outward_move_regroup_ancestors_without_moving_sibling ... ok
|
||||
test allocation_drag::tests::oa_sa_populated_collapsed_target_accepts_room_and_refuses_collision_without_relocating_contents ... ok
|
||||
test allocation_drag::tests::oa_sa_state_owned_action_and_availability_reject_reassignment_atomically ... ok
|
||||
test allocation_drag::tests::oa_sa_unfolding_prior_hidden_overlap_refuses_even_when_insertion_does_not_collide ... ok
|
||||
test allocation_drag::tests::own_deleted_allocation_routes_may_disappear_in_every_diagram_only ... ok
|
||||
test allocation_drag::tests::role_only_same_effective_performer_is_semantic_noop_and_outgoing_refuses ... ok
|
||||
test allocation_drag::tests::sa_port_and_deferred_exchange_allocation_cascade_refuses_and_retarget_is_invalid ... ok
|
||||
test allocation_drag::tests::same_performer_readability_preserves_snapped_noop_and_allocation_identity ... ok
|
||||
test allocation_drag::tests::scope_detects_first_child_allocation_and_role_transfer_and_finish_rejects_live_edits ... ok
|
||||
test allocation_drag::tests::selected_header_visibility_allows_viewport_edge_but_refuses_clipping ... ok
|
||||
test allocation_drag::tests::snapped_noop_returns_no_command_and_keeps_source_revision_and_redo ... ok
|
||||
test allocation_drag::tests::source_budget_refusal_keeps_project_bundle_history_and_retry_identity_exact ... ok
|
||||
test allocation_drag::tests::surviving_constraint_mutation_is_refused_even_when_core_candidate_is_valid ... ok
|
||||
test allocation_drag::tests::target_presentation_requires_accepted_identity_and_selected_header_visibility ... ok
|
||||
test allocation_drag::tests::toolbar_identity_allows_exact_incoming_placement_at_old_label_overlap ... ok
|
||||
test allocation_drag::tests::uniform_folded_grandchild_and_unplaced_scope_moves_exactly_q_and_all_l_oa_sa ... ok
|
||||
test allocation_drag::tests::uniform_role_only_and_mixed_direct_role_composites_same_target_do_not_allocate ... ok
|
||||
test allocation_drag::tests::uniformly_unassigned_composite_creates_only_stable_leaf_allocations_including_unplaced ... ok
|
||||
test allocation_drag::tests::unrequested_view_option_cleanup_is_not_whitelisted ... ok
|
||||
test allocation_drag::tests::unresolved_role_leaf_can_arrange_or_accept_explicit_target_without_guessing ... ok
|
||||
test app::allocation_drag::clearance::tests::corner_order_follows_boundary_crossing_and_is_deterministic ... ok
|
||||
test app::allocation_drag::clearance::tests::descendant_footprint_controls_clearance_without_mutating_projects ... ok
|
||||
test app::allocation_drag::clearance::tests::each_outgoing_edge_clears_the_source_and_passes_ordinary_placement ... ok
|
||||
test app::allocation_drag::clearance::tests::independent_selected_roots_keep_one_rigid_group_translation ... ok
|
||||
test app::allocation_drag::clearance::tests::no_crossing_no_overlap_no_source_and_nonfinite_requests_are_empty ... ok
|
||||
test app::allocation_drag::clearance::tests::source_shrink_uses_candidate_enclosure_not_frozen_target ... ok
|
||||
test app::allocation_drag::tests::actual_canvas_origin_change_cancels_held_work_before_any_legacy_release ... ok
|
||||
test app::allocation_drag::tests::arrange_is_one_gesture_and_snapped_noop_preserves_redo_and_source ... ok
|
||||
test app::allocation_drag::tests::candidate_semantics_are_cached_per_destination_with_stable_ids_and_own_index ... ok
|
||||
test app::allocation_drag::tests::destination_identity_does_not_describe_blocked_or_outside_viewport_as_unallocation ... ok
|
||||
test app::allocation_drag::tests::frozen_collapsed_target_uses_only_visible_header_and_port_edge_controls_occlude ... ok
|
||||
test app::allocation_drag::tests::frozen_targets_use_half_open_semantic_priority_and_refuse_incomparable_overlap ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_app_tree_dirty_description_retains_focus_and_middle_caret_for_next_key ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_app_tree_dirty_name_retains_focus_and_middle_caret_for_next_key ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_app_tree_exact_incoming_drag_keeps_destination_visible_and_canvas_stable ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_keyboard_arrange_shortcut_consumes_once_escape_cancels_and_dirty_owner_refuses ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_rendered_canvas_pixels_do_not_change_when_toolbar_feedback_text_changes ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_toolbar_hover_inspects_last_full_name_without_authorizing_an_outside_drop ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_tree_outgoing_boundary_motion_preview_release_and_cancel ... ok
|
||||
test app::allocation_drag::tests::full_tree::control_focus::dirty_selected_pin_outer_ring_keeps_real_name_focus_and_caret ... ok
|
||||
test app::allocation_drag::tests::group::allocation_bare_moves_cannot_bypass_armed_active_or_retired_lifetime ... ok
|
||||
test app::allocation_drag::tests::group::completed_text_apply_rejects_old_allocation_packets_without_retiring_a_new_drag ... ok
|
||||
test app::allocation_drag::tests::group::connection_lifetime::captured_connection_after_cancel_preserves_arrange_arm_and_live_group ... ok
|
||||
test app::allocation_drag::tests::group::connection_lifetime::captured_connection_context_mismatches_preserve_the_current_session ... ok
|
||||
test app::allocation_drag::tests::group::connection_lifetime::captured_connection_current_restart_and_pair_commit_after_tool_boundary ... ok
|
||||
test app::allocation_drag::tests::group::group_arrange_work_and_performer_anchors_preserve_every_responsibility ... ok
|
||||
test app::allocation_drag::tests::group::group_assignment_split_rebuild_preserves_primary_and_commits_one_complete_candidate ... ok
|
||||
test app::allocation_drag::tests::group::group_out_and_back_assigns_at_zero_delta_then_true_noop_preserves_redo ... ok
|
||||
test app::allocation_drag::tests::group::group_selection_aba_and_old_press_cannot_retire_a_new_token ... ok
|
||||
test app::allocation_drag::tests::group::group_source_refusal_retains_primary_source_and_both_history_paths ... ok
|
||||
test app::allocation_drag::tests::group::idle_group_press_rejects_selection_generation_aba_with_unchanged_other_stamps ... ok
|
||||
test app::allocation_drag::tests::group::ordinary_off_grid_performer_out_and_back_is_noop_with_redo ... ok
|
||||
test app::allocation_drag::tests::group::route_lifetime::captured_route_context_fields_independently_reject_before_retirement ... ok
|
||||
test app::allocation_drag::tests::group::route_lifetime::current_route_commits_without_session_preserve_exact_route_history ... ok
|
||||
test app::allocation_drag::tests::group::route_lifetime::same_context_route_attachment_after_selection_preserves_arrange_arm_and_live_group ... ok
|
||||
test app::allocation_drag::tests::group::route_lifetime::same_context_route_commit_after_selection_preserves_arrange_arm_and_live_group ... ok
|
||||
test app::allocation_drag::tests::group::route_lifetime::stale_route_attachment_preserves_arrange_arm_and_live_group ... ok
|
||||
test app::allocation_drag::tests::group::route_lifetime::stale_route_commit_preserves_arrange_arm_and_live_group ... ok
|
||||
test app::allocation_drag::tests::group::unsupported_group_click_collapses_but_threshold_crossing_never_becomes_click ... ok
|
||||
test app::allocation_drag::tests::modal_roundtrip_equal_fit_undo_resize_and_focus_retire_press_before_replay ... ok
|
||||
test app::allocation_drag::tests::mounted_clean_stationary_selects_exact_work_without_revision_or_legacy_apply ... ok
|
||||
test app::allocation_drag::tests::mounted_dirty_port_press_cannot_fall_through_to_applying_port_selection ... ok
|
||||
test app::allocation_drag::tests::mounted_dirty_property_press_does_not_select_apply_or_destroy_draft ... ok
|
||||
test app::allocation_drag::tests::mounted_oa_sa_ordered_incoming_between_outgoing_are_exact_single_history_steps ... ok
|
||||
test app::allocation_drag::tests::mounted_role_only_outgoing_refusal_retains_placement_and_same_performer_creates_no_allocation ... ok
|
||||
test app::allocation_drag::tests::mounted_uniform_parent_moves_hidden_placements_and_assigns_only_leaves ... ok
|
||||
test app::allocation_drag::tests::navigation_union::old_chooser_envelopes_preserve_a_current_canvas_gesture ... ok
|
||||
test app::allocation_drag::tests::navigation_union::stale_gesture_packets_preserve_navigation_and_a_new_live_cancel_still_works ... ok
|
||||
test app::allocation_drag::tests::oa_sa_clipped_selected_header_refuses_preview_release_and_retries_without_partial_source_or_history ... ok
|
||||
test app::allocation_drag::tests::oa_sa_toolbar_identity_accepts_exact_receiver_and_collapsed_header_drag ... ok
|
||||
test app::allocation_drag::tests::outgoing_boundary_drop_clears_source_without_changing_other_placements ... ok
|
||||
test app::allocation_drag::tests::outside_release_is_cancel_and_does_not_remove_an_assignment ... ok
|
||||
test app::allocation_drag::tests::pending_behavior_draft_refuses_actual_work_press_and_equal_document_replacement_retires_packet ... ok
|
||||
test app::allocation_drag::tests::refused_motion_retains_last_scene_but_release_never_commits_it ... ok
|
||||
test app::allocation_drag::tests::release_uses_ordered_native_position_before_later_motion_in_same_batch ... ok
|
||||
test app::allocation_drag::tests::same_snapped_position_on_opposite_shared_border_rebuilds_semantics_even_if_layout_refuses ... ok
|
||||
test app::allocation_drag::tests::source_and_modal_admission_reject_prebuilt_clean_press_without_selecting ... ok
|
||||
test app::allocation_drag::tests::source_refusal_is_atomic_and_a_new_gesture_can_retry_against_live_revision ... ok
|
||||
test app::allocation_drag::tests::stale_packets_do_not_cancel_a_newer_token_and_each_release_is_consumed_once ... ok
|
||||
test app::allocation_drag::tests::toolbar_destination_names_transformed_target_whose_header_is_clipped ... ok
|
||||
test app::allocation_drag::tests::toolbar_status_leaves_transformed_top_edge_work_header_readable ... ok
|
||||
|
||||
test result: ok. 121 passed; 0 failed; 0 ignored; 0 measured; 1424 filtered out; finished in 35.33s
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
Compiling oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop)
|
||||
Finished `test` profile [unoptimized] target(s) in 6m 05s
|
||||
Running unittests src\main.rs (C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\arcadia_studio-c5b42126ba36cd38.exe)
|
||||
|
||||
running 123 tests
|
||||
test allocation_drag::outgoing_header_retains_a_grab_region_without_requiring_full_camera_containment ... ok
|
||||
test allocation_drag::tests::allocation_artifact_and_manual_trace_cascades_are_named_and_refused ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_detects_hidden_moved_child_and_stationary_enclosure_growth ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_distributed_oa_sa_exact_scope_routes_source_history_and_anchor ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_noop_stale_tamper_target_and_all_selected_headers ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_performer_closure_does_not_acquire_incidental_semantic_root ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_preserves_existing_overlap_but_refuses_its_growth ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_preserves_roles_and_does_not_invent_unplaced_descendants ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_preserves_unrelated_folded_interior_diagnostic_boundary ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_simultaneous_roots_may_enter_each_others_vacated_bounds ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_validates_all_original_members_before_normalizing ... ok
|
||||
test allocation_drag::tests::assignment_boundary_tests::group_assignment_checks_original_identity_and_visibility_before_normalization ... ok
|
||||
test allocation_drag::tests::assignment_boundary_tests::group_assignment_distributed_parent_refuses_but_its_independent_leaves_can_reassign ... ok
|
||||
test allocation_drag::tests::assignment_boundary_tests::group_assignment_finish_rejects_stale_project_and_each_tampered_result ... ok
|
||||
test allocation_drag::tests::assignment_boundary_tests::group_assignment_normalizes_parent_child_and_assigns_unplaced_leaf_once ... ok
|
||||
test allocation_drag::tests::assignment_boundary_tests::group_assignment_requires_non_anchor_selected_header_inside_viewport ... ok
|
||||
test allocation_drag::tests::assignment_boundary_tests::group_assignment_unresolved_oa_role_leaf_accepts_explicit_target_but_not_outside ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_accumulates_stable_ids_and_aliases_without_partial_reuse ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_collateral_surviving_constraint_refuses_whole_semantic_candidate ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_deletes_only_own_routes_across_views_and_keeps_internal_flow ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_independent_performers_preserves_already_correct_provenance ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_source_failure_preserves_existing_history_and_retry_ids ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_unfolds_only_changed_destination_and_refuses_newly_exposed_overlap ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_uses_vacated_space_and_one_complete_route_owner_map ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_zero_delta_changes_semantics_once_and_then_is_noop ... ok
|
||||
test allocation_drag::tests::cache_plan_uses_candidate_index_and_same_pixel_different_target_changes_preview ... ok
|
||||
test allocation_drag::tests::collapsed_target_unfold_is_explicit_and_no_other_fold_changes ... ok
|
||||
test allocation_drag::tests::direct_exit_revealing_another_role_performer_has_truthful_refusal ... ok
|
||||
test allocation_drag::tests::explicit_different_performer_retains_advisory_role_warning ... ok
|
||||
test allocation_drag::tests::folded_subtree_cannot_hide_new_collision ... ok
|
||||
test allocation_drag::tests::geometry_refuses_new_overlap_bounds_and_invisible_headers_and_preserves_existing_overlap ... ok
|
||||
test allocation_drag::tests::group_arrange_experiment::mixed_arrange_experiment_detects_moved_folded_child_collision ... ok
|
||||
test allocation_drag::tests::group_arrange_experiment::mixed_arrange_experiment_detects_stationary_enclosure_growth_collision ... ok
|
||||
test allocation_drag::tests::group_arrange_experiment::mixed_arrange_experiment_distributed_root_and_performer_can_translate_rigidly ... ok
|
||||
test allocation_drag::tests::group_arrange_experiment::mixed_arrange_experiment_unrelated_folded_interiors_are_outside_diagnostic_domain ... ok
|
||||
test allocation_drag::tests::malformed_scope_wrong_projection_duplicate_placement_and_stable_id_collision_refuse ... ok
|
||||
test allocation_drag::tests::mixed_p_x_y_refuses_semantic_and_arrange_instead_of_stretching_destination ... ok
|
||||
test allocation_drag::tests::oa_communication_incompleteness_remains_advisory_without_collateral_loss ... ok
|
||||
test allocation_drag::tests::oa_sa_incoming_between_outgoing_preserve_identity_source_history_and_other_views ... ok
|
||||
test allocation_drag::tests::oa_sa_one_leaf_move_refuses_when_ancestor_regrouping_introduces_unrelated_overlap ... ok
|
||||
test allocation_drag::tests::oa_sa_one_leaf_reassignment_and_outward_move_regroup_ancestors_without_moving_sibling ... ok
|
||||
test allocation_drag::tests::oa_sa_populated_collapsed_target_accepts_room_and_refuses_collision_without_relocating_contents ... ok
|
||||
test allocation_drag::tests::oa_sa_state_owned_action_and_availability_reject_reassignment_atomically ... ok
|
||||
test allocation_drag::tests::oa_sa_unfolding_prior_hidden_overlap_refuses_even_when_insertion_does_not_collide ... ok
|
||||
test allocation_drag::tests::own_deleted_allocation_routes_may_disappear_in_every_diagram_only ... ok
|
||||
test allocation_drag::tests::role_only_same_effective_performer_is_semantic_noop_and_outgoing_refuses ... ok
|
||||
test allocation_drag::tests::sa_port_and_deferred_exchange_allocation_cascade_refuses_and_retarget_is_invalid ... ok
|
||||
test allocation_drag::tests::same_performer_readability_preserves_snapped_noop_and_allocation_identity ... ok
|
||||
test allocation_drag::tests::scope_detects_first_child_allocation_and_role_transfer_and_finish_rejects_live_edits ... ok
|
||||
test allocation_drag::tests::selected_header_visibility_allows_viewport_edge_but_refuses_clipping ... ok
|
||||
test allocation_drag::tests::snapped_noop_returns_no_command_and_keeps_source_revision_and_redo ... ok
|
||||
test allocation_drag::tests::source_budget_refusal_keeps_project_bundle_history_and_retry_identity_exact ... ok
|
||||
test allocation_drag::tests::surviving_constraint_mutation_is_refused_even_when_core_candidate_is_valid ... ok
|
||||
test allocation_drag::tests::target_presentation_requires_accepted_identity_and_selected_header_visibility ... ok
|
||||
test allocation_drag::tests::toolbar_identity_allows_exact_incoming_placement_at_old_label_overlap ... ok
|
||||
test allocation_drag::tests::uniform_folded_grandchild_and_unplaced_scope_moves_exactly_q_and_all_l_oa_sa ... ok
|
||||
test allocation_drag::tests::uniform_role_only_and_mixed_direct_role_composites_same_target_do_not_allocate ... ok
|
||||
test allocation_drag::tests::uniformly_unassigned_composite_creates_only_stable_leaf_allocations_including_unplaced ... ok
|
||||
test allocation_drag::tests::unrequested_view_option_cleanup_is_not_whitelisted ... ok
|
||||
test allocation_drag::tests::unresolved_role_leaf_can_arrange_or_accept_explicit_target_without_guessing ... ok
|
||||
test app::allocation_drag::clearance::tests::corner_order_follows_boundary_crossing_and_is_deterministic ... ok
|
||||
test app::allocation_drag::clearance::tests::descendant_footprint_controls_clearance_without_mutating_projects ... ok
|
||||
test app::allocation_drag::clearance::tests::each_outgoing_edge_clears_the_source_and_passes_ordinary_placement ... ok
|
||||
test app::allocation_drag::clearance::tests::independent_selected_roots_keep_one_rigid_group_translation ... ok
|
||||
test app::allocation_drag::clearance::tests::no_crossing_no_overlap_no_source_and_nonfinite_requests_are_empty ... ok
|
||||
test app::allocation_drag::clearance::tests::source_shrink_uses_candidate_enclosure_not_frozen_target ... ok
|
||||
test app::allocation_drag::tests::actual_canvas_origin_change_cancels_held_work_before_any_legacy_release ... ok
|
||||
test app::allocation_drag::tests::arrange_is_one_gesture_and_snapped_noop_preserves_redo_and_source ... ok
|
||||
test app::allocation_drag::tests::candidate_semantics_are_cached_per_destination_with_stable_ids_and_own_index ... ok
|
||||
test app::allocation_drag::tests::destination_identity_does_not_describe_blocked_or_outside_viewport_as_unallocation ... ok
|
||||
test app::allocation_drag::tests::frozen_collapsed_target_uses_only_visible_header_and_port_edge_controls_occlude ... ok
|
||||
test app::allocation_drag::tests::frozen_targets_use_half_open_semantic_priority_and_refuse_incomparable_overlap ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_app_tree_dirty_description_retains_focus_and_middle_caret_for_next_key ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_app_tree_dirty_name_retains_focus_and_middle_caret_for_next_key ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_app_tree_exact_incoming_drag_keeps_destination_visible_and_canvas_stable ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_keyboard_arrange_shortcut_consumes_once_escape_cancels_and_dirty_owner_refuses ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_rendered_canvas_pixels_do_not_change_when_toolbar_feedback_text_changes ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_toolbar_hover_inspects_last_full_name_without_authorizing_an_outside_drop ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_tree_outgoing_boundary_motion_preview_release_and_cancel ... ok
|
||||
test app::allocation_drag::tests::full_tree::control_focus::dirty_selected_pin_outer_ring_keeps_real_name_focus_and_caret ... ok
|
||||
test app::allocation_drag::tests::group::allocation_bare_moves_cannot_bypass_armed_active_or_retired_lifetime ... ok
|
||||
test app::allocation_drag::tests::group::completed_text_apply_rejects_old_allocation_packets_without_retiring_a_new_drag ... ok
|
||||
test app::allocation_drag::tests::group::connection_lifetime::captured_connection_after_cancel_preserves_arrange_arm_and_live_group ... ok
|
||||
test app::allocation_drag::tests::group::connection_lifetime::captured_connection_context_mismatches_preserve_the_current_session ... ok
|
||||
test app::allocation_drag::tests::group::connection_lifetime::captured_connection_current_restart_and_pair_commit_after_tool_boundary ... ok
|
||||
test app::allocation_drag::tests::group::group_arrange_work_and_performer_anchors_preserve_every_responsibility ... ok
|
||||
test app::allocation_drag::tests::group::group_assignment_split_rebuild_preserves_primary_and_commits_one_complete_candidate ... ok
|
||||
test app::allocation_drag::tests::group::group_out_and_back_assigns_at_zero_delta_then_true_noop_preserves_redo ... ok
|
||||
test app::allocation_drag::tests::group::group_selection_aba_and_old_press_cannot_retire_a_new_token ... ok
|
||||
test app::allocation_drag::tests::group::group_source_refusal_retains_primary_source_and_both_history_paths ... ok
|
||||
test app::allocation_drag::tests::group::idle_group_press_rejects_selection_generation_aba_with_unchanged_other_stamps ... ok
|
||||
test app::allocation_drag::tests::group::ordinary_off_grid_performer_out_and_back_is_noop_with_redo ... ok
|
||||
test app::allocation_drag::tests::group::route_lifetime::captured_route_context_fields_independently_reject_before_retirement ... ok
|
||||
test app::allocation_drag::tests::group::route_lifetime::current_route_commits_without_session_preserve_exact_route_history ... ok
|
||||
test app::allocation_drag::tests::group::route_lifetime::same_context_route_attachment_after_selection_preserves_arrange_arm_and_live_group ... ok
|
||||
test app::allocation_drag::tests::group::route_lifetime::same_context_route_commit_after_selection_preserves_arrange_arm_and_live_group ... ok
|
||||
test app::allocation_drag::tests::group::route_lifetime::stale_route_attachment_preserves_arrange_arm_and_live_group ... ok
|
||||
test app::allocation_drag::tests::group::route_lifetime::stale_route_commit_preserves_arrange_arm_and_live_group ... ok
|
||||
test app::allocation_drag::tests::group::unsupported_group_click_collapses_but_threshold_crossing_never_becomes_click ... ok
|
||||
test app::allocation_drag::tests::modal_roundtrip_equal_fit_undo_resize_and_focus_retire_press_before_replay ... ok
|
||||
test app::allocation_drag::tests::mounted_clean_stationary_selects_exact_work_without_revision_or_legacy_apply ... ok
|
||||
test app::allocation_drag::tests::mounted_dirty_port_press_cannot_fall_through_to_applying_port_selection ... ok
|
||||
test app::allocation_drag::tests::mounted_dirty_property_press_does_not_select_apply_or_destroy_draft ... ok
|
||||
test app::allocation_drag::tests::mounted_oa_sa_ordered_incoming_between_outgoing_are_exact_single_history_steps ... ok
|
||||
test app::allocation_drag::tests::mounted_role_only_outgoing_refusal_retains_placement_and_same_performer_creates_no_allocation ... ok
|
||||
test app::allocation_drag::tests::mounted_uniform_parent_moves_hidden_placements_and_assigns_only_leaves ... ok
|
||||
test app::allocation_drag::tests::navigation_union::old_chooser_envelopes_preserve_a_current_canvas_gesture ... ok
|
||||
test app::allocation_drag::tests::navigation_union::stale_gesture_packets_preserve_navigation_and_a_new_live_cancel_still_works ... ok
|
||||
test app::allocation_drag::tests::oa_sa_clipped_selected_header_refuses_preview_release_and_retries_without_partial_source_or_history ... ok
|
||||
test app::allocation_drag::tests::oa_sa_toolbar_identity_accepts_exact_receiver_and_collapsed_header_drag ... ok
|
||||
test app::allocation_drag::tests::outgoing_boundary_drop_clears_source_without_changing_other_placements ... ok
|
||||
test app::allocation_drag::tests::outgoing_clearance_collision_refuses_release_without_source_or_history_mutation ... ok
|
||||
test app::allocation_drag::tests::outgoing_clearance_preserves_source_enclosure_and_allocated_sibling ... ok
|
||||
test app::allocation_drag::tests::outside_release_is_cancel_and_does_not_remove_an_assignment ... ok
|
||||
test app::allocation_drag::tests::pending_behavior_draft_refuses_actual_work_press_and_equal_document_replacement_retires_packet ... ok
|
||||
test app::allocation_drag::tests::refused_motion_retains_last_scene_but_release_never_commits_it ... ok
|
||||
test app::allocation_drag::tests::release_uses_ordered_native_position_before_later_motion_in_same_batch ... ok
|
||||
test app::allocation_drag::tests::same_snapped_position_on_opposite_shared_border_rebuilds_semantics_even_if_layout_refuses ... ok
|
||||
test app::allocation_drag::tests::source_and_modal_admission_reject_prebuilt_clean_press_without_selecting ... ok
|
||||
test app::allocation_drag::tests::source_refusal_is_atomic_and_a_new_gesture_can_retry_against_live_revision ... ok
|
||||
test app::allocation_drag::tests::stale_packets_do_not_cancel_a_newer_token_and_each_release_is_consumed_once ... ok
|
||||
test app::allocation_drag::tests::toolbar_destination_names_transformed_target_whose_header_is_clipped ... ok
|
||||
test app::allocation_drag::tests::toolbar_status_leaves_transformed_top_edge_work_header_readable ... ok
|
||||
|
||||
test result: ok. 123 passed; 0 failed; 0 ignored; 0 measured; 1424 filtered out; finished in 54.86s
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
*.log -text
|
||||
*.rs -text
|
||||
@@ -1,153 +0,0 @@
|
||||
# Cold construction and pan alternatives
|
||||
|
||||
Follow-up to [the stationary-camera cache](README.md), for issue 44. Application
|
||||
source remains `336720559bcec511949caa8a42bee8a55c3822da`, with evidence checkpoint
|
||||
`2ca84676ee4dc30846208881a27d90a901482085`. These standalone experiments change no
|
||||
application code, dependencies, event handling or release. Native interaction
|
||||
remains paused; no result below establishes native FPS.
|
||||
|
||||
## Question and method
|
||||
|
||||
The existing cache removes repeated construction at identical camera inputs but
|
||||
misses during pan/zoom. Can simpler geometry or renderer-level translation remove
|
||||
that cost without changing the grid's appearance? Use the same unoptimized Iced
|
||||
0.14 WGPU dependencies and Windows environment recorded in `context.json`.
|
||||
Compile the new drivers with the same `rustc`/extern/linker arguments described
|
||||
in README, replacing the driver filename and output executable. No desktop window
|
||||
is created and no input is sent. Run `cold_grid_probe` with argument `20`; phase
|
||||
drivers take no arguments.
|
||||
|
||||
The cold driver compares original circles, one reusable origin circle transformed
|
||||
for each fill, one path per column, and direct 1.15-unit squares. Every candidate
|
||||
also constructs one origin-circle template; this small common overhead is retained.
|
||||
Three warmups precede 20 changing-pan samples. Construction ends before geometry
|
||||
destruction, rendering and readback. Percentiles use sorted index `ceil((n-1)*p)`.
|
||||
Fixed candidate ordering and uncontrolled OS load limit small timing comparisons.
|
||||
|
||||
## Cold construction results
|
||||
|
||||
`cold_grid_probe.rs` produces `grid-cold-01.log`. Median milliseconds:
|
||||
|
||||
| Canvas, zoom | Original | Reused circle | Column batch | Square |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| 1900x660, 1.0 | 61.0564 | 55.3975 | 54.3594 | 3.5527 |
|
||||
| 1900x660, 0.5 | 215.8919 | 162.3073 | 158.5968 | 7.5699 |
|
||||
| 1440x900, 1.0 | 60.1683 | 43.3860 | 60.8198 | 2.0340 |
|
||||
| 1440x900, 0.5 | 165.2049 | 133.3420 | 143.1489 | 6.8178 |
|
||||
|
||||
Both circle alternatives exactly match original pixels in all 12 sampled cases
|
||||
(four integer canvas/zoom pairs, three pan offsets, scale 1). This does not prove
|
||||
equivalence at fractional zoom, other DPI or other camera inputs. Squares match
|
||||
eight of those cases but differ at the third pan offset in each canvas/zoom pair:
|
||||
4,266–13,184 changed pixels, maximum channel difference 7. Their different shape
|
||||
and area are an explicit appearance change, not a pixel-preserving optimization.
|
||||
|
||||
Reusing circles and column batches leave substantial cold cost; the experiment
|
||||
does not justify adding them to production as the solution to low FPS. The square
|
||||
alternative is much faster in this scope, but is not adopted without evaluating
|
||||
its appearance and the actual interaction result. The previous all-grid single
|
||||
path experiment remains rejected for its much higher construction cost.
|
||||
|
||||
## Source explanation and independent review
|
||||
|
||||
A fresh read-only critic checked the probes and actual local Iced source. Each
|
||||
`Path::circle` allocates/builds a path and converts its arc into quadratic segments.
|
||||
Each WGPU `Frame::fill` allocates a boxed geometry builder and invokes Lyon fill
|
||||
tessellation. Same-color fills already share a solid mesh buffer: this is not
|
||||
one GPU draw call per dot. Frame translation transforms paths before tessellation,
|
||||
so path reuse alone does not reuse triangles.
|
||||
|
||||
Renderer-level `with_translation` can reuse cached triangles. Stock Canvas cannot
|
||||
attach a separate transform to each returned geometry; application adoption would
|
||||
need a small background widget or wrapper, with layout, clipping, lifecycle and
|
||||
interaction checks. Scaling the grid to handle zoom is unsuitable because the
|
||||
dot radius must remain fixed in screen units.
|
||||
|
||||
The critic also identified two important controls. Original opaque background
|
||||
geometry must not silently become screenshot-clear color: MSAA/alpha composition
|
||||
can change quantized pixels. Also, `iced_wgpu::triangle::Storage::prepare` rewrites
|
||||
cached mesh data when its transform changes. Construction/command-recording time
|
||||
therefore excludes real preparation/upload costs. The later probe measures a
|
||||
rendering/readback path separately; neither timer represents native presentation.
|
||||
|
||||
Primary sources: Iced 0.14 `iced_graphics/src/geometry/path{,/builder}.rs`,
|
||||
`iced_wgpu/src/{geometry,triangle}.rs`, `iced_core/src/renderer.rs`, and
|
||||
`iced_widget/src/canvas.rs`. The reviewer changed no files and did not execute the
|
||||
probes; execution and interpretation remain the parent's responsibility.
|
||||
|
||||
## Renderer-translated phase cache
|
||||
|
||||
`phase_grid_probe_v1.rs` / `grid-phase-01.log` cache zero-phase circles once for
|
||||
each size/spacing pair, translate by `pan.rem_euclid(step)`, and externally clip
|
||||
to the original viewport. The original nonnegative row/column indices and fixed
|
||||
screen-space radius are retained. Cache clip bounds include the radius margins.
|
||||
This first prototype uses screenshot clear color instead of opaque background
|
||||
geometry; its widespread one-level pixel differences are not accepted as parity.
|
||||
Its compiler emitted one duplicate Renderer-import warning, retained in the
|
||||
original driver. Later drivers remove that import and compile with `-D warnings`.
|
||||
|
||||
`phase_grid_opaque_probe.rs` / `grid-phase-opaque-01.log` add an oversized opaque
|
||||
background to the same cached geometry as the circles. Its bounds extend from
|
||||
`(-step,-step)` to `(width+2*step,height+2*step)`, covering the viewport at every
|
||||
positive remainder without adding extra dot rows or columns. This directly
|
||||
compares against the unchanged legacy painter. A proposed separate-background
|
||||
control was not executed; no result is claimed for it.
|
||||
|
||||
Of 75 comparisons (five size/zoom pairs, five offsets, three DPI scales), 73 are
|
||||
byte-exact. All 60 cases at integer canvas sizes and spacing 14 or 24 are exact.
|
||||
At 301.5x211.25, zoom 0.73, pan (-13.25,9.75), scale 1 changes 12 pixels by at
|
||||
most one channel level; scale 2 changes one pixel by at most six levels. These
|
||||
are failures of exact parity and are preserved, not hidden behind a tolerance.
|
||||
|
||||
The targeted `phase_grid_diagnostic.rs` / `grid-phase-diagnostic-01.log` locates
|
||||
all 12 scale-1 differences at the rightmost physical column x=301. The scale-2
|
||||
single-pixel difference does not recur. That diagnostic skips earlier large
|
||||
cases and their rendering/timing sequence: it does not exonerate the earlier
|
||||
failure or establish that every residual difference is at an edge.
|
||||
|
||||
The replay driver's first compilation failed with E0689 because the new control's
|
||||
`Vector::new(-13.25,9.75)` left its numeric type ambiguous before `rem_euclid`.
|
||||
An explicit `Vector<f32>` annotation corrects the experiment harness. No probe
|
||||
executed during that failed attempt; no application source changed.
|
||||
|
||||
`phase_grid_replay.rs` preserves the full opaque probe's rendering/timing sequence
|
||||
and adds coordinate diagnostics without extra renders until that sequence ends.
|
||||
`grid-phase-replay-01.log` reproduces both failures: the 12 right-edge pixels and
|
||||
one **interior** pixel at physical (149,263), where legacy RGBA is
|
||||
`[240,243,246,255]` and cached RGBA is `[246,248,251,255]`. The latter cannot be
|
||||
dismissed as viewport-edge rounding. Afterwards, all 12 same-renderer repeat
|
||||
controls (three repetitions of old/old and cached/cached at two scales) are
|
||||
byte-exact; the control cache builds once. This finite control does not establish
|
||||
universal determinism, but supports investigating differing geometry coverage
|
||||
instead of treating the reproduced pixel as random readback noise.
|
||||
|
||||
Cold construction plus command recording takes 68.42–173.72ms median for the four
|
||||
large viewports; cached translation takes 0.0008–0.0011ms. Each per-size/spacing
|
||||
cache builds exactly once through all pan comparisons and measurements. Zoom
|
||||
and resize through a single retained cache are not tested by these drivers.
|
||||
The labels containing `submit` mean renderer command recording, not GPU submission.
|
||||
|
||||
Two selected cases include construction, recording, backend preparation and GPU
|
||||
readback in a separate timer (two warmups, eight samples, sorted-index median):
|
||||
|
||||
| Canvas, zoom | Original p50 ms | Phase cache p50 ms |
|
||||
| --- | ---: | ---: |
|
||||
| 1900x660, 1.0 | 259.9265 | 212.6536 |
|
||||
| 1440x900, 0.5 | 337.2560 | 170.3780 |
|
||||
|
||||
These support a benefit within the headless path, but retain significant renderer
|
||||
and readback cost and variable outliers. They are not native frame times. Sample
|
||||
size, sequential ordering and uncontrolled machine load prohibit a precise
|
||||
general speedup claim. All driver/log hashes and execution mapping are in
|
||||
`cold-pan-context.json`.
|
||||
|
||||
The phase candidate removes the measured pan tessellation cost, but does not
|
||||
meet exact visual preservation. Do not adopt it as an invisible optimization.
|
||||
The small circle rewrites leave the main cost and squares change appearance.
|
||||
Further work should investigate the reproduced interior coverage difference or
|
||||
compare optimized dependency/build profiles before adding a custom widget.
|
||||
Any adopted rendering candidate must then verify a real mounted scene at a
|
||||
nonzero widget origin, foreground ordering, viewport clipping, pan/zoom/resize
|
||||
cache lifecycle and unchanged event behavior. Native interaction/FPS verification
|
||||
remains a separate required gate. No application implementation is changed by
|
||||
these experiments.
|
||||
@@ -1,180 +0,0 @@
|
||||
# Architecture background geometry cache
|
||||
|
||||
Scope: [issue 44](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/44).
|
||||
Production/test candidate: `636093aae67aa48fa1d35e403a205227e81d04de`, based on
|
||||
`c0908f101ac376afb8021e60e454e2a176e49d45`. The base includes the separately
|
||||
unaccepted outgoing-drag contribution for issue 40. This is a contribution
|
||||
experiment and repair, not an integrated release or full FPS acceptance.
|
||||
|
||||
## Question and evidence
|
||||
|
||||
The [preceding core probe](../architecture-perf-20260925/README.md) measured
|
||||
approximately 4.26ms median for preparation/projection/routing in an eight-card
|
||||
approximation. It did not reproduce the reported severe slowdown. Source review
|
||||
then identified repeated grid construction on every canvas redraw, despite the
|
||||
grid's independence from model edits, selection and pointer position.
|
||||
|
||||
The standalone headless Iced probe copies the former grid loop: background fill,
|
||||
then one radius-0.65 circle per grid point, with the same spacing, pan remainder
|
||||
and colors. It measures geometry construction independently from offscreen
|
||||
rendering/readback, using the actual compiled development-profile Iced renderer.
|
||||
It creates no desktop window and sends no input. Three warmups precede 20 samples
|
||||
per phase. The timer includes construction and returning geometry; returned
|
||||
geometry destruction occurs afterwards. Sorted index `ceil((n-1)*p)` supplies
|
||||
reported percentiles. Renderer startup is outside the timed phases.
|
||||
|
||||
`grid-performance-01.log`, produced by `grid_render_probe_v1.rs`, records:
|
||||
|
||||
| Canvas, zoom | Dots | Fresh geometry p50 ms | Cache retrieval p50 ms |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| 1900x660, 1.0 | 2,240 | 67.8095 | 0.0001 |
|
||||
| 1900x660, 0.5 | 6,528 | 192.5581 | 0.0001 |
|
||||
| 1440x900, 1.0 | 2,318 | 66.2206 | 0.0002 |
|
||||
| 1440x900, 0.5 | 6,695 | 219.1771 | 0.0002 |
|
||||
|
||||
Cold and warm cached grid pixels equal the uncached result byte for byte in all
|
||||
four cases. Cache misses remain comparable to fresh construction; caching does
|
||||
not improve continuously changing camera inputs. Offscreen screenshot/readback
|
||||
cost remains substantial and variable. It must not be converted to native FPS.
|
||||
|
||||
## Candidate comparison and chosen change
|
||||
|
||||
1. Rebuild each circle every draw: exact current appearance, but the measured
|
||||
repeated CPU cost alone is large in this development build.
|
||||
2. Batch circles into one path: keeps the same pixels in this experiment but is
|
||||
materially slower. `grid_render_probe.rs` and `grid-performance-02.log` retain
|
||||
this rejected candidate. Its construction medians range from 274.66ms to
|
||||
1,225.53ms, versus 63.82–178.24ms for the original loop in the same run.
|
||||
3. Cache background geometry while camera and size stay identical: selected.
|
||||
It preserves the original painter and removes its repeated construction from
|
||||
stationary-camera interactions. It adds a small local cache, not a second
|
||||
geometry implementation or a model-level cache.
|
||||
|
||||
`diagram/grid.rs` owns an Iced geometry cache and an exact raw `(zoom, pan)` key.
|
||||
Iced's cache checks exact frame size. Camera changes explicitly clear it. The
|
||||
colors and dot radius remain compile-time constants; future runtime styling
|
||||
must invalidate this cache too. The conservative key rebuilds after a whole
|
||||
grid-period pan and after zoom changes within the minimum-spacing clamp. No
|
||||
fractional coordinates are rounded or tolerance-compared.
|
||||
|
||||
Drawing order becomes `[cached background and dots, live diagram, warnings]`.
|
||||
The opaque background is in the first geometry, so it cannot cover the dots.
|
||||
Existing pan/zoom transforms still apply only to the live model geometry.
|
||||
The cache lives in the retained canvas State; model revisions and selections
|
||||
do not invalidate it. Existing focus/suspension tree resets cause a safe cold
|
||||
rebuild. No routing, model semantics, hit testing or gesture admission changes.
|
||||
|
||||
`production_grid_probe.rs` imports the actual candidate cache. Its exploratory
|
||||
run 03 was performed before the source commit and formatting; the raw log is
|
||||
retained as local precommit evidence, not relabeled as a binary from the commit.
|
||||
The same source is supplied for reproduction at the identified candidate.
|
||||
|
||||
The separately recompiled run 04 imports the committed `636093aa` cache and ran
|
||||
after the full desktop test process exited, before Clippy/build compilation.
|
||||
`grid-performance-04.log` records exact baseline/cached pixels and these medians:
|
||||
|
||||
| Canvas, zoom | Fresh geometry ms | Production cache hit ms | Changing-pan rebuild ms |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| 1900x660, 1.0 | 61.8281 | 0.0002 | 59.2995 |
|
||||
| 1900x660, 0.5 | 158.8603 | 0.0002 | 163.4359 |
|
||||
| 1440x900, 1.0 | 50.5385 | 0.0002 | 53.5840 |
|
||||
| 1440x900, 0.5 | 168.4092 | 0.0002 | 175.2457 |
|
||||
|
||||
These results support the same bounded conclusion as the exploratory trials.
|
||||
The run record preserves linked-library, driver and log hashes; timing noise
|
||||
and screenshot readback remain explicit limitations.
|
||||
Run 04 directly compares the production cache's cold pixels; its generic
|
||||
cold-and-warm marker refers to the Iced-cache control. Warm production-cache
|
||||
pixel parity is established separately by the committed grid tests.
|
||||
|
||||
## Verification and boundaries
|
||||
|
||||
Three committed headless tests check:
|
||||
|
||||
- Cache construction count through 18 exact camera/size changes, including
|
||||
negative/fractional pan, zero-pan zoom changes, the minimum-spacing transition,
|
||||
independent width/height resizing and return to previous dimensions.
|
||||
- Exact RGBA equality against an independent copy of the legacy loop and
|
||||
unsplit geometry. The scenes contain opaque cards, crossing edges, text,
|
||||
translucent overdraw, selection handles and a warning overlay. Foreground
|
||||
edits must visibly change pixels while retaining the background cache.
|
||||
- The actual `DiagramCanvas` with one retained State: selection and model
|
||||
revision changes must alter rendered pixels without rebuilding the grid;
|
||||
pan and resize must rebuild it. This is drawing/scene-invalidation evidence,
|
||||
not an event-driven native gesture test.
|
||||
|
||||
Fresh read-only independent review found no concrete cache-key, lifetime,
|
||||
ordering or test-oracle defect. It checked the corresponding Iced 0.14 cache
|
||||
and mesh-layer implementations. This review does not establish execution.
|
||||
At `636093aa`, `grid-tests-01.log` passes all three targeted tests in 2.97s.
|
||||
`grid-desktop-01.log` passes the full desktop suite in 362.14s: 1,543 passed,
|
||||
zero failed, seven existing manual/diagnostic tests ignored, none filtered out.
|
||||
All 1,550 distinct test names were counted; `context.json` lists the seven
|
||||
ignored cases and records raw log hashes. This is desktop regression evidence;
|
||||
it does not relabel the ignored checks as passing or establish native FPS.
|
||||
The [independent review](independent-review.md) states its exact scope.
|
||||
|
||||
The initial strict Clippy run (`grid-clippy-01.log`) failed on two uses of
|
||||
`chunks_exact(4)` in the new test helper. Test-only successor
|
||||
`336720559bcec511949caa8a42bee8a55c3822da` uses `as_chunks::<4>()`; it leaves
|
||||
production inputs unchanged. The original full-suite result remains associated
|
||||
with `636093aa`, not relabeled as execution at the successor.
|
||||
At the successor, strict all-target Clippy passes in 2m01s
|
||||
(`grid-clippy-02.log`) and the three affected grid tests pass again in 4.13s
|
||||
(`grid-tests-02.log`). No warning suppression or test exclusion was introduced.
|
||||
|
||||
The normal Windows GUI build at `33672055` also passes in 2m15s
|
||||
(`desktop-build-02.log`). The executable and required `libunwind.dll` are staged
|
||||
locally for later native verification, but this candidate has not been launched.
|
||||
The executable SHA-256 is
|
||||
`78548e93d4b57130ed507ed0f29eddbdbafb4721b38b0cd6d06101d5324114dd`;
|
||||
`context.json` records both staged file hashes. Staging is not publication of a
|
||||
new runnable release or native acceptance.
|
||||
|
||||
Remaining limits: the user's exact saved scene is unavailable; these results do
|
||||
not establish end-to-end native FPS. Continuous pan/zoom still pays the cold
|
||||
construction cost. Native desktop verification remains paused after user Escape.
|
||||
Neither issue 40 nor issue 44 is closed by this headless experiment. Linux/WSL
|
||||
remains deferred by the user. No published release has been replaced.
|
||||
|
||||
## Environment and reproduction
|
||||
|
||||
The subsequent [cold-construction and pan experiments](COLD-PAN.md) compare
|
||||
additional approaches, including a renderer-translated cache. They preserve
|
||||
their own pixel differences, timing limits and adoption gates; they do not
|
||||
change the production implementation verified above.
|
||||
|
||||
Windows 10.0.26200, Rust 1.98.1, x86_64-pc-windows-gnullvm, development profile
|
||||
with debug information disabled. The exposed CPU is Intel Core i7-10870H with
|
||||
4 cores/threads; Windows reports VMware SVGA 3D, driver 9.17.11.4. The headless
|
||||
renderer identifies itself as `wgpu`; the probe does not identify its selected
|
||||
adapter or equate it with the desktop presenter's adapter. Other OS workloads
|
||||
were not controlled, so measured outliers are preserved.
|
||||
|
||||
At the candidate source, the normal gates are:
|
||||
|
||||
```powershell
|
||||
cargo test --locked --offline -p oa-desktop diagram::grid::tests -- --test-threads=1
|
||||
cargo test --locked --offline -p oa-desktop -- --test-threads=1
|
||||
cargo clippy --locked --offline -p oa-desktop --all-targets -- -D warnings
|
||||
cargo build --locked --offline -p oa-desktop --bin arcadia-studio
|
||||
```
|
||||
|
||||
For the standalone probes, first build the desktop dependencies in that profile.
|
||||
Compile with `rustc --edition=2024 -C opt-level=0`, supplying `--extern iced=...`
|
||||
and `--extern tokio=...` for the matching rlibs, plus `-L dependency=...` for
|
||||
their directory. On this toolchain the link also needs `-C linker=clang.exe`
|
||||
and `-L native=.../windows_x86_64_gnullvm-0.52.6/lib`. The original inputs were
|
||||
`libiced-594f7d417d69ed72.rlib` and `libtokio-5e0ca96735e3073a.rlib` from
|
||||
`target/debug/deps`. Run the resulting executable with argument `20`.
|
||||
The production probe's relative module path requires compiling it at its
|
||||
versioned location. Keep linker and runtime DLLs on PATH.
|
||||
|
||||
The first experimental compile failed because the configured futures crate
|
||||
lacked its executor feature and an inherent screenshot method shadowed the
|
||||
Headless trait method. Using the existing Tokio runtime and explicitly calling
|
||||
`Headless::screenshot` corrected those driver errors. A second link failed for
|
||||
the Windows import library search path; `grid-build-02.log` retains that failure.
|
||||
Adding the native library path allowed subsequent probes to run. These failures
|
||||
occurred in the experimental driver, before execution; they are not passing
|
||||
application checks.
|
||||
@@ -1,51 +0,0 @@
|
||||
{
|
||||
"application_source": "336720559bcec511949caa8a42bee8a55c3822da",
|
||||
"base_evidence": "2ca84676ee4dc30846208881a27d90a901482085",
|
||||
"production_changes": false,
|
||||
"environment": "Same unoptimized Windows Iced/Tokio artifacts as context.json",
|
||||
"logs": [
|
||||
{
|
||||
"driver": "cold_grid_probe.rs",
|
||||
"exit_code": 0,
|
||||
"log": "grid-cold-01.log"
|
||||
},
|
||||
{
|
||||
"driver": "phase_grid_probe_v1.rs",
|
||||
"exit_code": 0,
|
||||
"compile_warning": "Unused duplicate Renderer trait import; retained in original driver",
|
||||
"log": "grid-phase-01.log"
|
||||
},
|
||||
{
|
||||
"driver": "phase_grid_opaque_probe.rs",
|
||||
"exit_code": 0,
|
||||
"compile_warnings_denied": true,
|
||||
"log": "grid-phase-opaque-01.log"
|
||||
},
|
||||
{
|
||||
"driver": "phase_grid_diagnostic.rs",
|
||||
"exit_code": 0,
|
||||
"compile_warnings_denied": true,
|
||||
"log": "grid-phase-diagnostic-01.log"
|
||||
},
|
||||
{
|
||||
"driver": "phase_grid_replay.rs",
|
||||
"log": "grid-phase-replay-01.log",
|
||||
"exit_code": 0,
|
||||
"compile_warnings_denied": true,
|
||||
"initial_compile": "E0689 ambiguous Vector scalar before rem_euclid, fixed with Vector<f32>; no execution on failed attempt"
|
||||
}
|
||||
],
|
||||
"sha256": {
|
||||
"cold_grid_probe.rs": "a4cc6543229ae4901d54061823a3b05ec6fe3b988f245df12898e013dbb93323",
|
||||
"phase_grid_probe_v1.rs": "c4293e8bb022fae5bffe67002a6f7c867549f2fa8a865b107f39070655bc57fc",
|
||||
"phase_grid_opaque_probe.rs": "43ac5be462ca31f9d1c49554291e08b60bac9de38face7138eaa850e4dcc0ab9",
|
||||
"phase_grid_diagnostic.rs": "8de3c980ac65eef4fcb32b1b121d14aca35b93c704c5b19a440ad9dc7042248c",
|
||||
"grid-cold-01.log": "ca5cac0be409bafcea2c154d42998cbd622c9e898892574bcd5a2df0c1a70886",
|
||||
"grid-phase-01.log": "e0b2e2a3e24d642e1b6c2b1b418f24c952399758159de051f7c1ba7efbb39c4a",
|
||||
"grid-phase-opaque-01.log": "2716cbeb589f4b23a48575680022231c0b94b56ba9c06ec1adbf9e3e6d07f7cd",
|
||||
"grid-phase-diagnostic-01.log": "1dbb0f745169b88c56a21683dba6797a3dbbe753e8dd742bc9ecc8ca24788886",
|
||||
"phase_grid_replay.rs": "96e62ba5d08f7e26d9705d3f90e3785151597b526293439345b59ed842de4971",
|
||||
"grid-phase-replay-01.log": "040034be1b8eb3d4fc010bbe34c5ded47a953b8295bcf2f9440507d1a149da1f"
|
||||
},
|
||||
"native_fps": "unverified"
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
//! Experimental cold-grid candidates; no window or input automation.
|
||||
use iced::advanced::{
|
||||
graphics::geometry::Renderer as _,
|
||||
renderer::{Headless, Renderer as _},
|
||||
};
|
||||
use iced::{
|
||||
Color, Point, Rectangle, Renderer, Size, Vector,
|
||||
widget::canvas::{Frame, Geometry, Path},
|
||||
};
|
||||
use std::{hint::black_box, time::Instant};
|
||||
const BG: Color = Color::from_rgb8(246, 248, 251);
|
||||
const DOT: Color = Color::from_rgb(0.86, 0.89, 0.91);
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum Candidate {
|
||||
Original,
|
||||
Template,
|
||||
Column,
|
||||
Square,
|
||||
}
|
||||
fn draw(r: &Renderer, size: Size, zoom: f32, pan: Vector, candidate: Candidate) -> Geometry {
|
||||
let mut f = Frame::new(r, size);
|
||||
f.fill_rectangle(Point::ORIGIN, size, BG);
|
||||
let step = (24. * zoom).max(14.);
|
||||
let ox = pan.x.rem_euclid(step);
|
||||
let oy = pan.y.rem_euclid(step);
|
||||
let circle = Path::circle(Point::ORIGIN, 0.65);
|
||||
for x in 0..(size.width / step) as i32 + 1 {
|
||||
if matches!(candidate, Candidate::Column) {
|
||||
let p = Path::new(|p| {
|
||||
for y in 0..(size.height / step) as i32 + 1 {
|
||||
p.circle(Point::new(ox + x as f32 * step, oy + y as f32 * step), 0.65);
|
||||
p.close();
|
||||
}
|
||||
});
|
||||
f.fill(&p, DOT);
|
||||
continue;
|
||||
}
|
||||
for y in 0..(size.height / step) as i32 + 1 {
|
||||
let center = Point::new(ox + x as f32 * step, oy + y as f32 * step);
|
||||
match candidate {
|
||||
Candidate::Original => f.fill(&Path::circle(center, 0.65), DOT),
|
||||
Candidate::Template => f.with_save(|f| {
|
||||
f.translate(Vector::new(center.x, center.y));
|
||||
f.fill(&circle, DOT);
|
||||
}),
|
||||
Candidate::Square => f.fill_rectangle(
|
||||
center - Vector::new(0.575, 0.575),
|
||||
Size::new(1.15, 1.15),
|
||||
DOT,
|
||||
),
|
||||
Candidate::Column => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
f.into_geometry()
|
||||
}
|
||||
fn pixels(r: &mut Renderer, size: Size, g: Geometry) -> Vec<u8> {
|
||||
r.reset(Rectangle::with_size(size));
|
||||
r.draw_geometry(g);
|
||||
Headless::screenshot(r, Size::new(size.width as u32, size.height as u32), 1., BG)
|
||||
}
|
||||
fn main() {
|
||||
let n = std::env::args()
|
||||
.nth(1)
|
||||
.map(|s| s.parse::<usize>().unwrap())
|
||||
.unwrap_or(20)
|
||||
.max(1);
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
let mut r = rt
|
||||
.block_on(<Renderer as Headless>::new(
|
||||
iced::Font::DEFAULT,
|
||||
16.into(),
|
||||
None,
|
||||
))
|
||||
.unwrap();
|
||||
println!(
|
||||
"renderer={} n={n}; construction only, unoptimized linked dependencies; NOT native FPS",
|
||||
r.name()
|
||||
);
|
||||
for (size, zoom) in [
|
||||
(Size::new(1900., 660.), 1.),
|
||||
(Size::new(1900., 660.), 0.5),
|
||||
(Size::new(1440., 900.), 1.),
|
||||
(Size::new(1440., 900.), 0.5),
|
||||
] {
|
||||
println!("size={}x{} zoom={zoom}", size.width, size.height);
|
||||
for candidate in [
|
||||
Candidate::Original,
|
||||
Candidate::Template,
|
||||
Candidate::Column,
|
||||
Candidate::Square,
|
||||
] {
|
||||
for pan in [
|
||||
Vector::new(-13.25, 9.75),
|
||||
Vector::new(0., 0.),
|
||||
Vector::new(6.125, -8.5),
|
||||
] {
|
||||
let base = draw(&r, size, zoom, pan, Candidate::Original);
|
||||
let base = pixels(&mut r, size, base);
|
||||
let test = draw(&r, size, zoom, pan, candidate);
|
||||
let test = pixels(&mut r, size, test);
|
||||
let changed = base
|
||||
.chunks_exact(4)
|
||||
.zip(test.chunks_exact(4))
|
||||
.filter(|(a, b)| a != b)
|
||||
.count();
|
||||
let max = base
|
||||
.iter()
|
||||
.zip(&test)
|
||||
.map(|(a, b)| a.abs_diff(*b))
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
println!(
|
||||
"{candidate:?} pan={},{} changed_pixels={changed} max_channel_delta={max}",
|
||||
pan.x, pan.y
|
||||
);
|
||||
}
|
||||
let mut ms = Vec::new();
|
||||
for i in 0..n + 3 {
|
||||
let pan = Vector::new(-13.25 + i as f32 * 0.25, 9.75);
|
||||
let now = Instant::now();
|
||||
let g = black_box(draw(&r, size, zoom, pan, candidate));
|
||||
let elapsed = now.elapsed().as_secs_f64() * 1000.;
|
||||
drop(g);
|
||||
if i >= 3 {
|
||||
ms.push(elapsed);
|
||||
}
|
||||
}
|
||||
ms.sort_by(f64::total_cmp);
|
||||
println!(
|
||||
"{candidate:?} changing_pan n={n} p50_ms={:.4} p95_ms={:.4} max_ms={:.4}",
|
||||
ms[((n - 1) as f64 * 0.5).ceil() as usize],
|
||||
ms[((n - 1) as f64 * 0.95).ceil() as usize],
|
||||
ms[n - 1]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
{
|
||||
"source": "636093aae67aa48fa1d35e403a205227e81d04de",
|
||||
"target": "x86_64-pc-windows-gnullvm",
|
||||
"rustc": "1.98.1",
|
||||
"rust_llvm": "22.1.8",
|
||||
"linker_clang": "23.1.1",
|
||||
"profile": "dev/test unoptimized debug=0",
|
||||
"incremental": false,
|
||||
"build_jobs": 1,
|
||||
"full_test_count": 1550,
|
||||
"passed": 1543,
|
||||
"ignored": 7,
|
||||
"failed": 0,
|
||||
"ignored_tests": [
|
||||
"app::source_rename::timing_tests::accepted_release1_prepare_commit_sync_and_confirm_timing",
|
||||
"app::source_rename::timing_tests::layout_timing_tests::accepted_release1_constructor_vs_reuse_through_next_highlight",
|
||||
"app::source_symbols::tests::source_symbols_100_modules_1000_definitions_10000_uses",
|
||||
"behavior::scenario::spacing::controller::timing_tests::saved_spacing_capture_prepare_20_lanes_1000_messages_3_views",
|
||||
"source_annotations::anchor_tests::final_multi_glyph_cluster_anchor_uses_logical_trailing_advance_edge",
|
||||
"source_annotations::anchor_tests::mixed_direction_omitted_separator_has_one_logical_predecessor_anchor",
|
||||
"source_annotations::performance_tests::annotation_projection_cost_distributions"
|
||||
],
|
||||
"log_sha256": {
|
||||
"grid-tests-01.log": "ed76ce90e9246dfddf1d3a96d6a1b91d0d0719701b1d09ddb639a1277c089612",
|
||||
"grid-desktop-01.log": "2000cf03685d6fde73c3dede90f84153a45c6ad0a5c2886a2bc09dda98ba63c3",
|
||||
"grid-performance-04.log": "c492aba0bbe600fa1c2a7f0ac52e5706d29cb67aca95fa3a31641ff7f90c2412",
|
||||
"grid-clippy-01.log": "2498a3d0be9738b8e43ae588c23d6e48f531fdbfc5f47d2f79c1a2f0f1b5bdc1",
|
||||
"grid-clippy-02.log": "69de93cce0c53f37f3c82eb28a28c1610548b30fec4680f02b9b7b118606040f",
|
||||
"grid-tests-02.log": "54c1d72402310be8b3026886dfda0339aa1a81c76b8165c40c97ea075524bff2",
|
||||
"desktop-build-02.log": "e39d1eaaceb8a5805a486c204d6eca7e1ce2b9b39b232e770ab0ad33cbe0e860"
|
||||
},
|
||||
"native_acceptance": "unverified",
|
||||
"source_clean_after_full_test": true,
|
||||
"timing_run": {
|
||||
"source": "636093aae67aa48fa1d35e403a205227e81d04de",
|
||||
"samples": 20,
|
||||
"warmups": 3,
|
||||
"probe_exe_sha256": "79c2e0b64cfacf24b7ee10d8690969d961753945f45d16a743ea750b9eb049d0",
|
||||
"probe_source_sha256": "e6efe1e510164444912d05a96e49c55d791b7046102013458a5ad94bd474a6cd",
|
||||
"grid_git_blob_sha256": "5e7c54d1b4da2e2c082f8ef011a3755f8808fcd67f1babf3be5448cd94240e6c",
|
||||
"iced_rlib_sha256": "9fec4bea1a5ac7e1ae75d786b76e3dbe97a6f63b30d11b6753f268c2ad9efbc6",
|
||||
"tokio_rlib_sha256": "78587ae94762065914b25065c85d9cb66106b71619ec7b65f5e16cdeb689abb9"
|
||||
},
|
||||
"test_only_successor": {
|
||||
"source": "336720559bcec511949caa8a42bee8a55c3822da",
|
||||
"change": "chunks_exact(4) replaced by as_chunks::<4>() in the pixel test helper",
|
||||
"targeted_passed": 3,
|
||||
"targeted_failed": 0,
|
||||
"targeted_filtered": 1547,
|
||||
"strict_clippy": "passed",
|
||||
"normal_build": "passed"
|
||||
},
|
||||
"staged_candidate": {
|
||||
"source": "336720559bcec511949caa8a42bee8a55c3822da",
|
||||
"launched": false,
|
||||
"files_sha256": {
|
||||
"arcadia-studio.exe": "78548e93d4b57130ed507ed0f29eddbdbafb4721b38b0cd6d06101d5324114dd",
|
||||
"libunwind.dll": "aa282fa47bb2c4e210cea5e470cdbdca58568f0be0bc73e404d86ab9cd95ef6a"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
Compiling oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop)
|
||||
Finished `dev` profile [unoptimized] target(s) in 2m 15s
|
||||
File diff suppressed because one or more lines are too long
@@ -1,25 +0,0 @@
|
||||
Checking oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop)
|
||||
error: using `chunks_exact` with a constant chunk size
|
||||
--> crates\oa-desktop\src\diagram\grid\tests.rs:231:10
|
||||
|
|
||||
231 | .chunks_exact(4)
|
||||
| ^^^^^^^^^^^^^^^ help: consider using `as_chunks` instead: `as_chunks::<4>().0.iter()`
|
||||
|
|
||||
= help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#chunks_exact_to_as_chunks
|
||||
= note: `-D clippy::chunks-exact-to-as-chunks` implied by `-D warnings`
|
||||
= help: to override `-D warnings` add `#[allow(clippy::chunks_exact_to_as_chunks)]`
|
||||
|
||||
error: using `chunks_exact` with a constant chunk size
|
||||
--> crates\oa-desktop\src\diagram\grid\tests.rs:232:21
|
||||
|
|
||||
232 | .zip(actual.chunks_exact(4))
|
||||
| ^^^^^^^^^^^^^^^
|
||||
|
|
||||
help: consider using `as_chunks::<4>()` instead
|
||||
--> crates\oa-desktop\src\diagram\grid\tests.rs:232:21
|
||||
|
|
||||
232 | .zip(actual.chunks_exact(4))
|
||||
| ^^^^^^^^^^^^^^^
|
||||
= help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#chunks_exact_to_as_chunks
|
||||
|
||||
error: could not compile `oa-desktop` (bin "arcadia-studio" test) due to 2 previous errors
|
||||
@@ -1,2 +0,0 @@
|
||||
Checking oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop)
|
||||
Finished `dev` profile [unoptimized] target(s) in 2m 01s
|
||||
@@ -1,69 +0,0 @@
|
||||
renderer=wgpu n=20; construction only, unoptimized linked dependencies; NOT native FPS
|
||||
size=1900x660 zoom=1
|
||||
Original pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
Original pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
Original pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
Original changing_pan n=20 p50_ms=61.0564 p95_ms=111.2137 max_ms=111.2137
|
||||
Template pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
Template pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
Template pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
Template changing_pan n=20 p50_ms=55.3975 p95_ms=73.8790 max_ms=73.8790
|
||||
Column pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
Column pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
Column pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
Column changing_pan n=20 p50_ms=54.3594 p95_ms=69.6607 max_ms=69.6607
|
||||
Square pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
Square pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
Square pan=6.125,-8.5 changed_pixels=4266 max_channel_delta=7
|
||||
Square changing_pan n=20 p50_ms=3.5527 p95_ms=5.6911 max_ms=5.6911
|
||||
size=1900x660 zoom=0.5
|
||||
Original pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
Original pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
Original pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
Original changing_pan n=20 p50_ms=215.8919 p95_ms=253.9666 max_ms=253.9666
|
||||
Template pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
Template pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
Template pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
Template changing_pan n=20 p50_ms=162.3073 p95_ms=212.2833 max_ms=212.2833
|
||||
Column pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
Column pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
Column pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
Column changing_pan n=20 p50_ms=158.5968 p95_ms=449.6066 max_ms=449.6066
|
||||
Square pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
Square pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
Square pan=6.125,-8.5 changed_pixels=12784 max_channel_delta=7
|
||||
Square changing_pan n=20 p50_ms=7.5699 p95_ms=13.3416 max_ms=13.3416
|
||||
size=1440x900 zoom=1
|
||||
Original pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
Original pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
Original pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
Original changing_pan n=20 p50_ms=60.1683 p95_ms=88.4967 max_ms=88.4967
|
||||
Template pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
Template pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
Template pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
Template changing_pan n=20 p50_ms=43.3860 p95_ms=53.1866 max_ms=53.1866
|
||||
Column pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
Column pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
Column pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
Column changing_pan n=20 p50_ms=60.8198 p95_ms=80.8932 max_ms=80.8932
|
||||
Square pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
Square pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
Square pan=6.125,-8.5 changed_pixels=4440 max_channel_delta=7
|
||||
Square changing_pan n=20 p50_ms=2.0340 p95_ms=4.0620 max_ms=4.0620
|
||||
size=1440x900 zoom=0.5
|
||||
Original pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
Original pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
Original pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
Original changing_pan n=20 p50_ms=165.2049 p95_ms=203.5331 max_ms=203.5331
|
||||
Template pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
Template pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
Template pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
Template changing_pan n=20 p50_ms=133.3420 p95_ms=143.9790 max_ms=143.9790
|
||||
Column pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
Column pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
Column pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
Column changing_pan n=20 p50_ms=143.1489 p95_ms=155.3680 max_ms=155.3680
|
||||
Square pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
Square pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
Square pan=6.125,-8.5 changed_pixels=13184 max_channel_delta=7
|
||||
Square changing_pan n=20 p50_ms=6.8178 p95_ms=10.8775 max_ms=10.8775
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,33 +0,0 @@
|
||||
renderer=wgpu samples=20; screenshot includes offscreen rendering + GPU readback, NOT native frame presentation
|
||||
|
||||
size=1900x660 zoom=1 dots=2240
|
||||
pixel_parity=cold_and_warm_exact
|
||||
fresh_geometry: n=20 p50_ms=67.8095 p95_ms=77.2227 max_ms=77.2227
|
||||
cached_geometry: n=20 p50_ms=0.0001 p95_ms=0.0002 max_ms=0.0002
|
||||
fresh_geometry_and_screenshot: n=20 p50_ms=171.4916 p95_ms=231.2029 max_ms=231.2029
|
||||
cached_geometry_and_screenshot: n=20 p50_ms=99.1916 p95_ms=125.0517 max_ms=125.0517
|
||||
cache_forced_miss_geometry: n=20 p50_ms=65.6279 p95_ms=75.6531 max_ms=75.6531
|
||||
|
||||
size=1900x660 zoom=0.5 dots=6528
|
||||
pixel_parity=cold_and_warm_exact
|
||||
fresh_geometry: n=20 p50_ms=192.5581 p95_ms=237.9956 max_ms=237.9956
|
||||
cached_geometry: n=20 p50_ms=0.0001 p95_ms=0.0009 max_ms=0.0009
|
||||
fresh_geometry_and_screenshot: n=20 p50_ms=328.0257 p95_ms=498.0908 max_ms=498.0908
|
||||
cached_geometry_and_screenshot: n=20 p50_ms=108.5102 p95_ms=119.6021 max_ms=119.6021
|
||||
cache_forced_miss_geometry: n=20 p50_ms=183.2740 p95_ms=212.6681 max_ms=212.6681
|
||||
|
||||
size=1440x900 zoom=1 dots=2318
|
||||
pixel_parity=cold_and_warm_exact
|
||||
fresh_geometry: n=20 p50_ms=66.2206 p95_ms=89.5746 max_ms=89.5746
|
||||
cached_geometry: n=20 p50_ms=0.0002 p95_ms=0.0010 max_ms=0.0010
|
||||
fresh_geometry_and_screenshot: n=20 p50_ms=194.1484 p95_ms=445.2525 max_ms=445.2525
|
||||
cached_geometry_and_screenshot: n=20 p50_ms=113.8430 p95_ms=152.8658 max_ms=152.8658
|
||||
cache_forced_miss_geometry: n=20 p50_ms=81.6705 p95_ms=200.7735 max_ms=200.7735
|
||||
|
||||
size=1440x900 zoom=0.5 dots=6695
|
||||
pixel_parity=cold_and_warm_exact
|
||||
fresh_geometry: n=20 p50_ms=219.1771 p95_ms=270.8434 max_ms=270.8434
|
||||
cached_geometry: n=20 p50_ms=0.0002 p95_ms=0.0003 max_ms=0.0003
|
||||
fresh_geometry_and_screenshot: n=20 p50_ms=387.7051 p95_ms=600.4906 max_ms=600.4906
|
||||
cached_geometry_and_screenshot: n=20 p50_ms=119.9417 p95_ms=157.2978 max_ms=157.2978
|
||||
cache_forced_miss_geometry: n=20 p50_ms=193.8187 p95_ms=222.6229 max_ms=222.6229
|
||||
@@ -1,41 +0,0 @@
|
||||
renderer=wgpu samples=20; screenshot includes offscreen rendering + GPU readback, NOT native frame presentation
|
||||
|
||||
size=1900x660 zoom=1 dots=2240
|
||||
batched_rgba_byte_differences=0
|
||||
pixel_parity=cold_and_warm_exact
|
||||
fresh_geometry: n=20 p50_ms=63.8161 p95_ms=74.3558 max_ms=74.3558
|
||||
batched_geometry: n=20 p50_ms=280.9291 p95_ms=361.9218 max_ms=361.9218
|
||||
cached_geometry: n=20 p50_ms=0.0001 p95_ms=0.0004 max_ms=0.0004
|
||||
fresh_geometry_and_screenshot: n=20 p50_ms=203.0484 p95_ms=314.9968 max_ms=314.9968
|
||||
cached_geometry_and_screenshot: n=20 p50_ms=109.2162 p95_ms=470.8047 max_ms=470.8047
|
||||
cache_forced_miss_geometry: n=20 p50_ms=73.1544 p95_ms=112.9079 max_ms=112.9079
|
||||
|
||||
size=1900x660 zoom=0.5 dots=6528
|
||||
batched_rgba_byte_differences=0
|
||||
pixel_parity=cold_and_warm_exact
|
||||
fresh_geometry: n=20 p50_ms=178.2363 p95_ms=204.1913 max_ms=204.1913
|
||||
batched_geometry: n=20 p50_ms=1225.5328 p95_ms=1791.5758 max_ms=1791.5758
|
||||
cached_geometry: n=20 p50_ms=0.0001 p95_ms=0.0008 max_ms=0.0008
|
||||
fresh_geometry_and_screenshot: n=20 p50_ms=281.4035 p95_ms=308.5933 max_ms=308.5933
|
||||
cached_geometry_and_screenshot: n=20 p50_ms=100.8220 p95_ms=114.5026 max_ms=114.5026
|
||||
cache_forced_miss_geometry: n=20 p50_ms=179.6717 p95_ms=319.2162 max_ms=319.2162
|
||||
|
||||
size=1440x900 zoom=1 dots=2318
|
||||
batched_rgba_byte_differences=0
|
||||
pixel_parity=cold_and_warm_exact
|
||||
fresh_geometry: n=20 p50_ms=67.1604 p95_ms=76.3298 max_ms=76.3298
|
||||
batched_geometry: n=20 p50_ms=274.6607 p95_ms=551.4707 max_ms=551.4707
|
||||
cached_geometry: n=20 p50_ms=0.0002 p95_ms=0.0003 max_ms=0.0003
|
||||
fresh_geometry_and_screenshot: n=20 p50_ms=168.4817 p95_ms=294.9456 max_ms=294.9456
|
||||
cached_geometry_and_screenshot: n=20 p50_ms=92.0521 p95_ms=105.3303 max_ms=105.3303
|
||||
cache_forced_miss_geometry: n=20 p50_ms=70.1733 p95_ms=83.6053 max_ms=83.6053
|
||||
|
||||
size=1440x900 zoom=0.5 dots=6695
|
||||
batched_rgba_byte_differences=0
|
||||
pixel_parity=cold_and_warm_exact
|
||||
fresh_geometry: n=20 p50_ms=177.7526 p95_ms=205.8862 max_ms=205.8862
|
||||
batched_geometry: n=20 p50_ms=1012.6393 p95_ms=1103.8729 max_ms=1103.8729
|
||||
cached_geometry: n=20 p50_ms=0.0002 p95_ms=0.0006 max_ms=0.0006
|
||||
fresh_geometry_and_screenshot: n=20 p50_ms=293.3138 p95_ms=315.0118 max_ms=315.0118
|
||||
cached_geometry_and_screenshot: n=20 p50_ms=110.6309 p95_ms=127.6823 max_ms=127.6823
|
||||
cache_forced_miss_geometry: n=20 p50_ms=195.3297 p95_ms=327.8771 max_ms=327.8771
|
||||
@@ -1,45 +0,0 @@
|
||||
renderer=wgpu samples=20; screenshot includes offscreen rendering + GPU readback, NOT native frame presentation
|
||||
|
||||
size=1900x660 zoom=1 dots=2240
|
||||
batched_rgba_byte_differences=0
|
||||
pixel_parity=cold_and_warm_exact
|
||||
fresh_geometry: n=20 p50_ms=70.2238 p95_ms=101.8816 max_ms=101.8816
|
||||
production_cached_geometry: n=20 p50_ms=0.0002 p95_ms=0.0005 max_ms=0.0005
|
||||
production_changing_pan_geometry: n=20 p50_ms=74.2265 p95_ms=150.4436 max_ms=150.4436
|
||||
cached_geometry: n=20 p50_ms=0.0001 p95_ms=0.0002 max_ms=0.0002
|
||||
fresh_geometry_and_screenshot: n=20 p50_ms=179.3216 p95_ms=272.4518 max_ms=272.4518
|
||||
cached_geometry_and_screenshot: n=20 p50_ms=108.8949 p95_ms=138.2430 max_ms=138.2430
|
||||
cache_forced_miss_geometry: n=20 p50_ms=73.2982 p95_ms=229.4395 max_ms=229.4395
|
||||
|
||||
size=1900x660 zoom=0.5 dots=6528
|
||||
batched_rgba_byte_differences=0
|
||||
pixel_parity=cold_and_warm_exact
|
||||
fresh_geometry: n=20 p50_ms=190.5914 p95_ms=245.6332 max_ms=245.6332
|
||||
production_cached_geometry: n=20 p50_ms=0.0002 p95_ms=0.0004 max_ms=0.0004
|
||||
production_changing_pan_geometry: n=20 p50_ms=200.1888 p95_ms=243.7068 max_ms=243.7068
|
||||
cached_geometry: n=20 p50_ms=0.0002 p95_ms=0.0002 max_ms=0.0002
|
||||
fresh_geometry_and_screenshot: n=20 p50_ms=356.6151 p95_ms=438.3689 max_ms=438.3689
|
||||
cached_geometry_and_screenshot: n=20 p50_ms=151.2161 p95_ms=236.6296 max_ms=236.6296
|
||||
cache_forced_miss_geometry: n=20 p50_ms=198.4648 p95_ms=312.0322 max_ms=312.0322
|
||||
|
||||
size=1440x900 zoom=1 dots=2318
|
||||
batched_rgba_byte_differences=0
|
||||
pixel_parity=cold_and_warm_exact
|
||||
fresh_geometry: n=20 p50_ms=83.2017 p95_ms=284.6453 max_ms=284.6453
|
||||
production_cached_geometry: n=20 p50_ms=0.0002 p95_ms=0.0010 max_ms=0.0010
|
||||
production_changing_pan_geometry: n=20 p50_ms=61.6783 p95_ms=79.8293 max_ms=79.8293
|
||||
cached_geometry: n=20 p50_ms=0.0002 p95_ms=0.0003 max_ms=0.0003
|
||||
fresh_geometry_and_screenshot: n=20 p50_ms=168.7804 p95_ms=206.0726 max_ms=206.0726
|
||||
cached_geometry_and_screenshot: n=20 p50_ms=114.7579 p95_ms=130.0840 max_ms=130.0840
|
||||
cache_forced_miss_geometry: n=20 p50_ms=103.6019 p95_ms=215.1691 max_ms=215.1691
|
||||
|
||||
size=1440x900 zoom=0.5 dots=6695
|
||||
batched_rgba_byte_differences=0
|
||||
pixel_parity=cold_and_warm_exact
|
||||
fresh_geometry: n=20 p50_ms=202.9283 p95_ms=247.2353 max_ms=247.2353
|
||||
production_cached_geometry: n=20 p50_ms=0.0002 p95_ms=0.0004 max_ms=0.0004
|
||||
production_changing_pan_geometry: n=20 p50_ms=223.5437 p95_ms=243.7359 max_ms=243.7359
|
||||
cached_geometry: n=20 p50_ms=0.0001 p95_ms=0.0002 max_ms=0.0002
|
||||
fresh_geometry_and_screenshot: n=20 p50_ms=299.4320 p95_ms=359.2924 max_ms=359.2924
|
||||
cached_geometry_and_screenshot: n=20 p50_ms=113.6960 p95_ms=180.7261 max_ms=180.7261
|
||||
cache_forced_miss_geometry: n=20 p50_ms=191.9370 p95_ms=294.2203 max_ms=294.2203
|
||||
@@ -1,45 +0,0 @@
|
||||
renderer=wgpu samples=20; screenshot includes offscreen rendering + GPU readback, NOT native frame presentation
|
||||
|
||||
size=1900x660 zoom=1 dots=2240
|
||||
batched_rgba_byte_differences=0
|
||||
pixel_parity=cold_and_warm_exact
|
||||
fresh_geometry: n=20 p50_ms=61.8281 p95_ms=68.9849 max_ms=68.9849
|
||||
production_cached_geometry: n=20 p50_ms=0.0002 p95_ms=0.0002 max_ms=0.0002
|
||||
production_changing_pan_geometry: n=20 p50_ms=59.2995 p95_ms=69.6670 max_ms=69.6670
|
||||
cached_geometry: n=20 p50_ms=0.0001 p95_ms=0.0029 max_ms=0.0029
|
||||
fresh_geometry_and_screenshot: n=20 p50_ms=136.3939 p95_ms=160.0453 max_ms=160.0453
|
||||
cached_geometry_and_screenshot: n=20 p50_ms=82.0448 p95_ms=105.5853 max_ms=105.5853
|
||||
cache_forced_miss_geometry: n=20 p50_ms=52.1529 p95_ms=58.0832 max_ms=58.0832
|
||||
|
||||
size=1900x660 zoom=0.5 dots=6528
|
||||
batched_rgba_byte_differences=0
|
||||
pixel_parity=cold_and_warm_exact
|
||||
fresh_geometry: n=20 p50_ms=158.8603 p95_ms=169.1878 max_ms=169.1878
|
||||
production_cached_geometry: n=20 p50_ms=0.0002 p95_ms=0.0002 max_ms=0.0002
|
||||
production_changing_pan_geometry: n=20 p50_ms=163.4359 p95_ms=262.5349 max_ms=262.5349
|
||||
cached_geometry: n=20 p50_ms=0.0002 p95_ms=0.0002 max_ms=0.0002
|
||||
fresh_geometry_and_screenshot: n=20 p50_ms=226.3661 p95_ms=247.0635 max_ms=247.0635
|
||||
cached_geometry_and_screenshot: n=20 p50_ms=78.5761 p95_ms=99.7537 max_ms=99.7537
|
||||
cache_forced_miss_geometry: n=20 p50_ms=154.7299 p95_ms=177.8883 max_ms=177.8883
|
||||
|
||||
size=1440x900 zoom=1 dots=2318
|
||||
batched_rgba_byte_differences=0
|
||||
pixel_parity=cold_and_warm_exact
|
||||
fresh_geometry: n=20 p50_ms=50.5385 p95_ms=57.4277 max_ms=57.4277
|
||||
production_cached_geometry: n=20 p50_ms=0.0002 p95_ms=0.0002 max_ms=0.0002
|
||||
production_changing_pan_geometry: n=20 p50_ms=53.5840 p95_ms=60.8569 max_ms=60.8569
|
||||
cached_geometry: n=20 p50_ms=0.0001 p95_ms=0.0005 max_ms=0.0005
|
||||
fresh_geometry_and_screenshot: n=20 p50_ms=124.3827 p95_ms=140.3617 max_ms=140.3617
|
||||
cached_geometry_and_screenshot: n=20 p50_ms=75.7445 p95_ms=97.7246 max_ms=97.7246
|
||||
cache_forced_miss_geometry: n=20 p50_ms=54.7114 p95_ms=61.8326 max_ms=61.8326
|
||||
|
||||
size=1440x900 zoom=0.5 dots=6695
|
||||
batched_rgba_byte_differences=0
|
||||
pixel_parity=cold_and_warm_exact
|
||||
fresh_geometry: n=20 p50_ms=168.4092 p95_ms=186.4184 max_ms=186.4184
|
||||
production_cached_geometry: n=20 p50_ms=0.0002 p95_ms=0.0003 max_ms=0.0003
|
||||
production_changing_pan_geometry: n=20 p50_ms=175.2457 p95_ms=205.4313 max_ms=205.4313
|
||||
cached_geometry: n=20 p50_ms=0.0001 p95_ms=0.0002 max_ms=0.0002
|
||||
fresh_geometry_and_screenshot: n=20 p50_ms=253.4003 p95_ms=420.1746 max_ms=420.1746
|
||||
cached_geometry_and_screenshot: n=20 p50_ms=85.1261 p95_ms=107.0600 max_ms=107.0600
|
||||
cache_forced_miss_geometry: n=20 p50_ms=155.4512 p95_ms=163.8325 max_ms=163.8325
|
||||
@@ -1,96 +0,0 @@
|
||||
renderer=wgpu; phase-cache prototype; NOT native FPS
|
||||
size=1900x660 zoom=1 step=24
|
||||
scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-13.25,9.75 changed_pixels=2212 max_channel_delta=1
|
||||
scale=1 pan=6.125,-8.5 changed_pixels=2133 max_channel_delta=1
|
||||
scale=1 pan=23.875,23.75 changed_pixels=2133 max_channel_delta=1
|
||||
scale=1 pan=-0.125,0.125 changed_pixels=4424 max_channel_delta=1
|
||||
scale=1.25 pan=0,0 changed_pixels=8745 max_channel_delta=1
|
||||
scale=1.25 pan=-13.25,9.75 changed_pixels=2212 max_channel_delta=1
|
||||
scale=1.25 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=23.875,23.75 changed_pixels=4266 max_channel_delta=1
|
||||
scale=1.25 pan=-0.125,0.125 changed_pixels=6557 max_channel_delta=1
|
||||
scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-13.25,9.75 changed_pixels=8848 max_channel_delta=1
|
||||
scale=2 pan=6.125,-8.5 changed_pixels=4266 max_channel_delta=1
|
||||
scale=2 pan=23.875,23.75 changed_pixels=10665 max_channel_delta=1
|
||||
scale=2 pan=-0.125,0.125 changed_pixels=6478 max_channel_delta=1
|
||||
cached=false construct_and_submit changing_pan n=20 p50_ms=66.6997 p95_ms=77.3888 max_ms=77.3888
|
||||
cached=true construct_and_submit changing_pan n=20 p50_ms=0.0010 p95_ms=0.0017 max_ms=0.0017
|
||||
cache_builds=1
|
||||
size=1900x660 zoom=0.5 step=14
|
||||
scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-13.25,9.75 changed_pixels=6392 max_channel_delta=1
|
||||
scale=1 pan=6.125,-8.5 changed_pixels=6392 max_channel_delta=1
|
||||
scale=1 pan=13.875,13.75 changed_pixels=6345 max_channel_delta=1
|
||||
scale=1 pan=-0.125,0.125 changed_pixels=12960 max_channel_delta=1
|
||||
scale=1.25 pan=0,0 changed_pixels=12781 max_channel_delta=1
|
||||
scale=1.25 pan=-13.25,9.75 changed_pixels=9588 max_channel_delta=1
|
||||
scale=1.25 pan=6.125,-8.5 changed_pixels=11288 max_channel_delta=1
|
||||
scale=1.25 pan=13.875,13.75 changed_pixels=9494 max_channel_delta=1
|
||||
scale=1.25 pan=-0.125,0.125 changed_pixels=12801 max_channel_delta=1
|
||||
scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-13.25,9.75 changed_pixels=25568 max_channel_delta=1
|
||||
scale=2 pan=6.125,-8.5 changed_pixels=12784 max_channel_delta=1
|
||||
scale=2 pan=13.875,13.75 changed_pixels=31725 max_channel_delta=1
|
||||
scale=2 pan=-0.125,0.125 changed_pixels=19170 max_channel_delta=1
|
||||
cached=false construct_and_submit changing_pan n=20 p50_ms=189.4570 p95_ms=221.8240 max_ms=221.8240
|
||||
cached=true construct_and_submit changing_pan n=20 p50_ms=0.0007 p95_ms=0.0008 max_ms=0.0008
|
||||
cache_builds=1
|
||||
size=1440x900 zoom=1 step=24
|
||||
scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-13.25,9.75 changed_pixels=2280 max_channel_delta=1
|
||||
scale=1 pan=6.125,-8.5 changed_pixels=2220 max_channel_delta=1
|
||||
scale=1 pan=23.875,23.75 changed_pixels=2220 max_channel_delta=1
|
||||
scale=1 pan=-0.125,0.125 changed_pixels=4522 max_channel_delta=1
|
||||
scale=1.25 pan=0,0 changed_pixels=9000 max_channel_delta=1
|
||||
scale=1.25 pan=-13.25,9.75 changed_pixels=2280 max_channel_delta=1
|
||||
scale=1.25 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=23.875,23.75 changed_pixels=4403 max_channel_delta=1
|
||||
scale=1.25 pan=-0.125,0.125 changed_pixels=6742 max_channel_delta=1
|
||||
scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-13.25,9.75 changed_pixels=9120 max_channel_delta=1
|
||||
scale=2 pan=6.125,-8.5 changed_pixels=4440 max_channel_delta=1
|
||||
scale=2 pan=23.875,23.75 changed_pixels=11026 max_channel_delta=1
|
||||
scale=2 pan=-0.125,0.125 changed_pixels=6683 max_channel_delta=1
|
||||
cached=false construct_and_submit changing_pan n=20 p50_ms=66.8644 p95_ms=93.9216 max_ms=93.9216
|
||||
cached=true construct_and_submit changing_pan n=20 p50_ms=0.0008 p95_ms=0.0008 max_ms=0.0008
|
||||
cache_builds=1
|
||||
size=1440x900 zoom=0.5 step=14
|
||||
scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-13.25,9.75 changed_pixels=6592 max_channel_delta=1
|
||||
scale=1 pan=6.125,-8.5 changed_pixels=6592 max_channel_delta=1
|
||||
scale=1 pan=13.875,13.75 changed_pixels=6528 max_channel_delta=1
|
||||
scale=1 pan=-0.125,0.125 changed_pixels=13260 max_channel_delta=1
|
||||
scale=1.25 pan=0,0 changed_pixels=13306 max_channel_delta=1
|
||||
scale=1.25 pan=-13.25,9.75 changed_pixels=9952 max_channel_delta=1
|
||||
scale=1.25 pan=6.125,-8.5 changed_pixels=11488 max_channel_delta=1
|
||||
scale=1.25 pan=13.875,13.75 changed_pixels=9792 max_channel_delta=1
|
||||
scale=1.25 pan=-0.125,0.125 changed_pixels=13209 max_channel_delta=1
|
||||
scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-13.25,9.75 changed_pixels=26368 max_channel_delta=1
|
||||
scale=2 pan=6.125,-8.5 changed_pixels=13184 max_channel_delta=1
|
||||
scale=2 pan=13.875,13.75 changed_pixels=32640 max_channel_delta=1
|
||||
scale=2 pan=-0.125,0.125 changed_pixels=19686 max_channel_delta=1
|
||||
cached=false construct_and_submit changing_pan n=20 p50_ms=202.5700 p95_ms=482.7433 max_ms=482.7433
|
||||
cached=true construct_and_submit changing_pan n=20 p50_ms=0.0011 p95_ms=0.0041 max_ms=0.0041
|
||||
cache_builds=1
|
||||
size=301.5x211.25 zoom=0.73 step=17.52
|
||||
scale=1 pan=0,0 changed_pixels=263 max_channel_delta=1
|
||||
scale=1 pan=-13.25,9.75 changed_pixels=251 max_channel_delta=1
|
||||
scale=1 pan=6.125,-8.5 changed_pixels=250 max_channel_delta=1
|
||||
scale=1 pan=17.395,17.27 changed_pixels=238 max_channel_delta=1
|
||||
scale=1 pan=-0.125,0.125 changed_pixels=241 max_channel_delta=1
|
||||
scale=1.25 pan=0,0 changed_pixels=379 max_channel_delta=1
|
||||
scale=1.25 pan=-13.25,9.75 changed_pixels=395 max_channel_delta=1
|
||||
scale=1.25 pan=6.125,-8.5 changed_pixels=332 max_channel_delta=1
|
||||
scale=1.25 pan=17.395,17.27 changed_pixels=289 max_channel_delta=1
|
||||
scale=1.25 pan=-0.125,0.125 changed_pixels=372 max_channel_delta=1
|
||||
scale=2 pan=0,0 changed_pixels=759 max_channel_delta=1
|
||||
scale=2 pan=-13.25,9.75 changed_pixels=626 max_channel_delta=6
|
||||
scale=2 pan=6.125,-8.5 changed_pixels=731 max_channel_delta=1
|
||||
scale=2 pan=17.395,17.27 changed_pixels=576 max_channel_delta=1
|
||||
scale=2 pan=-0.125,0.125 changed_pixels=784 max_channel_delta=1
|
||||
cached=false construct_and_submit changing_pan n=20 p50_ms=6.6260 p95_ms=21.1743 max_ms=21.1743
|
||||
cached=true construct_and_submit changing_pan n=20 p50_ms=0.0010 p95_ms=0.0012 max_ms=0.0012
|
||||
cache_builds=1
|
||||
@@ -1,32 +0,0 @@
|
||||
renderer=wgpu; phase-cache prototype; NOT native FPS
|
||||
size=301.5x211.25 zoom=0.73 step=17.52
|
||||
scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
difference_xy=301,26 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,27 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,62 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,80 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,97 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,115 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,132 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,150 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,167 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,184 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,185 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,202 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
scale=1 pan=-13.25,9.75 changed_pixels=12 max_channel_delta=1
|
||||
scale=1 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=17.395,17.27 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=17.395,17.27 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=17.395,17.27 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
cached=false construct_and_submit changing_pan n=20 p50_ms=6.1755 p95_ms=8.4025 max_ms=8.4025
|
||||
cached=true construct_and_submit changing_pan n=20 p50_ms=0.0011 p95_ms=0.0019 max_ms=0.0019
|
||||
cache_builds=1
|
||||
@@ -1,100 +0,0 @@
|
||||
renderer=wgpu; phase-cache prototype; NOT native FPS
|
||||
size=1900x660 zoom=1 step=24
|
||||
scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=23.875,23.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=23.875,23.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=23.875,23.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
cached=false construct_and_submit changing_pan n=20 p50_ms=72.0271 p95_ms=99.9725 max_ms=99.9725
|
||||
cached=true construct_and_submit changing_pan n=20 p50_ms=0.0011 p95_ms=0.0015 max_ms=0.0015
|
||||
cache_builds=1
|
||||
cached=false construct_submit_screenshot changing_pan n=8 p50_ms=259.9265 min_ms=253.3506 max_ms=281.4865
|
||||
cached=true construct_submit_screenshot changing_pan n=8 p50_ms=212.6536 min_ms=184.5191 max_ms=299.2711
|
||||
size=1900x660 zoom=0.5 step=14
|
||||
scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=13.875,13.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=13.875,13.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=13.875,13.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
cached=false construct_and_submit changing_pan n=20 p50_ms=167.4776 p95_ms=186.8968 max_ms=186.8968
|
||||
cached=true construct_and_submit changing_pan n=20 p50_ms=0.0008 p95_ms=0.0008 max_ms=0.0008
|
||||
cache_builds=1
|
||||
size=1440x900 zoom=1 step=24
|
||||
scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=23.875,23.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=23.875,23.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=23.875,23.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
cached=false construct_and_submit changing_pan n=20 p50_ms=68.4232 p95_ms=97.6147 max_ms=97.6147
|
||||
cached=true construct_and_submit changing_pan n=20 p50_ms=0.0008 p95_ms=0.0458 max_ms=0.0458
|
||||
cache_builds=1
|
||||
size=1440x900 zoom=0.5 step=14
|
||||
scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=13.875,13.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=13.875,13.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=13.875,13.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
cached=false construct_and_submit changing_pan n=20 p50_ms=173.7177 p95_ms=215.3188 max_ms=215.3188
|
||||
cached=true construct_and_submit changing_pan n=20 p50_ms=0.0008 p95_ms=0.0009 max_ms=0.0009
|
||||
cache_builds=1
|
||||
cached=false construct_submit_screenshot changing_pan n=8 p50_ms=337.2560 min_ms=319.0543 max_ms=366.4956
|
||||
cached=true construct_submit_screenshot changing_pan n=8 p50_ms=170.3780 min_ms=144.1992 max_ms=188.4337
|
||||
size=301.5x211.25 zoom=0.73 step=17.52
|
||||
scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-13.25,9.75 changed_pixels=12 max_channel_delta=1
|
||||
scale=1 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=17.395,17.27 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=17.395,17.27 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-13.25,9.75 changed_pixels=1 max_channel_delta=6
|
||||
scale=2 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=17.395,17.27 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
cached=false construct_and_submit changing_pan n=20 p50_ms=5.4797 p95_ms=6.8957 max_ms=6.8957
|
||||
cached=true construct_and_submit changing_pan n=20 p50_ms=0.0007 p95_ms=0.0008 max_ms=0.0008
|
||||
cache_builds=1
|
||||
@@ -1,127 +0,0 @@
|
||||
renderer=wgpu; phase-cache prototype; NOT native FPS
|
||||
size=1900x660 zoom=1 step=24
|
||||
scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=23.875,23.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=23.875,23.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=23.875,23.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
cached=false construct_and_submit changing_pan n=20 p50_ms=68.3424 p95_ms=88.0883 max_ms=88.0883
|
||||
cached=true construct_and_submit changing_pan n=20 p50_ms=0.0010 p95_ms=0.0012 max_ms=0.0012
|
||||
cache_builds=1
|
||||
cached=false construct_submit_screenshot changing_pan n=8 p50_ms=249.7269 min_ms=216.9378 max_ms=267.1425
|
||||
cached=true construct_submit_screenshot changing_pan n=8 p50_ms=215.6865 min_ms=177.7329 max_ms=262.5719
|
||||
size=1900x660 zoom=0.5 step=14
|
||||
scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=13.875,13.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=13.875,13.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=13.875,13.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
cached=false construct_and_submit changing_pan n=20 p50_ms=190.0141 p95_ms=231.4193 max_ms=231.4193
|
||||
cached=true construct_and_submit changing_pan n=20 p50_ms=0.0008 p95_ms=0.0008 max_ms=0.0008
|
||||
cache_builds=1
|
||||
size=1440x900 zoom=1 step=24
|
||||
scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=23.875,23.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=23.875,23.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=23.875,23.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
cached=false construct_and_submit changing_pan n=20 p50_ms=72.2449 p95_ms=86.5021 max_ms=86.5021
|
||||
cached=true construct_and_submit changing_pan n=20 p50_ms=0.0008 p95_ms=0.0008 max_ms=0.0008
|
||||
cache_builds=1
|
||||
size=1440x900 zoom=0.5 step=14
|
||||
scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=13.875,13.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=13.875,13.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=13.875,13.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
cached=false construct_and_submit changing_pan n=20 p50_ms=209.4728 p95_ms=329.0977 max_ms=329.0977
|
||||
cached=true construct_and_submit changing_pan n=20 p50_ms=0.0008 p95_ms=0.0009 max_ms=0.0009
|
||||
cache_builds=1
|
||||
cached=false construct_submit_screenshot changing_pan n=8 p50_ms=384.7680 min_ms=347.4879 max_ms=420.2894
|
||||
cached=true construct_submit_screenshot changing_pan n=8 p50_ms=164.1611 min_ms=150.1276 max_ms=186.4567
|
||||
size=301.5x211.25 zoom=0.73 step=17.52
|
||||
scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-13.25,9.75 changed_pixels=12 max_channel_delta=1
|
||||
difference_xy=301,26 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,27 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,62 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,80 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,97 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,115 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,132 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,150 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,167 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,184 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,185 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,202 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
scale=1 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=17.395,17.27 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=17.395,17.27 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-13.25,9.75 changed_pixels=1 max_channel_delta=6
|
||||
difference_xy=149,263 expected=[240, 243, 246, 255] actual=[246, 248, 251, 255]
|
||||
scale=2 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=17.395,17.27 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
cached=false construct_and_submit changing_pan n=20 p50_ms=5.5680 p95_ms=7.0634 max_ms=7.0634
|
||||
cached=true construct_and_submit changing_pan n=20 p50_ms=0.0007 p95_ms=0.0008 max_ms=0.0008
|
||||
cache_builds=1
|
||||
original_sequence_complete; beginning same-renderer repeat controls
|
||||
comparison=old/old repetition=1 size=301.5x211.25 scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
comparison=cached/cached repetition=1 size=301.5x211.25 scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
comparison=old/old repetition=1 size=301.5x211.25 scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
comparison=cached/cached repetition=1 size=301.5x211.25 scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
comparison=old/old repetition=2 size=301.5x211.25 scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
comparison=cached/cached repetition=2 size=301.5x211.25 scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
comparison=old/old repetition=2 size=301.5x211.25 scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
comparison=cached/cached repetition=2 size=301.5x211.25 scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
comparison=old/old repetition=3 size=301.5x211.25 scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
comparison=cached/cached repetition=3 size=301.5x211.25 scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
comparison=old/old repetition=3 size=301.5x211.25 scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
comparison=cached/cached repetition=3 size=301.5x211.25 scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
control_cache_builds=1
|
||||
@@ -1,11 +0,0 @@
|
||||
Compiling oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop)
|
||||
Finished `test` profile [unoptimized] target(s) in 4m 23s
|
||||
Running unittests src\main.rs (C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\arcadia_studio-c5b42126ba36cd38.exe)
|
||||
|
||||
running 3 tests
|
||||
test diagram::grid::tests::actual_canvas_keeps_background_cached_while_selection_and_revision_change ... ok
|
||||
test diagram::grid::tests::cache_reuses_unchanged_inputs_and_rebuilds_for_each_camera_or_size_change ... ok
|
||||
test diagram::grid::tests::cached_background_preserves_pixels_and_live_foreground_across_camera_and_size_changes ... ok
|
||||
|
||||
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 1547 filtered out; finished in 2.97s
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
Compiling oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop)
|
||||
Finished `test` profile [unoptimized] target(s) in 3m 31s
|
||||
Running unittests src\main.rs (C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\arcadia_studio-c5b42126ba36cd38.exe)
|
||||
|
||||
running 3 tests
|
||||
test diagram::grid::tests::actual_canvas_keeps_background_cached_while_selection_and_revision_change ... ok
|
||||
test diagram::grid::tests::cache_reuses_unchanged_inputs_and_rebuilds_for_each_camera_or_size_change ... ok
|
||||
test diagram::grid::tests::cached_background_preserves_pixels_and_live_foreground_across_camera_and_size_changes ... ok
|
||||
|
||||
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 1547 filtered out; finished in 4.13s
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
//! Headless rendering experiment; creates no desktop window and sends no input.
|
||||
use iced::advanced::{
|
||||
graphics::geometry::Renderer as _,
|
||||
renderer::{Headless, Renderer as _},
|
||||
};
|
||||
use iced::{
|
||||
Color, Point, Rectangle, Renderer, Size, Vector,
|
||||
widget::canvas::{Cache, Frame, Geometry, Path},
|
||||
};
|
||||
use std::{cell::Cell, hint::black_box, time::Instant};
|
||||
|
||||
const BG: Color = Color::from_rgb8(246, 248, 251);
|
||||
const DOT: Color = Color::from_rgb(0.86, 0.89, 0.91);
|
||||
|
||||
fn paint(frame: &mut Frame, size: Size, zoom: f32, pan: Vector) {
|
||||
frame.fill_rectangle(Point::ORIGIN, size, BG);
|
||||
let step = (24. * zoom).max(14.);
|
||||
let ox = pan.x.rem_euclid(step);
|
||||
let oy = pan.y.rem_euclid(step);
|
||||
for x in 0..(size.width / step) as i32 + 1 {
|
||||
for y in 0..(size.height / step) as i32 + 1 {
|
||||
frame.fill(
|
||||
&Path::circle(Point::new(ox + x as f32 * step, oy + y as f32 * step), 0.65),
|
||||
DOT,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn fresh(renderer: &Renderer, size: Size, zoom: f32, pan: Vector) -> Geometry {
|
||||
let mut frame = Frame::new(renderer, size);
|
||||
paint(&mut frame, size, zoom, pan);
|
||||
frame.into_geometry()
|
||||
}
|
||||
fn batched(renderer: &Renderer, size: Size, zoom: f32, pan: Vector) -> Geometry {
|
||||
let mut frame = Frame::new(renderer, size);
|
||||
frame.fill_rectangle(Point::ORIGIN, size, BG);
|
||||
let step = (24. * zoom).max(14.);
|
||||
let ox = pan.x.rem_euclid(step);
|
||||
let oy = pan.y.rem_euclid(step);
|
||||
let path = Path::new(|p| {
|
||||
for x in 0..(size.width / step) as i32 + 1 {
|
||||
for y in 0..(size.height / step) as i32 + 1 {
|
||||
p.circle(Point::new(ox + x as f32 * step, oy + y as f32 * step), 0.65);
|
||||
p.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
frame.fill(&path, DOT);
|
||||
frame.into_geometry()
|
||||
}
|
||||
fn screenshot(renderer: &mut Renderer, size: Size, geometry: Geometry) -> Vec<u8> {
|
||||
renderer.reset(Rectangle::with_size(size));
|
||||
renderer.draw_geometry(geometry);
|
||||
Headless::screenshot(
|
||||
renderer,
|
||||
Size::new(size.width as u32, size.height as u32),
|
||||
1.,
|
||||
BG,
|
||||
)
|
||||
}
|
||||
fn measure<T>(label: &str, n: usize, mut f: impl FnMut() -> T) {
|
||||
for _ in 0..3 {
|
||||
black_box(f());
|
||||
}
|
||||
let mut values = Vec::new();
|
||||
for _ in 0..n {
|
||||
let now = Instant::now();
|
||||
let value = black_box(f());
|
||||
values.push(now.elapsed().as_secs_f64() * 1000.);
|
||||
drop(value);
|
||||
}
|
||||
values.sort_by(f64::total_cmp);
|
||||
let at = |p: f64| values[((n - 1) as f64 * p).ceil() as usize];
|
||||
println!(
|
||||
"{label}: n={n} p50_ms={:.4} p95_ms={:.4} max_ms={:.4}",
|
||||
at(0.5),
|
||||
at(0.95),
|
||||
values[n - 1]
|
||||
);
|
||||
}
|
||||
fn main() {
|
||||
let n = std::env::args()
|
||||
.nth(1)
|
||||
.map(|s| s.parse::<usize>().unwrap())
|
||||
.unwrap_or(20)
|
||||
.max(1);
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
let mut renderer = runtime
|
||||
.block_on(<Renderer as Headless>::new(
|
||||
iced::Font::DEFAULT,
|
||||
16.into(),
|
||||
None,
|
||||
))
|
||||
.expect("headless production renderer");
|
||||
println!(
|
||||
"renderer={} samples={n}; screenshot includes offscreen rendering + GPU readback, NOT native frame presentation",
|
||||
renderer.name()
|
||||
);
|
||||
for (size, zoom) in [
|
||||
(Size::new(1900., 660.), 1.),
|
||||
(Size::new(1900., 660.), 0.5),
|
||||
(Size::new(1440., 900.), 1.),
|
||||
(Size::new(1440., 900.), 0.5),
|
||||
] {
|
||||
let pan = Vector::new(-13.25, 9.75);
|
||||
let cache = Cache::new();
|
||||
let calls = Cell::new(0);
|
||||
let cached = |r: &Renderer| {
|
||||
cache.draw(r, size, |f| {
|
||||
calls.set(calls.get() + 1);
|
||||
paint(f, size, zoom, pan);
|
||||
})
|
||||
};
|
||||
let step = (24f32 * zoom).max(14.);
|
||||
println!(
|
||||
"\nsize={}x{} zoom={zoom} dots={}",
|
||||
size.width,
|
||||
size.height,
|
||||
((size.width / step) as i32 + 1) * ((size.height / step) as i32 + 1)
|
||||
);
|
||||
let baseline = fresh(&renderer, size, zoom, pan);
|
||||
let baseline_pixels = screenshot(&mut renderer, size, baseline);
|
||||
let geometry = batched(&renderer, size, zoom, pan);
|
||||
let pixels = screenshot(&mut renderer, size, geometry);
|
||||
let differences = baseline_pixels
|
||||
.iter()
|
||||
.zip(&pixels)
|
||||
.filter(|(a, b)| a != b)
|
||||
.count();
|
||||
println!("batched_rgba_byte_differences={differences}");
|
||||
assert_eq!(
|
||||
differences, 0,
|
||||
"batched path must preserve exact grid pixels"
|
||||
);
|
||||
let geometry = cached(&renderer);
|
||||
assert_eq!(baseline_pixels, screenshot(&mut renderer, size, geometry));
|
||||
let geometry = cached(&renderer);
|
||||
assert_eq!(baseline_pixels, screenshot(&mut renderer, size, geometry));
|
||||
assert_eq!(calls.get(), 1);
|
||||
println!("pixel_parity=cold_and_warm_exact");
|
||||
measure("fresh_geometry", n, || fresh(&renderer, size, zoom, pan));
|
||||
measure("batched_geometry", n, || {
|
||||
batched(&renderer, size, zoom, pan)
|
||||
});
|
||||
measure("cached_geometry", n, || cached(&renderer));
|
||||
assert_eq!(calls.get(), 1, "identical camera must reuse geometry");
|
||||
measure("fresh_geometry_and_screenshot", n, || {
|
||||
let g = fresh(&renderer, size, zoom, pan);
|
||||
screenshot(&mut renderer, size, g)
|
||||
});
|
||||
measure("cached_geometry_and_screenshot", n, || {
|
||||
let g = cached(&renderer);
|
||||
screenshot(&mut renderer, size, g)
|
||||
});
|
||||
measure("cache_forced_miss_geometry", n, || {
|
||||
cache.clear();
|
||||
cached(&renderer)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
//! Headless rendering experiment; creates no desktop window and sends no input.
|
||||
use iced::advanced::{
|
||||
graphics::geometry::Renderer as _,
|
||||
renderer::{Headless, Renderer as _},
|
||||
};
|
||||
use iced::{
|
||||
Color, Point, Rectangle, Renderer, Size, Vector,
|
||||
widget::canvas::{Cache, Frame, Geometry, Path},
|
||||
};
|
||||
use std::{cell::Cell, hint::black_box, time::Instant};
|
||||
|
||||
const BG: Color = Color::from_rgb8(246, 248, 251);
|
||||
const DOT: Color = Color::from_rgb(0.86, 0.89, 0.91);
|
||||
|
||||
fn paint(frame: &mut Frame, size: Size, zoom: f32, pan: Vector) {
|
||||
frame.fill_rectangle(Point::ORIGIN, size, BG);
|
||||
let step = (24. * zoom).max(14.);
|
||||
let ox = pan.x.rem_euclid(step);
|
||||
let oy = pan.y.rem_euclid(step);
|
||||
for x in 0..(size.width / step) as i32 + 1 {
|
||||
for y in 0..(size.height / step) as i32 + 1 {
|
||||
frame.fill(
|
||||
&Path::circle(Point::new(ox + x as f32 * step, oy + y as f32 * step), 0.65),
|
||||
DOT,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn fresh(renderer: &Renderer, size: Size, zoom: f32, pan: Vector) -> Geometry {
|
||||
let mut frame = Frame::new(renderer, size);
|
||||
paint(&mut frame, size, zoom, pan);
|
||||
frame.into_geometry()
|
||||
}
|
||||
fn screenshot(renderer: &mut Renderer, size: Size, geometry: Geometry) -> Vec<u8> {
|
||||
renderer.reset(Rectangle::with_size(size));
|
||||
renderer.draw_geometry(geometry);
|
||||
Headless::screenshot(
|
||||
renderer,
|
||||
Size::new(size.width as u32, size.height as u32),
|
||||
1.,
|
||||
BG,
|
||||
)
|
||||
}
|
||||
fn measure<T>(label: &str, n: usize, mut f: impl FnMut() -> T) {
|
||||
for _ in 0..3 {
|
||||
black_box(f());
|
||||
}
|
||||
let mut values = Vec::new();
|
||||
for _ in 0..n {
|
||||
let now = Instant::now();
|
||||
let value = black_box(f());
|
||||
values.push(now.elapsed().as_secs_f64() * 1000.);
|
||||
drop(value);
|
||||
}
|
||||
values.sort_by(f64::total_cmp);
|
||||
let at = |p: f64| values[((n - 1) as f64 * p).ceil() as usize];
|
||||
println!(
|
||||
"{label}: n={n} p50_ms={:.4} p95_ms={:.4} max_ms={:.4}",
|
||||
at(0.5),
|
||||
at(0.95),
|
||||
values[n - 1]
|
||||
);
|
||||
}
|
||||
fn main() {
|
||||
let n = std::env::args()
|
||||
.nth(1)
|
||||
.map(|s| s.parse::<usize>().unwrap())
|
||||
.unwrap_or(20)
|
||||
.max(1);
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
let mut renderer = runtime
|
||||
.block_on(<Renderer as Headless>::new(
|
||||
iced::Font::DEFAULT,
|
||||
16.into(),
|
||||
None,
|
||||
))
|
||||
.expect("headless production renderer");
|
||||
println!(
|
||||
"renderer={} samples={n}; screenshot includes offscreen rendering + GPU readback, NOT native frame presentation",
|
||||
renderer.name()
|
||||
);
|
||||
for (size, zoom) in [
|
||||
(Size::new(1900., 660.), 1.),
|
||||
(Size::new(1900., 660.), 0.5),
|
||||
(Size::new(1440., 900.), 1.),
|
||||
(Size::new(1440., 900.), 0.5),
|
||||
] {
|
||||
let pan = Vector::new(-13.25, 9.75);
|
||||
let cache = Cache::new();
|
||||
let calls = Cell::new(0);
|
||||
let cached = |r: &Renderer| {
|
||||
cache.draw(r, size, |f| {
|
||||
calls.set(calls.get() + 1);
|
||||
paint(f, size, zoom, pan);
|
||||
})
|
||||
};
|
||||
let step = (24f32 * zoom).max(14.);
|
||||
println!(
|
||||
"\nsize={}x{} zoom={zoom} dots={}",
|
||||
size.width,
|
||||
size.height,
|
||||
((size.width / step) as i32 + 1) * ((size.height / step) as i32 + 1)
|
||||
);
|
||||
let baseline = fresh(&renderer, size, zoom, pan);
|
||||
let baseline_pixels = screenshot(&mut renderer, size, baseline);
|
||||
let geometry = cached(&renderer);
|
||||
assert_eq!(baseline_pixels, screenshot(&mut renderer, size, geometry));
|
||||
let geometry = cached(&renderer);
|
||||
assert_eq!(baseline_pixels, screenshot(&mut renderer, size, geometry));
|
||||
assert_eq!(calls.get(), 1);
|
||||
println!("pixel_parity=cold_and_warm_exact");
|
||||
measure("fresh_geometry", n, || fresh(&renderer, size, zoom, pan));
|
||||
measure("cached_geometry", n, || cached(&renderer));
|
||||
assert_eq!(calls.get(), 1, "identical camera must reuse geometry");
|
||||
measure("fresh_geometry_and_screenshot", n, || {
|
||||
let g = fresh(&renderer, size, zoom, pan);
|
||||
screenshot(&mut renderer, size, g)
|
||||
});
|
||||
measure("cached_geometry_and_screenshot", n, || {
|
||||
let g = cached(&renderer);
|
||||
screenshot(&mut renderer, size, g)
|
||||
});
|
||||
measure("cache_forced_miss_geometry", n, || {
|
||||
cache.clear();
|
||||
cached(&renderer)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
# Independent cache and evaluator review
|
||||
|
||||
Read-only GPT-6 Astra review of
|
||||
`636093aae67aa48fa1d35e403a205227e81d04de`, 2026-09-25. The reviewer did not
|
||||
edit files, compile, launch a desktop window or control the user interface.
|
||||
|
||||
No concrete cache-key, lifetime or drawing-order defect was found. The review
|
||||
checked Iced 0.14 source as well as the application: Iced compares exact cache
|
||||
size, explicit camera changes clear geometry, and context/gesture retirement
|
||||
retains the model-independent grid. Normal canvas state survives view rebuilds;
|
||||
the focus wrapper's suspension reset causes a safe cold miss. Cached and live
|
||||
mesh submissions retain their invocation order. Background geometry has no
|
||||
text/images, and existing model transforms still apply to the live scene.
|
||||
|
||||
The evaluator uses an independent copy of the original grid painter, rather
|
||||
than calling the candidate's painter to produce its expected image. It compares
|
||||
the former combined frame with the split cached background, including opaque
|
||||
and translucent foreground, text, overlay, fractional inputs and live edits.
|
||||
The real DiagramCanvas check demonstrates changing selection/model revision
|
||||
pixels while the grid construction count remains one, then checks pan/resize
|
||||
invalidation. It exercises retained-state drawing and scene invalidation;
|
||||
event-driven native gesture acceptance is outside that test.
|
||||
|
||||
The reviewer inspected `grid-tests-01.log`: all three tests pass, none fail or
|
||||
are ignored, 1,547 are filtered, and execution takes 2.97s. Source identity is
|
||||
provided by the accompanying run record and clean tracked implementation, not
|
||||
by an embedded hash in the Cargo log. Full-suite/build/Clippy results published
|
||||
later remain separate evidence and must be read in their own logs.
|
||||
|
||||
The measurements support avoiding repeated grid construction for an unchanged
|
||||
camera. They do not establish native FPS, performance on another renderer, or
|
||||
improvement during continuously changing pan/zoom. No broad final application
|
||||
or release acceptance is claimed by this review.
|
||||
|
||||
A second read-only audit checked the full desktop log's 1,550 distinct names,
|
||||
1,543 passes and seven ignored cases against `context.json`. It verified all
|
||||
three recorded log hashes, driver-source hash, committed grid blob, linked Iced
|
||||
and Tokio libraries, timing tables, sample method and rejected-batch ranges.
|
||||
It did not independently verify the probe executable, execution chronology,
|
||||
Clippy/build gates or environment assertions. It clarified that run 04's generic
|
||||
cold-and-warm marker is not a production-cache warm-pixel check; that assertion
|
||||
belongs to the separate committed tests.
|
||||
@@ -1,209 +0,0 @@
|
||||
//! Renderer-level phase translation experiment, not application code.
|
||||
use iced::advanced::{Renderer as _, graphics::geometry::Renderer as _, renderer::Headless};
|
||||
use iced::{
|
||||
Color, Point, Rectangle, Renderer, Size, Vector,
|
||||
widget::canvas::{Cache, Frame, Geometry, Path},
|
||||
};
|
||||
use std::{cell::Cell, hint::black_box, time::Instant};
|
||||
const BG: Color = Color::from_rgb8(246, 248, 251);
|
||||
const DOT: Color = Color::from_rgb(0.86, 0.89, 0.91);
|
||||
fn dots(f: &mut Frame, size: Size, step: f32, phase: Vector) {
|
||||
for x in 0..(size.width / step) as i32 + 1 {
|
||||
for y in 0..(size.height / step) as i32 + 1 {
|
||||
f.fill(
|
||||
&Path::circle(
|
||||
Point::new(phase.x + x as f32 * step, phase.y + y as f32 * step),
|
||||
0.65,
|
||||
),
|
||||
DOT,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn old(r: &Renderer, size: Size, step: f32, phase: Vector) -> Geometry {
|
||||
let mut f = Frame::new(r, size);
|
||||
f.fill_rectangle(Point::ORIGIN, size, BG);
|
||||
dots(&mut f, size, step, phase);
|
||||
f.into_geometry()
|
||||
}
|
||||
fn phase(r: &Renderer, cache: &Cache, size: Size, step: f32, builds: &Cell<usize>) -> Geometry {
|
||||
cache.draw_with_bounds(
|
||||
r,
|
||||
Rectangle {
|
||||
x: -step,
|
||||
y: -step,
|
||||
width: size.width + 3. * step,
|
||||
height: size.height + 3. * step,
|
||||
},
|
||||
|f| {
|
||||
builds.set(builds.get() + 1);
|
||||
f.fill_rectangle(
|
||||
Point::new(-step, -step),
|
||||
Size::new(size.width + 3. * step, size.height + 3. * step),
|
||||
BG,
|
||||
);
|
||||
dots(f, size, step, Vector::ZERO);
|
||||
},
|
||||
)
|
||||
}
|
||||
fn submit(r: &mut Renderer, size: Size, g: Geometry, pan: Vector) {
|
||||
r.with_layer(Rectangle::with_size(size), |r| {
|
||||
r.with_translation(pan, |r| r.draw_geometry(g))
|
||||
});
|
||||
}
|
||||
fn shot(r: &mut Renderer, size: Size, scale: f32) -> Vec<u8> {
|
||||
Headless::screenshot(
|
||||
r,
|
||||
Size::new(
|
||||
(size.width * scale).ceil() as u32,
|
||||
(size.height * scale).ceil() as u32,
|
||||
),
|
||||
scale,
|
||||
BG,
|
||||
)
|
||||
}
|
||||
fn main() {
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
let mut r = rt
|
||||
.block_on(<Renderer as Headless>::new(
|
||||
iced::Font::DEFAULT,
|
||||
16.into(),
|
||||
None,
|
||||
))
|
||||
.unwrap();
|
||||
println!(
|
||||
"renderer={}; phase-cache prototype; NOT native FPS",
|
||||
r.name()
|
||||
);
|
||||
for (size, zoom) in [
|
||||
(Size::new(1900., 660.), 1.),
|
||||
(Size::new(1900., 660.), 0.5),
|
||||
(Size::new(1440., 900.), 1.),
|
||||
(Size::new(1440., 900.), 0.5),
|
||||
(Size::new(301.5, 211.25), 0.73),
|
||||
] {
|
||||
if size.width != 301.5 {
|
||||
continue;
|
||||
}
|
||||
let step = (24f32 * zoom).max(14.);
|
||||
let cache = Cache::new();
|
||||
let builds = Cell::new(0);
|
||||
println!(
|
||||
"size={}x{} zoom={zoom} step={step}",
|
||||
size.width, size.height
|
||||
);
|
||||
for scale in [1., 1.25, 2.] {
|
||||
for pan in [
|
||||
Vector::ZERO,
|
||||
Vector::new(-13.25, 9.75),
|
||||
Vector::new(6.125, -8.5),
|
||||
Vector::new(step - 0.125, step - 0.25),
|
||||
Vector::new(-0.125, 0.125),
|
||||
] {
|
||||
let offset = Vector::new(pan.x.rem_euclid(step), pan.y.rem_euclid(step));
|
||||
r.reset(Rectangle::with_size(size));
|
||||
let g = old(&r, size, step, offset);
|
||||
r.draw_geometry(g);
|
||||
let expected = shot(&mut r, size, scale);
|
||||
r.reset(Rectangle::with_size(size));
|
||||
let g = phase(&r, &cache, size, step, &builds);
|
||||
submit(&mut r, size, g, offset);
|
||||
let actual = shot(&mut r, size, scale);
|
||||
assert_eq!(expected.len(), actual.len());
|
||||
for (index, (a, b)) in expected
|
||||
.chunks_exact(4)
|
||||
.zip(actual.chunks_exact(4))
|
||||
.enumerate()
|
||||
.filter(|(_, (a, b))| a != b)
|
||||
.take(20)
|
||||
{
|
||||
println!(
|
||||
"difference_xy={},{} expected={a:?} actual={b:?}",
|
||||
index % (size.width * scale).ceil() as usize,
|
||||
index / (size.width * scale).ceil() as usize
|
||||
);
|
||||
}
|
||||
let changed = expected
|
||||
.chunks_exact(4)
|
||||
.zip(actual.chunks_exact(4))
|
||||
.filter(|(a, b)| a != b)
|
||||
.count();
|
||||
let max = expected
|
||||
.iter()
|
||||
.zip(&actual)
|
||||
.map(|(a, b)| a.abs_diff(*b))
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
println!(
|
||||
"scale={scale} pan={},{} changed_pixels={changed} max_channel_delta={max}",
|
||||
pan.x, pan.y
|
||||
);
|
||||
}
|
||||
}
|
||||
assert_eq!(builds.get(), 1);
|
||||
for cached in [false, true] {
|
||||
let mut ms = Vec::new();
|
||||
for i in 0..23 {
|
||||
r.reset(Rectangle::with_size(size));
|
||||
let offset = Vector::new((i as f32 * 0.25).rem_euclid(step), 9.75);
|
||||
let now = Instant::now();
|
||||
let g = if cached {
|
||||
phase(&r, &cache, size, step, &builds)
|
||||
} else {
|
||||
old(&r, size, step, offset)
|
||||
};
|
||||
if cached {
|
||||
submit(&mut r, size, g, offset)
|
||||
} else {
|
||||
r.draw_geometry(g)
|
||||
};
|
||||
let elapsed = black_box(now.elapsed().as_secs_f64() * 1000.);
|
||||
if i >= 3 {
|
||||
ms.push(elapsed);
|
||||
}
|
||||
}
|
||||
ms.sort_by(f64::total_cmp);
|
||||
println!(
|
||||
"cached={cached} construct_and_submit changing_pan n=20 p50_ms={:.4} p95_ms={:.4} max_ms={:.4}",
|
||||
ms[10], ms[19], ms[19]
|
||||
);
|
||||
}
|
||||
println!("cache_builds={}", builds.get());
|
||||
assert_eq!(builds.get(), 1);
|
||||
// Includes backend prepare/upload and readback; still not native presentation.
|
||||
if size.width == 1900. && zoom == 1. || size.width == 1440. && zoom == 0.5 {
|
||||
for cached in [false, true] {
|
||||
let mut ms = Vec::new();
|
||||
for i in 0..10 {
|
||||
r.reset(Rectangle::with_size(size));
|
||||
let offset = Vector::new((i as f32 * 0.25).rem_euclid(step), 9.75);
|
||||
let now = Instant::now();
|
||||
let g = if cached {
|
||||
phase(&r, &cache, size, step, &builds)
|
||||
} else {
|
||||
old(&r, size, step, offset)
|
||||
};
|
||||
if cached {
|
||||
submit(&mut r, size, g, offset);
|
||||
} else {
|
||||
r.draw_geometry(g);
|
||||
}
|
||||
let pixels = black_box(shot(&mut r, size, 1.));
|
||||
let elapsed = now.elapsed().as_secs_f64() * 1000.;
|
||||
drop(pixels);
|
||||
if i >= 2 {
|
||||
ms.push(elapsed);
|
||||
}
|
||||
}
|
||||
ms.sort_by(f64::total_cmp);
|
||||
println!(
|
||||
"cached={cached} construct_submit_screenshot changing_pan n=8 p50_ms={:.4} min_ms={:.4} max_ms={:.4}",
|
||||
ms[4], ms[0], ms[7]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
//! Renderer-level phase translation experiment, not application code.
|
||||
use iced::advanced::{Renderer as _, graphics::geometry::Renderer as _, renderer::Headless};
|
||||
use iced::{
|
||||
Color, Point, Rectangle, Renderer, Size, Vector,
|
||||
widget::canvas::{Cache, Frame, Geometry, Path},
|
||||
};
|
||||
use std::{cell::Cell, hint::black_box, time::Instant};
|
||||
const BG: Color = Color::from_rgb8(246, 248, 251);
|
||||
const DOT: Color = Color::from_rgb(0.86, 0.89, 0.91);
|
||||
fn dots(f: &mut Frame, size: Size, step: f32, phase: Vector) {
|
||||
for x in 0..(size.width / step) as i32 + 1 {
|
||||
for y in 0..(size.height / step) as i32 + 1 {
|
||||
f.fill(
|
||||
&Path::circle(
|
||||
Point::new(phase.x + x as f32 * step, phase.y + y as f32 * step),
|
||||
0.65,
|
||||
),
|
||||
DOT,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn old(r: &Renderer, size: Size, step: f32, phase: Vector) -> Geometry {
|
||||
let mut f = Frame::new(r, size);
|
||||
f.fill_rectangle(Point::ORIGIN, size, BG);
|
||||
dots(&mut f, size, step, phase);
|
||||
f.into_geometry()
|
||||
}
|
||||
fn phase(r: &Renderer, cache: &Cache, size: Size, step: f32, builds: &Cell<usize>) -> Geometry {
|
||||
cache.draw_with_bounds(
|
||||
r,
|
||||
Rectangle {
|
||||
x: -step,
|
||||
y: -step,
|
||||
width: size.width + 3. * step,
|
||||
height: size.height + 3. * step,
|
||||
},
|
||||
|f| {
|
||||
builds.set(builds.get() + 1);
|
||||
f.fill_rectangle(
|
||||
Point::new(-step, -step),
|
||||
Size::new(size.width + 3. * step, size.height + 3. * step),
|
||||
BG,
|
||||
);
|
||||
dots(f, size, step, Vector::ZERO);
|
||||
},
|
||||
)
|
||||
}
|
||||
fn submit(r: &mut Renderer, size: Size, g: Geometry, pan: Vector) {
|
||||
r.with_layer(Rectangle::with_size(size), |r| {
|
||||
r.with_translation(pan, |r| r.draw_geometry(g))
|
||||
});
|
||||
}
|
||||
fn shot(r: &mut Renderer, size: Size, scale: f32) -> Vec<u8> {
|
||||
Headless::screenshot(
|
||||
r,
|
||||
Size::new(
|
||||
(size.width * scale).ceil() as u32,
|
||||
(size.height * scale).ceil() as u32,
|
||||
),
|
||||
scale,
|
||||
BG,
|
||||
)
|
||||
}
|
||||
fn main() {
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
let mut r = rt
|
||||
.block_on(<Renderer as Headless>::new(
|
||||
iced::Font::DEFAULT,
|
||||
16.into(),
|
||||
None,
|
||||
))
|
||||
.unwrap();
|
||||
println!(
|
||||
"renderer={}; phase-cache prototype; NOT native FPS",
|
||||
r.name()
|
||||
);
|
||||
for (size, zoom) in [
|
||||
(Size::new(1900., 660.), 1.),
|
||||
(Size::new(1900., 660.), 0.5),
|
||||
(Size::new(1440., 900.), 1.),
|
||||
(Size::new(1440., 900.), 0.5),
|
||||
(Size::new(301.5, 211.25), 0.73),
|
||||
] {
|
||||
let step = (24f32 * zoom).max(14.);
|
||||
let cache = Cache::new();
|
||||
let builds = Cell::new(0);
|
||||
println!(
|
||||
"size={}x{} zoom={zoom} step={step}",
|
||||
size.width, size.height
|
||||
);
|
||||
for scale in [1., 1.25, 2.] {
|
||||
for pan in [
|
||||
Vector::ZERO,
|
||||
Vector::new(-13.25, 9.75),
|
||||
Vector::new(6.125, -8.5),
|
||||
Vector::new(step - 0.125, step - 0.25),
|
||||
Vector::new(-0.125, 0.125),
|
||||
] {
|
||||
let offset = Vector::new(pan.x.rem_euclid(step), pan.y.rem_euclid(step));
|
||||
r.reset(Rectangle::with_size(size));
|
||||
let g = old(&r, size, step, offset);
|
||||
r.draw_geometry(g);
|
||||
let expected = shot(&mut r, size, scale);
|
||||
r.reset(Rectangle::with_size(size));
|
||||
let g = phase(&r, &cache, size, step, &builds);
|
||||
submit(&mut r, size, g, offset);
|
||||
let actual = shot(&mut r, size, scale);
|
||||
assert_eq!(expected.len(), actual.len());
|
||||
let changed = expected
|
||||
.chunks_exact(4)
|
||||
.zip(actual.chunks_exact(4))
|
||||
.filter(|(a, b)| a != b)
|
||||
.count();
|
||||
let max = expected
|
||||
.iter()
|
||||
.zip(&actual)
|
||||
.map(|(a, b)| a.abs_diff(*b))
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
println!(
|
||||
"scale={scale} pan={},{} changed_pixels={changed} max_channel_delta={max}",
|
||||
pan.x, pan.y
|
||||
);
|
||||
}
|
||||
}
|
||||
assert_eq!(builds.get(), 1);
|
||||
for cached in [false, true] {
|
||||
let mut ms = Vec::new();
|
||||
for i in 0..23 {
|
||||
r.reset(Rectangle::with_size(size));
|
||||
let offset = Vector::new((i as f32 * 0.25).rem_euclid(step), 9.75);
|
||||
let now = Instant::now();
|
||||
let g = if cached {
|
||||
phase(&r, &cache, size, step, &builds)
|
||||
} else {
|
||||
old(&r, size, step, offset)
|
||||
};
|
||||
if cached {
|
||||
submit(&mut r, size, g, offset)
|
||||
} else {
|
||||
r.draw_geometry(g)
|
||||
};
|
||||
let elapsed = black_box(now.elapsed().as_secs_f64() * 1000.);
|
||||
if i >= 3 {
|
||||
ms.push(elapsed);
|
||||
}
|
||||
}
|
||||
ms.sort_by(f64::total_cmp);
|
||||
println!(
|
||||
"cached={cached} construct_and_submit changing_pan n=20 p50_ms={:.4} p95_ms={:.4} max_ms={:.4}",
|
||||
ms[10], ms[19], ms[19]
|
||||
);
|
||||
}
|
||||
println!("cache_builds={}", builds.get());
|
||||
assert_eq!(builds.get(), 1);
|
||||
// Includes backend prepare/upload and readback; still not native presentation.
|
||||
if size.width == 1900. && zoom == 1. || size.width == 1440. && zoom == 0.5 {
|
||||
for cached in [false, true] {
|
||||
let mut ms = Vec::new();
|
||||
for i in 0..10 {
|
||||
r.reset(Rectangle::with_size(size));
|
||||
let offset = Vector::new((i as f32 * 0.25).rem_euclid(step), 9.75);
|
||||
let now = Instant::now();
|
||||
let g = if cached {
|
||||
phase(&r, &cache, size, step, &builds)
|
||||
} else {
|
||||
old(&r, size, step, offset)
|
||||
};
|
||||
if cached {
|
||||
submit(&mut r, size, g, offset);
|
||||
} else {
|
||||
r.draw_geometry(g);
|
||||
}
|
||||
let pixels = black_box(shot(&mut r, size, 1.));
|
||||
let elapsed = now.elapsed().as_secs_f64() * 1000.;
|
||||
drop(pixels);
|
||||
if i >= 2 {
|
||||
ms.push(elapsed);
|
||||
}
|
||||
}
|
||||
ms.sort_by(f64::total_cmp);
|
||||
println!(
|
||||
"cached={cached} construct_submit_screenshot changing_pan n=8 p50_ms={:.4} min_ms={:.4} max_ms={:.4}",
|
||||
ms[4], ms[0], ms[7]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
//! Renderer-level phase translation experiment, not application code.
|
||||
use iced::advanced::{
|
||||
Renderer as _,
|
||||
graphics::geometry::Renderer as _,
|
||||
renderer::{Headless, Renderer as _},
|
||||
};
|
||||
use iced::{
|
||||
Color, Point, Rectangle, Renderer, Size, Vector,
|
||||
widget::canvas::{Cache, Frame, Geometry, Path},
|
||||
};
|
||||
use std::{cell::Cell, hint::black_box, time::Instant};
|
||||
const BG: Color = Color::from_rgb8(246, 248, 251);
|
||||
const DOT: Color = Color::from_rgb(0.86, 0.89, 0.91);
|
||||
fn dots(f: &mut Frame, size: Size, step: f32, phase: Vector) {
|
||||
for x in 0..(size.width / step) as i32 + 1 {
|
||||
for y in 0..(size.height / step) as i32 + 1 {
|
||||
f.fill(
|
||||
&Path::circle(
|
||||
Point::new(phase.x + x as f32 * step, phase.y + y as f32 * step),
|
||||
0.65,
|
||||
),
|
||||
DOT,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn old(r: &Renderer, size: Size, step: f32, phase: Vector) -> Geometry {
|
||||
let mut f = Frame::new(r, size);
|
||||
f.fill_rectangle(Point::ORIGIN, size, BG);
|
||||
dots(&mut f, size, step, phase);
|
||||
f.into_geometry()
|
||||
}
|
||||
fn phase(r: &Renderer, cache: &Cache, size: Size, step: f32, builds: &Cell<usize>) -> Geometry {
|
||||
cache.draw_with_bounds(
|
||||
r,
|
||||
Rectangle {
|
||||
x: -1.,
|
||||
y: -1.,
|
||||
width: size.width + step + 2.,
|
||||
height: size.height + step + 2.,
|
||||
},
|
||||
|f| {
|
||||
builds.set(builds.get() + 1);
|
||||
dots(f, size, step, Vector::ZERO);
|
||||
},
|
||||
)
|
||||
}
|
||||
fn submit(r: &mut Renderer, size: Size, g: Geometry, pan: Vector) {
|
||||
r.with_layer(Rectangle::with_size(size), |r| {
|
||||
r.with_translation(pan, |r| r.draw_geometry(g))
|
||||
});
|
||||
}
|
||||
fn shot(r: &mut Renderer, size: Size, scale: f32) -> Vec<u8> {
|
||||
Headless::screenshot(
|
||||
r,
|
||||
Size::new(
|
||||
(size.width * scale).ceil() as u32,
|
||||
(size.height * scale).ceil() as u32,
|
||||
),
|
||||
scale,
|
||||
BG,
|
||||
)
|
||||
}
|
||||
fn main() {
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
let mut r = rt
|
||||
.block_on(<Renderer as Headless>::new(
|
||||
iced::Font::DEFAULT,
|
||||
16.into(),
|
||||
None,
|
||||
))
|
||||
.unwrap();
|
||||
println!(
|
||||
"renderer={}; phase-cache prototype; NOT native FPS",
|
||||
r.name()
|
||||
);
|
||||
for (size, zoom) in [
|
||||
(Size::new(1900., 660.), 1.),
|
||||
(Size::new(1900., 660.), 0.5),
|
||||
(Size::new(1440., 900.), 1.),
|
||||
(Size::new(1440., 900.), 0.5),
|
||||
(Size::new(301.5, 211.25), 0.73),
|
||||
] {
|
||||
let step = (24f32 * zoom).max(14.);
|
||||
let cache = Cache::new();
|
||||
let builds = Cell::new(0);
|
||||
println!(
|
||||
"size={}x{} zoom={zoom} step={step}",
|
||||
size.width, size.height
|
||||
);
|
||||
for scale in [1., 1.25, 2.] {
|
||||
for pan in [
|
||||
Vector::ZERO,
|
||||
Vector::new(-13.25, 9.75),
|
||||
Vector::new(6.125, -8.5),
|
||||
Vector::new(step - 0.125, step - 0.25),
|
||||
Vector::new(-0.125, 0.125),
|
||||
] {
|
||||
let offset = Vector::new(pan.x.rem_euclid(step), pan.y.rem_euclid(step));
|
||||
r.reset(Rectangle::with_size(size));
|
||||
let g = old(&r, size, step, offset);
|
||||
r.draw_geometry(g);
|
||||
let expected = shot(&mut r, size, scale);
|
||||
r.reset(Rectangle::with_size(size));
|
||||
let g = phase(&r, &cache, size, step, &builds);
|
||||
submit(&mut r, size, g, offset);
|
||||
let actual = shot(&mut r, size, scale);
|
||||
assert_eq!(expected.len(), actual.len());
|
||||
let changed = expected
|
||||
.chunks_exact(4)
|
||||
.zip(actual.chunks_exact(4))
|
||||
.filter(|(a, b)| a != b)
|
||||
.count();
|
||||
let max = expected
|
||||
.iter()
|
||||
.zip(&actual)
|
||||
.map(|(a, b)| a.abs_diff(*b))
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
println!(
|
||||
"scale={scale} pan={},{} changed_pixels={changed} max_channel_delta={max}",
|
||||
pan.x, pan.y
|
||||
);
|
||||
}
|
||||
}
|
||||
assert_eq!(builds.get(), 1);
|
||||
for cached in [false, true] {
|
||||
let mut ms = Vec::new();
|
||||
for i in 0..23 {
|
||||
r.reset(Rectangle::with_size(size));
|
||||
let offset = Vector::new((i as f32 * 0.25).rem_euclid(step), 9.75);
|
||||
let now = Instant::now();
|
||||
let g = if cached {
|
||||
phase(&r, &cache, size, step, &builds)
|
||||
} else {
|
||||
old(&r, size, step, offset)
|
||||
};
|
||||
if cached {
|
||||
submit(&mut r, size, g, offset)
|
||||
} else {
|
||||
r.draw_geometry(g)
|
||||
};
|
||||
let elapsed = black_box(now.elapsed().as_secs_f64() * 1000.);
|
||||
if i >= 3 {
|
||||
ms.push(elapsed);
|
||||
}
|
||||
}
|
||||
ms.sort_by(f64::total_cmp);
|
||||
println!(
|
||||
"cached={cached} construct_and_submit changing_pan n=20 p50_ms={:.4} p95_ms={:.4} max_ms={:.4}",
|
||||
ms[10], ms[19], ms[19]
|
||||
);
|
||||
}
|
||||
println!("cache_builds={}", builds.get());
|
||||
assert_eq!(builds.get(), 1);
|
||||
}
|
||||
}
|
||||
@@ -1,285 +0,0 @@
|
||||
//! Full-sequence opaque phase replay with diagnostics and repeat controls; not application code.
|
||||
use iced::advanced::{Renderer as _, graphics::geometry::Renderer as _, renderer::Headless};
|
||||
use iced::{
|
||||
Color, Point, Rectangle, Renderer, Size, Vector,
|
||||
widget::canvas::{Cache, Frame, Geometry, Path},
|
||||
};
|
||||
use std::{cell::Cell, hint::black_box, time::Instant};
|
||||
const BG: Color = Color::from_rgb8(246, 248, 251);
|
||||
const DOT: Color = Color::from_rgb(0.86, 0.89, 0.91);
|
||||
fn dots(f: &mut Frame, size: Size, step: f32, phase: Vector) {
|
||||
for x in 0..(size.width / step) as i32 + 1 {
|
||||
for y in 0..(size.height / step) as i32 + 1 {
|
||||
f.fill(
|
||||
&Path::circle(
|
||||
Point::new(phase.x + x as f32 * step, phase.y + y as f32 * step),
|
||||
0.65,
|
||||
),
|
||||
DOT,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn old(r: &Renderer, size: Size, step: f32, phase: Vector) -> Geometry {
|
||||
let mut f = Frame::new(r, size);
|
||||
f.fill_rectangle(Point::ORIGIN, size, BG);
|
||||
dots(&mut f, size, step, phase);
|
||||
f.into_geometry()
|
||||
}
|
||||
fn phase(r: &Renderer, cache: &Cache, size: Size, step: f32, builds: &Cell<usize>) -> Geometry {
|
||||
cache.draw_with_bounds(
|
||||
r,
|
||||
Rectangle {
|
||||
x: -step,
|
||||
y: -step,
|
||||
width: size.width + 3. * step,
|
||||
height: size.height + 3. * step,
|
||||
},
|
||||
|f| {
|
||||
builds.set(builds.get() + 1);
|
||||
f.fill_rectangle(
|
||||
Point::new(-step, -step),
|
||||
Size::new(size.width + 3. * step, size.height + 3. * step),
|
||||
BG,
|
||||
);
|
||||
dots(f, size, step, Vector::ZERO);
|
||||
},
|
||||
)
|
||||
}
|
||||
fn submit(r: &mut Renderer, size: Size, g: Geometry, pan: Vector) {
|
||||
r.with_layer(Rectangle::with_size(size), |r| {
|
||||
r.with_translation(pan, |r| r.draw_geometry(g))
|
||||
});
|
||||
}
|
||||
fn shot(r: &mut Renderer, size: Size, scale: f32) -> Vec<u8> {
|
||||
Headless::screenshot(
|
||||
r,
|
||||
Size::new(
|
||||
(size.width * scale).ceil() as u32,
|
||||
(size.height * scale).ceil() as u32,
|
||||
),
|
||||
scale,
|
||||
BG,
|
||||
)
|
||||
}
|
||||
// Diagnostics only: this performs no rendering and does not mutate the renderer.
|
||||
fn log_coordinates(expected: &[u8], actual: &[u8], size: Size, scale: f32) {
|
||||
assert_eq!(expected.len(), actual.len());
|
||||
let physical_width = (size.width * scale).ceil() as usize;
|
||||
for (index, (a, b)) in expected
|
||||
.as_chunks::<4>()
|
||||
.0
|
||||
.iter()
|
||||
.zip(actual.as_chunks::<4>().0)
|
||||
.enumerate()
|
||||
.filter(|(_, (a, b))| a != b)
|
||||
.take(20)
|
||||
{
|
||||
println!(
|
||||
"difference_xy={},{} expected={a:?} actual={b:?}",
|
||||
index % physical_width,
|
||||
index / physical_width
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn report_control(label: &str, expected: &[u8], actual: &[u8], size: Size, scale: f32) {
|
||||
assert_eq!(expected.len(), actual.len());
|
||||
let changed = expected
|
||||
.as_chunks::<4>()
|
||||
.0
|
||||
.iter()
|
||||
.zip(actual.as_chunks::<4>().0)
|
||||
.filter(|(a, b)| a != b)
|
||||
.count();
|
||||
let max = expected
|
||||
.iter()
|
||||
.zip(actual)
|
||||
.map(|(a, b)| a.abs_diff(*b))
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
println!(
|
||||
"comparison={label} size={}x{} scale={scale} pan=-13.25,9.75 changed_pixels={changed} max_channel_delta={max}",
|
||||
size.width, size.height
|
||||
);
|
||||
log_coordinates(expected, actual, size, scale);
|
||||
}
|
||||
fn main() {
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
let mut r = rt
|
||||
.block_on(<Renderer as Headless>::new(
|
||||
iced::Font::DEFAULT,
|
||||
16.into(),
|
||||
None,
|
||||
))
|
||||
.unwrap();
|
||||
println!(
|
||||
"renderer={}; phase-cache prototype; NOT native FPS",
|
||||
r.name()
|
||||
);
|
||||
for (size, zoom) in [
|
||||
(Size::new(1900., 660.), 1.),
|
||||
(Size::new(1900., 660.), 0.5),
|
||||
(Size::new(1440., 900.), 1.),
|
||||
(Size::new(1440., 900.), 0.5),
|
||||
(Size::new(301.5, 211.25), 0.73),
|
||||
] {
|
||||
let step = (24f32 * zoom).max(14.);
|
||||
let cache = Cache::new();
|
||||
let builds = Cell::new(0);
|
||||
println!(
|
||||
"size={}x{} zoom={zoom} step={step}",
|
||||
size.width, size.height
|
||||
);
|
||||
for scale in [1., 1.25, 2.] {
|
||||
for pan in [
|
||||
Vector::ZERO,
|
||||
Vector::new(-13.25, 9.75),
|
||||
Vector::new(6.125, -8.5),
|
||||
Vector::new(step - 0.125, step - 0.25),
|
||||
Vector::new(-0.125, 0.125),
|
||||
] {
|
||||
let offset = Vector::new(pan.x.rem_euclid(step), pan.y.rem_euclid(step));
|
||||
r.reset(Rectangle::with_size(size));
|
||||
let g = old(&r, size, step, offset);
|
||||
r.draw_geometry(g);
|
||||
let expected = shot(&mut r, size, scale);
|
||||
r.reset(Rectangle::with_size(size));
|
||||
let g = phase(&r, &cache, size, step, &builds);
|
||||
submit(&mut r, size, g, offset);
|
||||
let actual = shot(&mut r, size, scale);
|
||||
assert_eq!(expected.len(), actual.len());
|
||||
let changed = expected
|
||||
.chunks_exact(4)
|
||||
.zip(actual.chunks_exact(4))
|
||||
.filter(|(a, b)| a != b)
|
||||
.count();
|
||||
let max = expected
|
||||
.iter()
|
||||
.zip(&actual)
|
||||
.map(|(a, b)| a.abs_diff(*b))
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
println!(
|
||||
"scale={scale} pan={},{} changed_pixels={changed} max_channel_delta={max}",
|
||||
pan.x, pan.y
|
||||
);
|
||||
// Keep the original render/timing sequence; print diagnostics only.
|
||||
log_coordinates(&expected, &actual, size, scale);
|
||||
}
|
||||
}
|
||||
assert_eq!(builds.get(), 1);
|
||||
for cached in [false, true] {
|
||||
let mut ms = Vec::new();
|
||||
for i in 0..23 {
|
||||
r.reset(Rectangle::with_size(size));
|
||||
let offset = Vector::new((i as f32 * 0.25).rem_euclid(step), 9.75);
|
||||
let now = Instant::now();
|
||||
let g = if cached {
|
||||
phase(&r, &cache, size, step, &builds)
|
||||
} else {
|
||||
old(&r, size, step, offset)
|
||||
};
|
||||
if cached {
|
||||
submit(&mut r, size, g, offset)
|
||||
} else {
|
||||
r.draw_geometry(g)
|
||||
};
|
||||
let elapsed = black_box(now.elapsed().as_secs_f64() * 1000.);
|
||||
if i >= 3 {
|
||||
ms.push(elapsed);
|
||||
}
|
||||
}
|
||||
ms.sort_by(f64::total_cmp);
|
||||
println!(
|
||||
"cached={cached} construct_and_submit changing_pan n=20 p50_ms={:.4} p95_ms={:.4} max_ms={:.4}",
|
||||
ms[10], ms[19], ms[19]
|
||||
);
|
||||
}
|
||||
println!("cache_builds={}", builds.get());
|
||||
assert_eq!(builds.get(), 1);
|
||||
// Includes backend prepare/upload and readback; still not native presentation.
|
||||
if size.width == 1900. && zoom == 1. || size.width == 1440. && zoom == 0.5 {
|
||||
for cached in [false, true] {
|
||||
let mut ms = Vec::new();
|
||||
for i in 0..10 {
|
||||
r.reset(Rectangle::with_size(size));
|
||||
let offset = Vector::new((i as f32 * 0.25).rem_euclid(step), 9.75);
|
||||
let now = Instant::now();
|
||||
let g = if cached {
|
||||
phase(&r, &cache, size, step, &builds)
|
||||
} else {
|
||||
old(&r, size, step, offset)
|
||||
};
|
||||
if cached {
|
||||
submit(&mut r, size, g, offset);
|
||||
} else {
|
||||
r.draw_geometry(g);
|
||||
}
|
||||
let pixels = black_box(shot(&mut r, size, 1.));
|
||||
let elapsed = now.elapsed().as_secs_f64() * 1000.;
|
||||
drop(pixels);
|
||||
if i >= 2 {
|
||||
ms.push(elapsed);
|
||||
}
|
||||
}
|
||||
ms.sort_by(f64::total_cmp);
|
||||
println!(
|
||||
"cached={cached} construct_submit_screenshot changing_pan n=8 p50_ms={:.4} min_ms={:.4} max_ms={:.4}",
|
||||
ms[4], ms[0], ms[7]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// All original cases, timings and screenshots have completed above.
|
||||
// Reuse the same renderer for controls. This cache is newly constructed,
|
||||
// then retained across all control repetitions; original per-case caches
|
||||
// have already gone out of scope, exactly as in the opaque probe.
|
||||
println!("original_sequence_complete; beginning same-renderer repeat controls");
|
||||
let size = Size::new(301.5, 211.25);
|
||||
let step = (24f32 * 0.73).max(14.);
|
||||
let pan: Vector<f32> = Vector::new(-13.25, 9.75);
|
||||
let offset = Vector::new(pan.x.rem_euclid(step), pan.y.rem_euclid(step));
|
||||
let cache = Cache::new();
|
||||
let builds = Cell::new(0);
|
||||
for repetition in 1..=3 {
|
||||
for scale in [1., 2.] {
|
||||
r.reset(Rectangle::with_size(size));
|
||||
let g = old(&r, size, step, offset);
|
||||
r.draw_geometry(g);
|
||||
let first = shot(&mut r, size, scale);
|
||||
r.reset(Rectangle::with_size(size));
|
||||
let g = old(&r, size, step, offset);
|
||||
r.draw_geometry(g);
|
||||
let second = shot(&mut r, size, scale);
|
||||
report_control(
|
||||
&format!("old/old repetition={repetition}"),
|
||||
&first,
|
||||
&second,
|
||||
size,
|
||||
scale,
|
||||
);
|
||||
|
||||
r.reset(Rectangle::with_size(size));
|
||||
let g = phase(&r, &cache, size, step, &builds);
|
||||
submit(&mut r, size, g, offset);
|
||||
let first = shot(&mut r, size, scale);
|
||||
r.reset(Rectangle::with_size(size));
|
||||
let g = phase(&r, &cache, size, step, &builds);
|
||||
submit(&mut r, size, g, offset);
|
||||
let second = shot(&mut r, size, scale);
|
||||
report_control(
|
||||
&format!("cached/cached repetition={repetition}"),
|
||||
&first,
|
||||
&second,
|
||||
size,
|
||||
scale,
|
||||
);
|
||||
}
|
||||
}
|
||||
println!("control_cache_builds={}", builds.get());
|
||||
assert_eq!(builds.get(), 1);
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
//! Headless rendering experiment; creates no desktop window and sends no input.
|
||||
use iced::advanced::{
|
||||
graphics::geometry::Renderer as _,
|
||||
renderer::{Headless, Renderer as _},
|
||||
};
|
||||
use iced::widget::canvas;
|
||||
use iced::{
|
||||
Color, Point, Rectangle, Renderer, Size, Vector,
|
||||
widget::canvas::{Cache, Frame, Geometry, Path},
|
||||
};
|
||||
use std::{cell::Cell, hint::black_box, time::Instant};
|
||||
|
||||
// Compile the actual candidate cache, not a second implementation of its key.
|
||||
#[path = "../../../crates/oa-desktop/src/diagram/grid.rs"]
|
||||
mod grid;
|
||||
mod style {
|
||||
pub const LINE: iced::Color = iced::Color::from_rgb(0.86, 0.89, 0.91);
|
||||
}
|
||||
|
||||
const BG: Color = Color::from_rgb8(246, 248, 251);
|
||||
const DOT: Color = Color::from_rgb(0.86, 0.89, 0.91);
|
||||
|
||||
fn paint(frame: &mut Frame, size: Size, zoom: f32, pan: Vector) {
|
||||
frame.fill_rectangle(Point::ORIGIN, size, BG);
|
||||
let step = (24. * zoom).max(14.);
|
||||
let ox = pan.x.rem_euclid(step);
|
||||
let oy = pan.y.rem_euclid(step);
|
||||
for x in 0..(size.width / step) as i32 + 1 {
|
||||
for y in 0..(size.height / step) as i32 + 1 {
|
||||
frame.fill(
|
||||
&Path::circle(Point::new(ox + x as f32 * step, oy + y as f32 * step), 0.65),
|
||||
DOT,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn fresh(renderer: &Renderer, size: Size, zoom: f32, pan: Vector) -> Geometry {
|
||||
let mut frame = Frame::new(renderer, size);
|
||||
paint(&mut frame, size, zoom, pan);
|
||||
frame.into_geometry()
|
||||
}
|
||||
fn batched(renderer: &Renderer, size: Size, zoom: f32, pan: Vector) -> Geometry {
|
||||
let mut frame = Frame::new(renderer, size);
|
||||
frame.fill_rectangle(Point::ORIGIN, size, BG);
|
||||
let step = (24. * zoom).max(14.);
|
||||
let ox = pan.x.rem_euclid(step);
|
||||
let oy = pan.y.rem_euclid(step);
|
||||
let path = Path::new(|p| {
|
||||
for x in 0..(size.width / step) as i32 + 1 {
|
||||
for y in 0..(size.height / step) as i32 + 1 {
|
||||
p.circle(Point::new(ox + x as f32 * step, oy + y as f32 * step), 0.65);
|
||||
p.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
frame.fill(&path, DOT);
|
||||
frame.into_geometry()
|
||||
}
|
||||
fn screenshot(renderer: &mut Renderer, size: Size, geometry: Geometry) -> Vec<u8> {
|
||||
renderer.reset(Rectangle::with_size(size));
|
||||
renderer.draw_geometry(geometry);
|
||||
Headless::screenshot(
|
||||
renderer,
|
||||
Size::new(size.width as u32, size.height as u32),
|
||||
1.,
|
||||
BG,
|
||||
)
|
||||
}
|
||||
fn measure<T>(label: &str, n: usize, mut f: impl FnMut() -> T) {
|
||||
for _ in 0..3 {
|
||||
black_box(f());
|
||||
}
|
||||
let mut values = Vec::new();
|
||||
for _ in 0..n {
|
||||
let now = Instant::now();
|
||||
let value = black_box(f());
|
||||
values.push(now.elapsed().as_secs_f64() * 1000.);
|
||||
drop(value);
|
||||
}
|
||||
values.sort_by(f64::total_cmp);
|
||||
let at = |p: f64| values[((n - 1) as f64 * p).ceil() as usize];
|
||||
println!(
|
||||
"{label}: n={n} p50_ms={:.4} p95_ms={:.4} max_ms={:.4}",
|
||||
at(0.5),
|
||||
at(0.95),
|
||||
values[n - 1]
|
||||
);
|
||||
}
|
||||
fn main() {
|
||||
let n = std::env::args()
|
||||
.nth(1)
|
||||
.map(|s| s.parse::<usize>().unwrap())
|
||||
.unwrap_or(20)
|
||||
.max(1);
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
let mut renderer = runtime
|
||||
.block_on(<Renderer as Headless>::new(
|
||||
iced::Font::DEFAULT,
|
||||
16.into(),
|
||||
None,
|
||||
))
|
||||
.expect("headless production renderer");
|
||||
println!(
|
||||
"renderer={} samples={n}; screenshot includes offscreen rendering + GPU readback, NOT native frame presentation",
|
||||
renderer.name()
|
||||
);
|
||||
for (size, zoom) in [
|
||||
(Size::new(1900., 660.), 1.),
|
||||
(Size::new(1900., 660.), 0.5),
|
||||
(Size::new(1440., 900.), 1.),
|
||||
(Size::new(1440., 900.), 0.5),
|
||||
] {
|
||||
let pan = Vector::new(-13.25, 9.75);
|
||||
let cache = Cache::new();
|
||||
let calls = Cell::new(0);
|
||||
let cached = |r: &Renderer| {
|
||||
cache.draw(r, size, |f| {
|
||||
calls.set(calls.get() + 1);
|
||||
paint(f, size, zoom, pan);
|
||||
})
|
||||
};
|
||||
let step = (24f32 * zoom).max(14.);
|
||||
println!(
|
||||
"\nsize={}x{} zoom={zoom} dots={}",
|
||||
size.width,
|
||||
size.height,
|
||||
((size.width / step) as i32 + 1) * ((size.height / step) as i32 + 1)
|
||||
);
|
||||
let baseline = fresh(&renderer, size, zoom, pan);
|
||||
let baseline_pixels = screenshot(&mut renderer, size, baseline);
|
||||
let geometry = batched(&renderer, size, zoom, pan);
|
||||
let pixels = screenshot(&mut renderer, size, geometry);
|
||||
let differences = baseline_pixels
|
||||
.iter()
|
||||
.zip(&pixels)
|
||||
.filter(|(a, b)| a != b)
|
||||
.count();
|
||||
println!("batched_rgba_byte_differences={differences}");
|
||||
assert_eq!(
|
||||
differences, 0,
|
||||
"batched path must preserve exact grid pixels"
|
||||
);
|
||||
let geometry = cached(&renderer);
|
||||
assert_eq!(baseline_pixels, screenshot(&mut renderer, size, geometry));
|
||||
let geometry = cached(&renderer);
|
||||
assert_eq!(baseline_pixels, screenshot(&mut renderer, size, geometry));
|
||||
assert_eq!(calls.get(), 1);
|
||||
println!("pixel_parity=cold_and_warm_exact");
|
||||
measure("fresh_geometry", n, || fresh(&renderer, size, zoom, pan));
|
||||
let production = grid::Grid::default();
|
||||
let g = production.draw(&renderer, size, zoom, pan);
|
||||
assert_eq!(baseline_pixels, screenshot(&mut renderer, size, g));
|
||||
measure("production_cached_geometry", n, || {
|
||||
production.draw(&renderer, size, zoom, pan)
|
||||
});
|
||||
let mut tick = 0;
|
||||
measure("production_changing_pan_geometry", n, || {
|
||||
tick += 1;
|
||||
production.draw(
|
||||
&renderer,
|
||||
size,
|
||||
zoom,
|
||||
pan + Vector::new(tick as f32 * 0.25, 0.),
|
||||
)
|
||||
});
|
||||
measure("cached_geometry", n, || cached(&renderer));
|
||||
assert_eq!(calls.get(), 1, "identical camera must reuse geometry");
|
||||
measure("fresh_geometry_and_screenshot", n, || {
|
||||
let g = fresh(&renderer, size, zoom, pan);
|
||||
screenshot(&mut renderer, size, g)
|
||||
});
|
||||
measure("cached_geometry_and_screenshot", n, || {
|
||||
let g = cached(&renderer);
|
||||
screenshot(&mut renderer, size, g)
|
||||
});
|
||||
measure("cache_forced_miss_geometry", n, || {
|
||||
cache.clear();
|
||||
cached(&renderer)
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user