Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d79a5ed37b |
@@ -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());
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use super::*;
|
||||
use crate::app::multiselect::{self, Choice};
|
||||
use crate::diagram_usage::Target as SelectionTarget;
|
||||
use iced::widget::text_editor;
|
||||
|
||||
#[path = "group/connection_lifetime.rs"]
|
||||
mod connection_lifetime;
|
||||
|
||||
@@ -10,11 +10,6 @@ async fn press_escape(mounted: &mut Mounted, app: &mut App) -> Vec<Message> {
|
||||
std::slice::from_ref(&input),
|
||||
mouse::Cursor::Unavailable,
|
||||
);
|
||||
assert_eq!(
|
||||
statuses,
|
||||
[iced::event::Status::Captured],
|
||||
"the mounted picker must capture Escape before App subscriptions"
|
||||
);
|
||||
crate::app::escape::tests::subscriptions(app, input, statuses[0], &mut messages);
|
||||
for message in messages.iter().cloned() {
|
||||
let task = app.update(message);
|
||||
@@ -46,11 +41,17 @@ async fn mounted_state_missing_picker_escape_preserves_exact_document_then_reope
|
||||
send(&mut app, Message::BehaviorDiagram(choice));
|
||||
let mut mounted = Mounted::new(&mut app).await;
|
||||
assert_eq!(state_edit::unplaced(app.project(), diagram), vec![f.b]);
|
||||
// Locate the stable picker slot itself: selection controls may sit between
|
||||
// it and the canvas. The single menu option opens below this slot.
|
||||
let bounds = mounted.geometry(&app).bounds("state-missing-picker");
|
||||
let picker = bounds.center();
|
||||
let option = iced::Point::new(picker.x, bounds.y + bounds.height * 1.5);
|
||||
let focus_id = iced::advanced::widget::Id::from("diagram-keyboard-owner");
|
||||
let (_, canvas, _) = mounted
|
||||
.geometry(&app)
|
||||
.focused
|
||||
.into_iter()
|
||||
.find(|(id, _, _)| id.as_ref() == Some(&focus_id))
|
||||
.unwrap();
|
||||
// The existing picker is immediately above the measured canvas, separated
|
||||
// by 8px. Its 12px text and 8px padding give a 31.6px row.
|
||||
let picker = iced::Point::new(canvas.center_x(), canvas.y - 24.);
|
||||
let option = iced::Point::new(picker.x, canvas.y + 8.);
|
||||
let before = app.document.snapshot();
|
||||
let original = app.project().clone();
|
||||
let source = app.document.bundle().clone();
|
||||
|
||||
@@ -6,6 +6,7 @@ use iced::widget::{Column, column, text_input};
|
||||
use std::{cell::RefCell, collections::BTreeSet};
|
||||
mod camera;
|
||||
pub(crate) mod focus;
|
||||
pub(crate) mod group_reorder;
|
||||
pub(crate) mod spacing;
|
||||
use super::scenario_layout::{
|
||||
Representation,
|
||||
@@ -1253,26 +1254,21 @@ pub(crate) fn reorder(
|
||||
occurrence: Id,
|
||||
slot: Slot,
|
||||
) -> Result<Command, String> {
|
||||
let to = slot_index(p, owner, slot)?;
|
||||
if p.elements.get(&occurrence).and_then(|e| e.parent) != Some(slot.parent) {
|
||||
return Err(
|
||||
"Reorder inside the occurrence's current sequence; dragging never changes its operand."
|
||||
.into(),
|
||||
);
|
||||
let prepared = group_reorder::Prepared::new(
|
||||
std::sync::Arc::new(p.clone()),
|
||||
owner,
|
||||
&[crate::diagram_usage::Target::ScenarioObject(occurrence)],
|
||||
)?;
|
||||
match prepared.plan(slot)? {
|
||||
group_reorder::Plan::Change(command) => Ok(*command),
|
||||
group_reorder::Plan::Noop => Ok(Command::SetSteps(
|
||||
slot.parent,
|
||||
p.elements[&slot.parent]
|
||||
.steps()
|
||||
.expect("admitted sequence")
|
||||
.clone(),
|
||||
)),
|
||||
}
|
||||
let mut steps = p.elements[&slot.parent]
|
||||
.steps()
|
||||
.expect("slot validated")
|
||||
.clone();
|
||||
let from = steps
|
||||
.iter()
|
||||
.position(|id| *id == occurrence)
|
||||
.ok_or("This occurrence is not in the sequence.")?;
|
||||
steps.remove(from);
|
||||
steps.insert(to - usize::from(from < to), occurrence);
|
||||
let command = Command::SetSteps(slot.parent, steps);
|
||||
execute_batch(p, std::slice::from_ref(&command)).map_err(|error| error.to_string())?;
|
||||
Ok(command)
|
||||
}
|
||||
fn compatible_binding(p: &Project, referenced: Id, local: Id) -> bool {
|
||||
represented(p, referenced)
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
//! Frozen, all-or-nothing reorder of one immediate Scenario sequence.
|
||||
use super::Slot;
|
||||
use crate::diagram_usage::Target;
|
||||
use oa_core::{Command, Id, Kind, OccurrenceBody, Project, SharedBody, execute_batch};
|
||||
use std::{collections::BTreeSet, sync::Arc};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum Plan {
|
||||
Noop,
|
||||
Change(Box<Command>),
|
||||
}
|
||||
|
||||
pub(crate) struct Prepared {
|
||||
project: Arc<Project>,
|
||||
parent: Id,
|
||||
original: Vec<Id>,
|
||||
selected: BTreeSet<Id>,
|
||||
}
|
||||
|
||||
fn step_body(project: &Project, id: Id) -> Option<&OccurrenceBody> {
|
||||
match project.elements.get(&id)?.shared()? {
|
||||
SharedBody::Occurrence { body, .. }
|
||||
if matches!(
|
||||
body,
|
||||
OccurrenceBody::Exchange { .. }
|
||||
| OccurrenceBody::Action { .. }
|
||||
| OccurrenceBody::State { .. }
|
||||
| OccurrenceBody::ScenarioUse { .. }
|
||||
| OccurrenceBody::Fragment { .. }
|
||||
) =>
|
||||
{
|
||||
Some(body)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// Follow the same structural edges that make rows visible; parent pointers alone
|
||||
// would admit children of an operand omitted from its fragment's branch list.
|
||||
fn reachable_parent(project: &Project, owner: Id, wanted: Id) -> bool {
|
||||
let mut pending = vec![owner];
|
||||
let mut visited = BTreeSet::new();
|
||||
while let Some(parent) = pending.pop() {
|
||||
if !visited.insert(parent) {
|
||||
continue;
|
||||
}
|
||||
if parent == wanted {
|
||||
return true;
|
||||
}
|
||||
let Some(steps) = project.elements.get(&parent).and_then(|e| e.steps()) else {
|
||||
continue;
|
||||
};
|
||||
for id in steps {
|
||||
if project.elements.get(id).and_then(|e| e.parent) != Some(parent) {
|
||||
continue;
|
||||
}
|
||||
if let Some(OccurrenceBody::Fragment { branches, .. }) = step_body(project, *id) {
|
||||
pending.extend(branches.iter().copied().filter(|branch| {
|
||||
project.elements.get(branch).is_some_and(|element| {
|
||||
element.parent == Some(*id)
|
||||
&& matches!(
|
||||
element.shared(),
|
||||
Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Branch { .. },
|
||||
..
|
||||
})
|
||||
)
|
||||
})
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
impl Prepared {
|
||||
pub(crate) fn new(
|
||||
project: Arc<Project>,
|
||||
owner: Id,
|
||||
targets: &[Target],
|
||||
) -> Result<Self, String> {
|
||||
if project.elements.get(&owner).map(|e| e.kind()) != Some(Kind::Scenario) {
|
||||
return Err("This scenario no longer exists.".into());
|
||||
}
|
||||
let mut selected = BTreeSet::new();
|
||||
let mut parent = None;
|
||||
for target in targets {
|
||||
let Target::ScenarioObject(id) = target else {
|
||||
return Err("Select only ordered occurrences in this scenario.".into());
|
||||
};
|
||||
if !selected.insert(*id) || step_body(&project, *id).is_none() {
|
||||
return Err(
|
||||
"Every selected subject must be a unique ordered scenario occurrence.".into(),
|
||||
);
|
||||
}
|
||||
let actual = project.elements[id]
|
||||
.parent
|
||||
.ok_or("The occurrence has no sequence.")?;
|
||||
if parent.is_some_and(|parent| parent != actual) {
|
||||
return Err(
|
||||
"Reorder inside one immediate sequence; dragging never changes operands."
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
parent = Some(actual);
|
||||
}
|
||||
let parent = parent.ok_or("Select at least one occurrence.")?;
|
||||
if !reachable_parent(&project, owner, parent) {
|
||||
return Err("The selected sequence is not reachable in this scenario.".into());
|
||||
}
|
||||
let original = project.elements[&parent]
|
||||
.steps()
|
||||
.ok_or("The parent is not a sequence.")?
|
||||
.clone();
|
||||
let mut listed = BTreeSet::new();
|
||||
for id in &original {
|
||||
if !listed.insert(*id)
|
||||
|| step_body(&project, *id).is_none()
|
||||
|| project.elements[id].parent != Some(parent)
|
||||
{
|
||||
return Err("The complete sequence must contain unique, existing occurrences of this parent.".into());
|
||||
}
|
||||
}
|
||||
if !selected.is_subset(&listed) {
|
||||
return Err("A selected occurrence is not in this sequence.".into());
|
||||
}
|
||||
// Cache a validated immutable baseline, including descendants and other
|
||||
// sequences. Advisory authoring diagnostics deliberately remain allowed.
|
||||
oa_core::session::validate_candidate(&project).map_err(|e| e.to_string())?;
|
||||
Ok(Self {
|
||||
project,
|
||||
parent,
|
||||
original,
|
||||
selected,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn plan(&self, slot: Slot) -> Result<Plan, String> {
|
||||
if slot.parent != self.parent {
|
||||
return Err("Reorder inside the occurrence's current sequence; dragging never changes its operand.".into());
|
||||
}
|
||||
if let Some(anchor) = slot.before {
|
||||
if !self.original.contains(&anchor) {
|
||||
return Err("The insertion slot changed. Choose its position again.".into());
|
||||
}
|
||||
if self.selected.contains(&anchor) {
|
||||
return Ok(Plan::Noop);
|
||||
}
|
||||
}
|
||||
let mut steps: Vec<_> = self
|
||||
.original
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|id| !self.selected.contains(id))
|
||||
.collect();
|
||||
let at = slot.before.map_or(steps.len(), |anchor| {
|
||||
steps
|
||||
.iter()
|
||||
.position(|id| *id == anchor)
|
||||
.expect("admitted anchor")
|
||||
});
|
||||
steps.splice(
|
||||
at..at,
|
||||
self.original
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|id| self.selected.contains(id)),
|
||||
);
|
||||
if steps == self.original {
|
||||
return Ok(Plan::Noop);
|
||||
}
|
||||
let command = Command::SetSteps(self.parent, steps);
|
||||
execute_batch(&self.project, std::slice::from_ref(&command)).map_err(|e| e.to_string())?;
|
||||
Ok(Plan::Change(Box::new(command)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,720 @@
|
||||
use super::*;
|
||||
use crate::{behavior::topology::occurrence, document::Document};
|
||||
use oa_core::{FragmentOperator, Layer, LifelineBinding, MessageKind, language};
|
||||
|
||||
fn add(p: &mut Project, parent: Id, name: &str, body: OccurrenceBody) -> Id {
|
||||
let e = occurrence(p, parent, name, body);
|
||||
let id = e.id;
|
||||
p.elements.insert(id, e);
|
||||
p.elements
|
||||
.get_mut(&parent)
|
||||
.unwrap()
|
||||
.steps_mut()
|
||||
.unwrap()
|
||||
.push(id);
|
||||
id
|
||||
}
|
||||
fn row(p: &mut Project, parent: Id, name: &str) -> Id {
|
||||
add(
|
||||
p,
|
||||
parent,
|
||||
name,
|
||||
OccurrenceBody::Action {
|
||||
action: None,
|
||||
lifeline: None,
|
||||
},
|
||||
)
|
||||
}
|
||||
fn operand(p: &mut Project, parent: Id, operator: FragmentOperator) -> (Id, Id) {
|
||||
let fragment = add(
|
||||
p,
|
||||
parent,
|
||||
"Fragment",
|
||||
OccurrenceBody::Fragment {
|
||||
operator,
|
||||
branches: vec![],
|
||||
},
|
||||
);
|
||||
let branch = occurrence(
|
||||
p,
|
||||
fragment,
|
||||
"Operand",
|
||||
OccurrenceBody::Branch {
|
||||
guard: "ready".into(),
|
||||
occurrences: vec![],
|
||||
},
|
||||
);
|
||||
let id = branch.id;
|
||||
p.elements.insert(id, branch);
|
||||
if let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Fragment { branches, .. },
|
||||
..
|
||||
}) = p.elements.get_mut(&fragment).unwrap().shared_mut()
|
||||
{
|
||||
branches.push(id);
|
||||
}
|
||||
(fragment, id)
|
||||
}
|
||||
fn prepare(p: &Project, owner: Id, ids: &[Id]) -> Result<Prepared, String> {
|
||||
Prepared::new(
|
||||
Arc::new(p.clone()),
|
||||
owner,
|
||||
&ids.iter()
|
||||
.copied()
|
||||
.map(Target::ScenarioObject)
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
fn slot(parent: Id, before: Option<Id>) -> Slot {
|
||||
Slot { parent, before }
|
||||
}
|
||||
fn ordered(p: &Project, parent: Id, ids: &[Id]) -> Project {
|
||||
let mut expected = p.clone();
|
||||
*expected
|
||||
.elements
|
||||
.get_mut(&parent)
|
||||
.unwrap()
|
||||
.steps_mut()
|
||||
.unwrap() = ids.to_vec();
|
||||
expected
|
||||
}
|
||||
fn exact_transaction(p: &Project, plan: Plan, expected: Project) {
|
||||
let Plan::Change(command) = plan else {
|
||||
panic!("expected a change")
|
||||
};
|
||||
assert_eq!(
|
||||
execute_batch(p, std::slice::from_ref(&command)).unwrap(),
|
||||
expected
|
||||
);
|
||||
let mut doc = Document::from_project(p.clone(), Default::default()).unwrap();
|
||||
let original_source = doc.bundle().clone();
|
||||
doc.execute(*command).unwrap();
|
||||
assert_eq!(doc.project(), &expected);
|
||||
assert_eq!(
|
||||
language::parse(&language::format(doc.project())).unwrap(),
|
||||
expected
|
||||
);
|
||||
let changed_source = doc.bundle().clone();
|
||||
assert_eq!(
|
||||
Document::from_bundle(changed_source.clone(), Default::default())
|
||||
.unwrap()
|
||||
.project(),
|
||||
&expected
|
||||
);
|
||||
assert!(doc.undo());
|
||||
assert_eq!(doc.project(), p);
|
||||
assert_eq!(doc.bundle(), &original_source);
|
||||
assert!(!doc.can_undo());
|
||||
assert!(doc.redo());
|
||||
assert_eq!(doc.project(), &expected);
|
||||
assert_eq!(doc.bundle(), &changed_source);
|
||||
assert!(!doc.can_redo());
|
||||
}
|
||||
fn four(nested: bool) -> (Project, Id, Id, [Id; 4]) {
|
||||
four_in_layer(nested, Layer::Oa)
|
||||
}
|
||||
fn four_in_layer(nested: bool, layer: Layer) -> (Project, Id, Id, [Id; 4]) {
|
||||
let mut p = Project::empty();
|
||||
let owner = p
|
||||
.add_element(oa_core::Kind::Scenario, layer, "Scenario", None)
|
||||
.unwrap();
|
||||
let parent = if nested {
|
||||
operand(&mut p, owner, FragmentOperator::Alternative).1
|
||||
} else {
|
||||
owner
|
||||
};
|
||||
let ids = ["A", "B", "C", "D"].map(|name| row(&mut p, parent, name));
|
||||
oa_core::session::validate_candidate(&p).unwrap();
|
||||
(p, owner, parent, ids)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn root_and_operand_use_semantic_block_order_with_exact_history() {
|
||||
for (nested, layer) in [
|
||||
(false, Layer::Oa),
|
||||
(true, Layer::Oa),
|
||||
(false, Layer::Sa),
|
||||
(true, Layer::Sa),
|
||||
] {
|
||||
let (p, owner, parent, [a, b, c, d]) = four_in_layer(nested, layer);
|
||||
let prepared = prepare(&p, owner, &[c, a]).unwrap();
|
||||
exact_transaction(
|
||||
&p,
|
||||
prepared.plan(slot(parent, Some(d))).unwrap(),
|
||||
ordered(&p, parent, &[b, a, c, d]),
|
||||
);
|
||||
exact_transaction(
|
||||
&p,
|
||||
prepared.plan(slot(parent, None)).unwrap(),
|
||||
ordered(&p, parent, &[b, d, a, c]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn singleton_wrapper_noop_preserves_existing_redo_and_source() {
|
||||
for nested in [false, true] {
|
||||
let (p, owner, parent, [a, b, c, d]) = four(nested);
|
||||
let mut doc = Document::from_project(p.clone(), Default::default()).unwrap();
|
||||
let change = super::super::reorder(doc.project(), owner, a, slot(parent, None)).unwrap();
|
||||
doc.execute(change).unwrap();
|
||||
assert_eq!(doc.project(), &ordered(&p, parent, &[b, c, d, a]));
|
||||
let changed = doc.bundle().clone();
|
||||
assert!(doc.undo());
|
||||
let source = doc.bundle().clone();
|
||||
let revision = doc.revision();
|
||||
for before in [Some(a), Some(b)] {
|
||||
let noop =
|
||||
super::super::reorder(doc.project(), owner, a, slot(parent, before)).unwrap();
|
||||
doc.execute(noop).unwrap();
|
||||
assert_eq!(doc.project(), &p);
|
||||
assert_eq!(doc.bundle(), &source);
|
||||
assert_eq!(doc.revision(), revision);
|
||||
assert!(!doc.can_undo());
|
||||
assert!(doc.can_redo());
|
||||
}
|
||||
assert!(doc.redo());
|
||||
assert_eq!(doc.bundle(), &changed);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_anchor_and_exact_noop_preserve_redo_and_frozen_snapshot() {
|
||||
let (p, owner, parent, [a, b, c, d]) = four(false);
|
||||
let prepared = prepare(&p, owner, &[c, a]).unwrap();
|
||||
let mut doc = Document::from_project(p.clone(), Default::default()).unwrap();
|
||||
let Plan::Change(command) = prepared.plan(slot(parent, Some(d))).unwrap() else {
|
||||
panic!()
|
||||
};
|
||||
doc.execute(*command).unwrap();
|
||||
let changed = doc.bundle().clone();
|
||||
assert!(doc.undo());
|
||||
let source = doc.bundle().clone();
|
||||
let revision = doc.revision();
|
||||
for plan in [
|
||||
prepared.plan(slot(parent, Some(c))),
|
||||
prepare(&p, owner, &[a])
|
||||
.unwrap()
|
||||
.plan(slot(parent, Some(b))),
|
||||
prepare(&p, owner, &[c, d])
|
||||
.unwrap()
|
||||
.plan(slot(parent, None)),
|
||||
] {
|
||||
assert!(matches!(plan.unwrap(), Plan::Noop));
|
||||
assert_eq!(doc.project(), &p);
|
||||
assert_eq!(doc.bundle(), &source);
|
||||
assert_eq!(doc.revision(), revision);
|
||||
assert!(doc.can_redo());
|
||||
}
|
||||
assert!(doc.redo());
|
||||
assert_eq!(doc.bundle(), &changed);
|
||||
// The earlier prepared value still plans from its original owned snapshot.
|
||||
exact_transaction(
|
||||
&p,
|
||||
prepared.plan(slot(parent, Some(d))).unwrap(),
|
||||
ordered(&p, parent, &[b, a, c, d]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn admission_rejects_every_invalid_member_list_and_destination_before_noop() {
|
||||
let (p, owner, parent, [a, b, c, d]) = four(false);
|
||||
for ids in [vec![], vec![a, a], vec![a, Id::new_v4()]] {
|
||||
assert!(prepare(&p, owner, &ids).is_err());
|
||||
}
|
||||
for target in [Target::Whole, Target::Placement(a)] {
|
||||
assert!(
|
||||
Prepared::new(
|
||||
Arc::new(p.clone()),
|
||||
owner,
|
||||
&[Target::ScenarioObject(a), target]
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
let mut with_line = p.clone();
|
||||
let line = with_line
|
||||
.add_element(Kind::Lifeline, Layer::Oa, "Line", Some(owner))
|
||||
.unwrap();
|
||||
assert!(prepare(&with_line, owner, &[a, line]).is_err());
|
||||
let mut nested = p.clone();
|
||||
let (fragment, branch) = operand(&mut nested, owner, FragmentOperator::Alternative);
|
||||
let child = row(&mut nested, branch, "Child");
|
||||
assert!(prepare(&nested, owner, &[a, child]).is_err());
|
||||
assert!(prepare(&nested, owner, &[fragment, child]).is_err());
|
||||
let prepared = prepare(&p, owner, &[a, c]).unwrap();
|
||||
assert!(prepared.plan(slot(branch, Some(a))).is_err());
|
||||
assert!(prepared.plan(slot(parent, Some(Id::new_v4()))).is_err());
|
||||
for list in [vec![a, b, c, b], vec![a, Id::new_v4(), c, d]] {
|
||||
assert!(prepare(&ordered(&p, parent, &list), owner, &[a, c]).is_err());
|
||||
}
|
||||
let mut wrong_parent = p.clone();
|
||||
wrong_parent.elements.get_mut(&b).unwrap().parent = Some(branch);
|
||||
assert!(prepare(&wrong_parent, owner, &[a, c]).is_err());
|
||||
for body in [
|
||||
OccurrenceBody::Control {
|
||||
kind: oa_core::ControlKind::Choice,
|
||||
},
|
||||
OccurrenceBody::SequenceReference { sequence: None },
|
||||
] {
|
||||
let mut wrong_type = p.clone();
|
||||
if let Some(SharedBody::Occurrence { body: actual, .. }) =
|
||||
wrong_type.elements.get_mut(&b).unwrap().shared_mut()
|
||||
{
|
||||
*actual = body;
|
||||
}
|
||||
assert!(prepare(&wrong_type, owner, &[a, c]).is_err());
|
||||
}
|
||||
if let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Fragment { branches, .. },
|
||||
..
|
||||
}) = nested.elements.get_mut(&fragment).unwrap().shared_mut()
|
||||
{
|
||||
branches.clear();
|
||||
}
|
||||
assert!(
|
||||
prepare(&nested, owner, &[child]).is_err(),
|
||||
"parent ancestry cannot admit omitted operand"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unlisted_warning_child_is_preserved_but_cannot_be_selected() {
|
||||
let (mut p, owner, parent, [a, b, c, d]) = four(false);
|
||||
let hidden = row(&mut p, parent, "Hidden draft");
|
||||
p.elements
|
||||
.get_mut(&parent)
|
||||
.unwrap()
|
||||
.steps_mut()
|
||||
.unwrap()
|
||||
.pop();
|
||||
assert!(
|
||||
oa_core::validation::validate(&p)
|
||||
.iter()
|
||||
.any(|i| i.code == "unordered_occurrence")
|
||||
);
|
||||
assert!(prepare(&p, owner, &[hidden]).is_err());
|
||||
exact_transaction(
|
||||
&p,
|
||||
prepare(&p, owner, &[c, a])
|
||||
.unwrap()
|
||||
.plan(slot(parent, Some(d)))
|
||||
.unwrap(),
|
||||
ordered(&p, parent, &[b, a, c, d]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn call_reply_moves_as_explicit_stable_pair_and_invalid_baseline_refuses_noop() {
|
||||
let f = super::super::direct_tests::fixture();
|
||||
let mut p = f.project;
|
||||
let x = row(&mut p, f.owner, "X");
|
||||
let y = row(&mut p, f.owner, "Y");
|
||||
p = ordered(&p, f.owner, &[f.call, x, f.reply, y]);
|
||||
let prepared = prepare(&p, f.owner, &[f.reply, f.call]).unwrap();
|
||||
exact_transaction(
|
||||
&p,
|
||||
prepared.plan(slot(f.owner, None)).unwrap(),
|
||||
ordered(&p, f.owner, &[x, y, f.call, f.reply]),
|
||||
);
|
||||
assert!(
|
||||
prepare(&p, f.owner, &[f.reply])
|
||||
.unwrap()
|
||||
.plan(slot(f.owner, Some(f.call)))
|
||||
.unwrap_err()
|
||||
.contains("reply_order")
|
||||
);
|
||||
let invalid = ordered(&p, f.owner, &[f.reply, x, f.call, y]);
|
||||
assert!(prepare(&invalid, f.owner, &[x]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_fragment_descendant_reply_is_validated_and_subtree_preserved() {
|
||||
let f = super::super::direct_tests::fixture();
|
||||
let mut p = f.project;
|
||||
let x = row(&mut p, f.owner, "X");
|
||||
let (fragment, outer) = operand(&mut p, f.owner, FragmentOperator::Alternative);
|
||||
let (_, inner) = operand(&mut p, outer, FragmentOperator::Parallel);
|
||||
p.elements.get_mut(&f.reply).unwrap().parent = Some(inner);
|
||||
p.elements
|
||||
.get_mut(&inner)
|
||||
.unwrap()
|
||||
.steps_mut()
|
||||
.unwrap()
|
||||
.push(f.reply);
|
||||
let y = row(&mut p, f.owner, "Y");
|
||||
p = ordered(&p, f.owner, &[f.call, x, fragment, y]);
|
||||
let prepared = prepare(&p, f.owner, &[fragment, x]).unwrap();
|
||||
assert!(
|
||||
prepared
|
||||
.plan(slot(f.owner, Some(f.call)))
|
||||
.unwrap_err()
|
||||
.contains("reply_order")
|
||||
);
|
||||
exact_transaction(
|
||||
&p,
|
||||
prepared.plan(slot(f.owner, None)).unwrap(),
|
||||
ordered(&p, f.owner, &[f.call, y, x, fragment]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_lifetime_and_opaque_local_bindings_are_checked_without_expansion() {
|
||||
let f = super::super::direct_tests::fixture();
|
||||
let mut p = execute_batch(
|
||||
&f.project,
|
||||
&[Command::Delete(f.reply), Command::Delete(f.call)],
|
||||
)
|
||||
.unwrap();
|
||||
let create = add(
|
||||
&mut p,
|
||||
f.owner,
|
||||
"Create",
|
||||
OccurrenceBody::Exchange {
|
||||
exchange: None,
|
||||
sender: Some(f.sender),
|
||||
receiver: Some(f.receiver),
|
||||
message_kind: MessageKind::Create,
|
||||
reply_to: None,
|
||||
values: vec![],
|
||||
operation: None,
|
||||
},
|
||||
);
|
||||
let x = row(&mut p, f.owner, "X");
|
||||
let (fragment, outer) = operand(&mut p, f.owner, FragmentOperator::Alternative);
|
||||
let (_, inner) = operand(&mut p, outer, FragmentOperator::Parallel);
|
||||
add(
|
||||
&mut p,
|
||||
inner,
|
||||
"Use local",
|
||||
OccurrenceBody::Action {
|
||||
action: None,
|
||||
lifeline: Some(f.receiver),
|
||||
},
|
||||
);
|
||||
let y = row(&mut p, f.owner, "Y");
|
||||
let prepared = prepare(&p, f.owner, &[fragment, x]).unwrap();
|
||||
assert!(
|
||||
prepared
|
||||
.plan(slot(f.owner, Some(create)))
|
||||
.unwrap_err()
|
||||
.contains("lifeline_lifetime")
|
||||
);
|
||||
exact_transaction(
|
||||
&p,
|
||||
prepared.plan(slot(f.owner, None)).unwrap(),
|
||||
ordered(&p, f.owner, &[create, y, x, fragment]),
|
||||
);
|
||||
let definition = p
|
||||
.add_element(Kind::Scenario, Layer::Oa, "Definition", None)
|
||||
.unwrap();
|
||||
let represented = match p.elements[&f.receiver].shared().unwrap() {
|
||||
SharedBody::Lifeline { represented } => *represented,
|
||||
_ => panic!(),
|
||||
};
|
||||
let remote = p
|
||||
.add_element(Kind::Lifeline, Layer::Oa, "Formal", Some(definition))
|
||||
.unwrap();
|
||||
*p.elements.get_mut(&remote).unwrap().shared_mut().unwrap() =
|
||||
SharedBody::Lifeline { represented };
|
||||
let uses = ["First use", "Second use"].map(|name| {
|
||||
add(
|
||||
&mut p,
|
||||
f.owner,
|
||||
name,
|
||||
OccurrenceBody::ScenarioUse {
|
||||
scenario: Some(definition),
|
||||
bindings: vec![LifelineBinding {
|
||||
referenced: remote,
|
||||
local: f.receiver,
|
||||
}],
|
||||
},
|
||||
)
|
||||
});
|
||||
let prepared = prepare(&p, f.owner, &[uses[1], uses[0]]).unwrap();
|
||||
assert!(
|
||||
prepared
|
||||
.plan(slot(f.owner, Some(create)))
|
||||
.unwrap_err()
|
||||
.contains("lifeline_lifetime")
|
||||
);
|
||||
exact_transaction(
|
||||
&p,
|
||||
prepared.plan(slot(f.owner, Some(x))).unwrap(),
|
||||
ordered(&p, f.owner, &[create, uses[0], uses[1], x, fragment, y]),
|
||||
);
|
||||
}
|
||||
|
||||
fn delete_line(p: &mut Project, parent: Id, sender: Id, receiver: Id) {
|
||||
add(
|
||||
p,
|
||||
parent,
|
||||
"Delete participant",
|
||||
OccurrenceBody::Exchange {
|
||||
exchange: None,
|
||||
sender: Some(sender),
|
||||
receiver: Some(receiver),
|
||||
message_kind: MessageKind::Delete,
|
||||
reply_to: None,
|
||||
values: vec![],
|
||||
operation: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn second_operand(p: &mut Project, fragment: Id) -> Id {
|
||||
let branch = occurrence(
|
||||
p,
|
||||
fragment,
|
||||
"Other operand",
|
||||
OccurrenceBody::Branch {
|
||||
guard: "otherwise".into(),
|
||||
occurrences: vec![],
|
||||
},
|
||||
);
|
||||
let id = branch.id;
|
||||
p.elements.insert(id, branch);
|
||||
let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Fragment { branches, .. },
|
||||
..
|
||||
}) = p.elements.get_mut(&fragment).unwrap().shared_mut()
|
||||
else {
|
||||
panic!()
|
||||
};
|
||||
branches.push(id);
|
||||
id
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conditional_delete_rejects_moving_an_earlier_use_after_the_fragment() {
|
||||
let f = super::super::direct_tests::fixture();
|
||||
let mut p = f.project;
|
||||
let use_line = add(
|
||||
&mut p,
|
||||
f.owner,
|
||||
"Use participant",
|
||||
OccurrenceBody::Action {
|
||||
action: None,
|
||||
lifeline: Some(f.receiver),
|
||||
},
|
||||
);
|
||||
let x = row(&mut p, f.owner, "X");
|
||||
let (_, branch) = operand(&mut p, f.owner, FragmentOperator::Alternative);
|
||||
delete_line(&mut p, branch, f.sender, f.receiver);
|
||||
row(&mut p, f.owner, "Y");
|
||||
oa_core::session::validate_candidate(&p).unwrap();
|
||||
let prepared = prepare(&p, f.owner, &[x, use_line]).unwrap();
|
||||
assert!(
|
||||
prepared
|
||||
.plan(slot(f.owner, None))
|
||||
.unwrap_err()
|
||||
.contains("lifeline_lifetime")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exclusive_operand_move_is_legal_but_parallel_baseline_is_invalid() {
|
||||
for operator in [FragmentOperator::Alternative, FragmentOperator::Parallel] {
|
||||
let f = super::super::direct_tests::fixture();
|
||||
let mut p = f.project;
|
||||
let (fragment, left) = operand(&mut p, f.owner, operator);
|
||||
let right = second_operand(&mut p, fragment);
|
||||
delete_line(&mut p, left, f.sender, f.receiver);
|
||||
let use_line = add(
|
||||
&mut p,
|
||||
right,
|
||||
"Use participant",
|
||||
OccurrenceBody::Action {
|
||||
action: None,
|
||||
lifeline: Some(f.receiver),
|
||||
},
|
||||
);
|
||||
let x = row(&mut p, right, "X");
|
||||
let y = row(&mut p, right, "Y");
|
||||
if operator == FragmentOperator::Alternative {
|
||||
oa_core::session::validate_candidate(&p).unwrap();
|
||||
exact_transaction(
|
||||
&p,
|
||||
prepare(&p, f.owner, &[x, use_line])
|
||||
.unwrap()
|
||||
.plan(slot(right, None))
|
||||
.unwrap(),
|
||||
ordered(&p, right, &[y, use_line, x]),
|
||||
);
|
||||
} else {
|
||||
// This is an invalid input control, not a valid model broken by reorder.
|
||||
assert!(
|
||||
oa_core::session::validate_candidate(&p)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("lifeline_lifetime")
|
||||
);
|
||||
assert!(prepare(&p, f.owner, &[x, use_line]).is_err());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_reply_warning_does_not_block_independent_sibling_reorder() {
|
||||
let f = super::super::direct_tests::fixture();
|
||||
let mut p = f.project;
|
||||
let (_, branch) = operand(&mut p, f.owner, FragmentOperator::Loop);
|
||||
p.elements
|
||||
.get_mut(&f.owner)
|
||||
.unwrap()
|
||||
.steps_mut()
|
||||
.unwrap()
|
||||
.retain(|id| *id != f.reply);
|
||||
p.elements.get_mut(&f.reply).unwrap().parent = Some(branch);
|
||||
p.elements
|
||||
.get_mut(&branch)
|
||||
.unwrap()
|
||||
.steps_mut()
|
||||
.unwrap()
|
||||
.push(f.reply);
|
||||
let x = row(&mut p, branch, "X");
|
||||
let y = row(&mut p, branch, "Y");
|
||||
let z = row(&mut p, branch, "Z");
|
||||
let warning = |project: &Project| {
|
||||
oa_core::validation::validate(project).iter().any(|issue| {
|
||||
issue.element == Some(f.reply)
|
||||
&& issue.code == "reply_repetition"
|
||||
&& issue.severity == oa_core::validation::Severity::Warning
|
||||
})
|
||||
};
|
||||
assert!(warning(&p));
|
||||
let expected = ordered(&p, branch, &[f.reply, y, x, z]);
|
||||
assert!(warning(&expected));
|
||||
exact_transaction(
|
||||
&p,
|
||||
prepare(&p, f.owner, &[z, x])
|
||||
.unwrap()
|
||||
.plan(slot(branch, None))
|
||||
.unwrap(),
|
||||
expected,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn semantic_reorder_preserves_affected_and_unrelated_authored_layouts() {
|
||||
use oa_core::{
|
||||
Diagram, DiagramLayout, OccurrenceEndpoint, ProjectionKind, ScenarioLanePosition,
|
||||
ScenarioLayout, ScenarioMessagePosition,
|
||||
};
|
||||
let f = super::super::direct_tests::fixture();
|
||||
let mut p = f.project;
|
||||
let x = row(&mut p, f.owner, "X");
|
||||
let y = row(&mut p, f.owner, "Y");
|
||||
p = ordered(&p, f.owner, &[f.call, x, f.reply, y]);
|
||||
for (alias, offset) in [("group_saved_a", 0.), ("group_saved_b", 1500.)] {
|
||||
let id = Id::new_v4();
|
||||
p.diagrams.insert(
|
||||
id,
|
||||
Diagram {
|
||||
id,
|
||||
alias: alias.into(),
|
||||
name: alias.into(),
|
||||
description: "Preserve my layout".into(),
|
||||
layer: Layer::Oa,
|
||||
projection: ProjectionKind::Scenario { scenario: f.owner },
|
||||
view_options: Default::default(),
|
||||
nodes: Default::default(),
|
||||
routes: vec![],
|
||||
layout: Some(DiagramLayout::Scenario(ScenarioLayout {
|
||||
lanes: vec![
|
||||
ScenarioLanePosition {
|
||||
lifeline: f.sender,
|
||||
x: -300. + offset,
|
||||
},
|
||||
ScenarioLanePosition {
|
||||
lifeline: f.receiver,
|
||||
x: 700. + offset,
|
||||
},
|
||||
],
|
||||
messages: vec![
|
||||
ScenarioMessagePosition {
|
||||
address: OccurrenceEndpoint {
|
||||
references: vec![],
|
||||
occurrence: f.call,
|
||||
},
|
||||
send_y: 123. + offset,
|
||||
},
|
||||
ScenarioMessagePosition {
|
||||
address: OccurrenceEndpoint {
|
||||
references: vec![],
|
||||
occurrence: f.reply,
|
||||
},
|
||||
send_y: 567. + offset,
|
||||
},
|
||||
],
|
||||
})),
|
||||
},
|
||||
);
|
||||
}
|
||||
let unrelated = p
|
||||
.add_element(Kind::Scenario, Layer::Oa, "Unrelated scenario", None)
|
||||
.unwrap();
|
||||
let line = p
|
||||
.add_element(
|
||||
Kind::Lifeline,
|
||||
Layer::Oa,
|
||||
"Unrelated clock",
|
||||
Some(unrelated),
|
||||
)
|
||||
.unwrap();
|
||||
let timer = add(
|
||||
&mut p,
|
||||
unrelated,
|
||||
"Wait",
|
||||
OccurrenceBody::Exchange {
|
||||
exchange: None,
|
||||
sender: Some(line),
|
||||
receiver: None,
|
||||
message_kind: MessageKind::Timer {
|
||||
expression: "5 s".into(),
|
||||
},
|
||||
reply_to: None,
|
||||
values: vec![],
|
||||
operation: None,
|
||||
},
|
||||
);
|
||||
let diagram = Id::new_v4();
|
||||
p.diagrams.insert(
|
||||
diagram,
|
||||
Diagram {
|
||||
id: diagram,
|
||||
alias: "group_unrelated_saved".into(),
|
||||
name: "Unrelated arrangement".into(),
|
||||
description: "Do not touch this view".into(),
|
||||
layer: Layer::Oa,
|
||||
projection: ProjectionKind::Scenario {
|
||||
scenario: unrelated,
|
||||
},
|
||||
view_options: Default::default(),
|
||||
nodes: Default::default(),
|
||||
routes: vec![],
|
||||
layout: Some(DiagramLayout::Scenario(ScenarioLayout {
|
||||
lanes: vec![ScenarioLanePosition {
|
||||
lifeline: line,
|
||||
x: -123.,
|
||||
}],
|
||||
messages: vec![ScenarioMessagePosition {
|
||||
address: OccurrenceEndpoint {
|
||||
references: vec![],
|
||||
occurrence: timer,
|
||||
},
|
||||
send_y: 987.,
|
||||
}],
|
||||
})),
|
||||
},
|
||||
);
|
||||
oa_core::session::validate_candidate(&p).unwrap();
|
||||
exact_transaction(
|
||||
&p,
|
||||
prepare(&p, f.owner, &[f.reply, f.call])
|
||||
.unwrap()
|
||||
.plan(slot(f.owner, None))
|
||||
.unwrap(),
|
||||
ordered(&p, f.owner, &[x, y, f.call, f.reply]),
|
||||
);
|
||||
}
|
||||
@@ -187,7 +187,7 @@ pub fn view<'a>(app: &'a App, machine: Id, mut out: Column<'a, Message>) -> Elem
|
||||
.into();
|
||||
out = out.push(
|
||||
column![
|
||||
container(missing_controls).id("state-missing-picker"),
|
||||
missing_controls,
|
||||
selection_controls(app),
|
||||
crate::diagram::focus::wrap(
|
||||
canvas.map(event),
|
||||
|
||||
@@ -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;
|
||||
|
||||
+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:
|
||||
|
||||
+1
-13
@@ -2128,19 +2128,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:
|
||||
|
||||
+2
-12
@@ -9,8 +9,7 @@ excluded by the user's explicit request, including after networking recovers.**
|
||||
|
||||
## Assembled source and acceptance boundary
|
||||
|
||||
Combined code through **4deced53b98cd4d7649ba7aa60fa4b9b30d2f3b8**, plus the
|
||||
test-only import correction from **7c2b06b63f8a0fe14b3f001996bdf965b36a4e85**, adds the reviewed
|
||||
Combined code **4deced53b98cd4d7649ba7aa60fa4b9b30d2f3b8** adds the reviewed
|
||||
Connection/Route early-admission correction to the assembled Scenario/Text,
|
||||
ordinary/State/Process/Allocation selection, direct drawing, Traceability and
|
||||
picker changes. The [source handoff](prerelease-integrated-source-20260924.md)
|
||||
@@ -22,16 +21,7 @@ claim combined Cargo, native, packaged or release acceptance. The independent
|
||||
further material source defect in its stated scope. A subsequent peer compile
|
||||
found a missing `projection::` qualification in the new Route test helper;
|
||||
the current code includes that one-line correction. The original compile failed
|
||||
before running tests. The first combined compile at `897d9f04` subsequently
|
||||
found a missing `text_editor` import in the Allocation Text Apply regression;
|
||||
the current source includes that one-line correction too. Both failures remain
|
||||
recorded in the source handoff. The subsequent full test run at `8ddfadf4`
|
||||
failed the mounted State picker fixture: added selection controls invalidated
|
||||
its canvas-relative clicks. Source successor `a9cd364eacbbffa0693bab05b5a2b65f9d105535`
|
||||
measures an identified fixed-height picker slot and checks Escape capture,
|
||||
retaining the exact preservation/reopen/Place/Undo assertions. The source
|
||||
handoff links the preserved failed and interrupted attempts. Corrected combined
|
||||
Windows execution and package acceptance remain pending.
|
||||
before running tests. Combined execution remains pending.
|
||||
|
||||
Live build, final-source and publication results belong in
|
||||
[issue37](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/37).
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -62,9 +62,11 @@ command or source schema is unnecessary. Preview and commit must consume the
|
||||
same plan; tests independently construct expected geometry rather than merely
|
||||
comparing those two uses of the implementation.
|
||||
|
||||
Process and Scenario layouts are derived from semantic order, not free XY node
|
||||
placements. Their group drag must reorder a stable block while retaining its
|
||||
internal order and applicable owner/column/operand boundaries. Existing topology
|
||||
Process reorder and Scenario occurrence reorder change semantic sequence order.
|
||||
Saved Scenario presentations can additionally store lane/message coordinates;
|
||||
semantic group reordering preserves those authored records. A sequence group drag
|
||||
must reorder a stable block while retaining its internal order and applicable
|
||||
owner/column/operand boundaries. Existing topology
|
||||
and temporal/lifecycle validation remains authoritative. Qualified reused
|
||||
Process content must not silently edit the reusable definition. These adapters
|
||||
are required work for the issue, not waived because ordinary diagrams are easier.
|
||||
@@ -84,8 +86,11 @@ The [explicit Arrange amendment](allocation-arrange-amendment-20260923.md) defin
|
||||
the bounded one-or-more work/performer footprint and diagnostic collision domain.
|
||||
Its [pure planner evidence](allocation-arrange-planner-20260923.md) and the
|
||||
[ordinary assignment foundation](allocation-assignment-planner-20260923.md)
|
||||
establish bounded source/planner behavior. App/native group input acceptance
|
||||
remains pending.
|
||||
establish bounded source/planner behavior. The [Allocation App/native checkpoint](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/57aec979f640ba1b8aa6fcbb7d9e26b29cf51596/harness/allocation-group-app-20260924.md)
|
||||
adds bounded Windows group assignment, explicit Arrange and ordinary performer
|
||||
acceptance; its limitations and source identity remain explicit. The
|
||||
[Scenario reorder policy](scenario-group-reorder-policy-20260924.md) defines the
|
||||
strict sibling-block increment without implying adapter or removal acceptance.
|
||||
|
||||
## Related group actions
|
||||
|
||||
|
||||
@@ -80,30 +80,6 @@ actionable defect in the stated boundary. These do not substitute for the final
|
||||
combined execution. The vendor patch-description correction is incorporated at
|
||||
`17eaa03fd69741dff746f21d150dc04c341411b3`.
|
||||
|
||||
## Mounted State picker correction
|
||||
|
||||
The combined Windows test run at `8ddfadf440b5c208b1adb6b211a97ba0c99cff04`
|
||||
failed the mounted State missing-picker Escape regression. The test located the
|
||||
picker relative to the canvas, but the integrated selection toolbar adds 56px
|
||||
plus an 8px gap between them. Both the opening and option clicks were stale.
|
||||
Parent and independent source inspection agreed on this fixture defect; they
|
||||
found no reason to change the production Escape policy from this failure.
|
||||
|
||||
Successor `a9cd364eacbbffa0693bab05b5a2b65f9d105535` adds a zero-padding
|
||||
identified container around the existing fixed-height picker slot. The fixture
|
||||
measures that slot, checks Escape capture before App subscriptions, and retains
|
||||
zero messages, exact document/source/selection/camera/revision/history,
|
||||
reopen-and-Place, and exact source Undo checks. The option coordinate assumes
|
||||
this fixture's single downward-opening menu at 1800 by 1100; it is not a general
|
||||
overlay geometry evaluator. No production Escape handler changes.
|
||||
|
||||
The peer records 2,199 passes, one failure and eight ignores for that failed
|
||||
full run, with lint and product builds not run. The earlier final-02 attempt
|
||||
was interrupted during harness listing before test bodies; neither attempt is
|
||||
a passed gate. [Preserved attempts and v2 recipe](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/184c1307d1ae3d49007480020fa1c080c94ececa/harness/prerelease37-windows-gates-20260924/attempts-8dd)
|
||||
retain their distinct boundaries. The corrected source requires current
|
||||
combined Windows gates and packaged native acceptance.
|
||||
|
||||
## Combined gates and package handoff
|
||||
|
||||
The first peer compilation exposed E0425 in the new Route fixture: its
|
||||
@@ -114,16 +90,6 @@ single test line. Production code is unchanged. This concrete finding supersedes
|
||||
the earlier `9e99618c` build handoff; the failed compile ran no tests and remains
|
||||
part of the peer's evidence. Source review and formatting did not catch it.
|
||||
|
||||
The first combined Windows attempt on `897d9f04` then found E0433 in the
|
||||
Allocation Text Apply test added at `542b10ac`: the test's module lacked
|
||||
`iced::widget::text_editor`. The current successor incorporates peer
|
||||
`7c2b06b63f8a0fe14b3f001996bdf965b36a4e85`, adding that import only. Production
|
||||
code remains unchanged. The failed combined compile ran no tests, lint or
|
||||
product build; its 3,709 source inputs stayed unchanged and formatting passed.
|
||||
[Preserved failure evidence](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/2bd0e615204be728b5c2b77cc7e28504c18a2277/harness/prerelease37-windows-gates-20260924/README.md)
|
||||
records that distinct failure. The corrected successor requires fresh combined
|
||||
execution; no earlier pass is substituted.
|
||||
|
||||
Codex-ytr83r owns the exact-candidate Windows build/package on its separate
|
||||
machine. Codex-9e8r6t owns independent source/seam review and a downloaded-package
|
||||
check. Codex-956atf remains the sole integration, tag and release publication owner.
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# Scenario group reorder policy
|
||||
|
||||
Derived from [issue 29](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/29) and the [multiple-selection contract](multiselect-contract-20260923.md). Accepted design for the next Scenario adapter; this document alone establishes no implementation, native, integration or release acceptance. The original decision inspected `495464e84b5bae8ddad93e9fc69ef353e7f1c884`; the post-prerelease port additionally inspects the combined base `897d9f049406d5be597543eb0a6755b796259302` and preserves its saved Scenario presentation model.
|
||||
|
||||
## Operation and identity
|
||||
|
||||
A Scenario drag changes semantic sequence order. It moves the exact selected local occurrences as one stable block inside their common immediate Scenario or Branch parent. It does not edit free XY positions, migrate operands, change a referenced definition or automatically select dependencies.
|
||||
|
||||
Saved Scenario layouts are separate presentation records. Semantic reorder preserves all authored lane/message coordinates, metadata and other views exactly, even when the changed order requires derived presentation to accommodate those preferences. A future group presentation move is a separate capability; this increment does not silently reinterpret stored coordinates as sequence order. App admission must distinguish the active saved/derived representation and its visit lifetime, including a same-owner representation round trip.
|
||||
|
||||
Eligible bodies are Exchange (including lifecycle and timer messages), Action, State, ScenarioUse and Fragment. A Fragment moves as one occurrence; its branches and descendant lists remain unchanged. Lifelines, Branch headers, Process-only occurrence bodies and other typed targets are unsupported movement members. Cross-parent sets and a Fragment plus one of its descendants refuse as a whole. These objects may still be selected and inspected; selection is not movement permission.
|
||||
|
||||
Every original target must exist, have its actual eligible body, belong to the same immediate parent and occur exactly once in that parent's list. Check the entire edited list, including unselected entries, for duplicate/missing IDs, body and parent mismatches. The selected parent must be reachable from the active Scenario through the actual sequence lists and Fragment branch lists. An ancestor pointer alone cannot authorize a hidden Branch omitted from its Fragment. The App must also admit against exact current rendered visibility.
|
||||
|
||||
An unlisted child is a supported warning-only authoring state: leave it unlisted and unchanged. Integrity of the edited list does not require every parented element to appear in it. Missing optional reference/binding information or loop warnings must not become blanket movement errors. The existing full-project error validator remains authoritative.
|
||||
|
||||
## Stable-block transformation
|
||||
|
||||
Let `S` be the original parent list and `T` the exact selected set. Form `B` by retaining members of `T` in their order in `S`; form `R` from the other members. The destination is an exact `Slot { parent, before }`.
|
||||
|
||||
1. Complete typed/list/reachability admission and validate the unchanged baseline before any no-op result.
|
||||
2. Require the destination's same parent and an existing exact anchor, or append.
|
||||
3. If the anchor is selected, leave `S` exactly unchanged. Do not normalize to a neighboring row or compact a noncontiguous selection.
|
||||
4. Otherwise insert all of `B` before the anchor in `R`, or append. Exact unchanged order is a no-op.
|
||||
5. Validate the complete executed candidate. On success use one `SetSteps` transaction; on error change nothing.
|
||||
|
||||
For `S=[A,B,C,D]`, selecting C then A and dropping before D produces `[B,A,C,D]`, regardless of primary or pressed member. Dropping that same set before C leaves `[A,B,C,D]`. Selecting `[Call,Reply]` preserves that internal order; it never implicitly selects another reply or expands a referenced Scenario.
|
||||
|
||||
Full candidate validation includes unselected descendants. Moving a Fragment ahead of an external Call can invalidate a nested Reply; moving an occurrence with local lifeline use before Create or after a possibly coexecuting Delete must be refused. ALT exclusivity and PAR coexecution cannot be inferred from painted row order. Opaque ScenarioUse bindings remain local lifetime uses under the existing validator, without interprocedural expansion.
|
||||
|
||||
## Gesture and transaction requirements
|
||||
|
||||
Capture immutable model and exact selection identity before movement. Cache proposals by distinct Slot, not pointer pixels. Before release, verify document, surface, revision, epoch, selection generation and complete membership; refuse a stale set before reconciliation can prune it. Recompute against the same accepted snapshot and execute once through Document. Successful source updates, Undo and Redo each remain one transaction. Refusal and no-op preserve model, source, history branch, selection and primary.
|
||||
|
||||
Use the nearest displayed slot, then apply parent admission. A farther legal slot must not replace a nearer invalid operand destination. Feedback identifies the selected count, destination/parent and refusal or no-change reason. A Fragment's interior can contain invalid operand slots even when a header anchor would be a no-op. Pointer/camera/focus lifetime, real selection modifiers and native readability need separate adapter verification; a passing pure planner does not establish them.
|
||||
|
||||
## Alternatives and acceptance
|
||||
|
||||
Contiguous-only selection rejects useful sibling groups without simplifying nested temporal validation. Stable semantic ordering is therefore preferred. Parent/child normalization silently drops selected intent, while automatic call/reply expansion has no unique closure in the presence of alternative replies, loops and nested fragments. Strict refusal keeps this first increment precise. Revisit these decisions only with an explicit user workflow and a compatible whole-set specification.
|
||||
|
||||
Discriminating evaluators include literal whole-Project transformations at root and operand levels, noncontiguous order versus click order, selected-anchor/unchanged no-op with existing redo, malformed unselected entries, omitted Branch reachability, warning-only unlisted children, nested reply/lifetime failures and legal nested-fragment movement. Successful cases must preserve every other sequence, binding, relation, value, ID and metadata record. Use independent source round trips and one-step Document history, not preview/commit agreement alone.
|
||||
|
||||
Aggregate removal remains separate required work. Until it exists, an interface displaying a group must refuse legacy primary-only Remove/Delete/reorder actions. Live ownership and progress remain in issue 29.
|
||||
Reference in New Issue
Block a user