Files
ARCADIA-Studio/harness/experiments/behavior-anchor-plan-20260912/mod.rs
T

449 lines
15 KiB
Rust

//! Experimental complete anchor input for the actual Scene, cfg(test) only.
//! No persistence schema and no replacement line/lifecycle/activation renderer.
use super::*;
use std::cell::RefCell;
mod tests;
type Candidate = BTreeMap<Id, f32>;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum AnchorRole {
MessageSend,
MessageReceive,
Observation,
FrameStart,
OperandStart,
}
#[derive(Clone, Copy, Debug, PartialEq)]
struct Anchor {
y: f32,
role: AnchorRole,
}
#[derive(Clone, Copy, Debug, PartialEq)]
struct MessageAnchors {
send_y: f32,
receive_y: f32,
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum Diagnostic {
SourceOrderCrossing { scope: Id, before: Id, after: Id },
FrameExtendsPastFollowingAnchor { frame: Id, following: Id },
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum PlanError {
UnknownAnchor(Id),
NonFinite(Id),
CompoundMoveNotImplemented(Id),
FixedFrameHeaderWouldMove { frame: Id },
OperandScopeOutsideEnclosingInterval { operand: Id, fragment: Id },
LifecycleAnchorOutsideRenderScope { occurrence: Id, scope: Id },
LifetimeInversionNotSupported { create: Id, delete: Id },
}
#[derive(Clone, Debug)]
struct Plan {
anchors: BTreeMap<Id, Anchor>,
messages: BTreeMap<Id, MessageAnchors>,
frames: BTreeMap<Id, Rectangle>,
scopes: BTreeMap<Id, (f32, f32)>,
height: f32,
diagnostics: Vec<Diagnostic>,
}
fn occurrence(p: &Project, id: Id) -> Option<&OccurrenceBody> {
match p.elements.get(&id)?.shared()? {
SharedBody::Occurrence { body, .. } => Some(body),
_ => None,
}
}
fn role(body: &OccurrenceBody) -> AnchorRole {
match body {
OccurrenceBody::Exchange {
message_kind: MessageKind::Create | MessageKind::Delete,
..
} => AnchorRole::MessageReceive,
OccurrenceBody::Exchange { .. } => AnchorRole::MessageSend,
OccurrenceBody::Fragment { .. } => AnchorRole::FrameStart,
OccurrenceBody::Branch { .. } => AnchorRole::OperandStart,
_ => AnchorRole::Observation,
}
}
fn inside(p: &Project, id: Id, owner: Id) -> bool {
let mut current = p.elements.get(&id).and_then(|e| e.parent);
for _ in 0..p.elements.len() {
match current {
Some(parent) if parent == owner => return true,
Some(parent) => current = p.elements.get(&parent).and_then(|e| e.parent),
None => return false,
}
}
false
}
fn depth(p: &Project, id: Id) -> usize {
let mut current = p.elements.get(&id).and_then(|e| e.parent);
let mut result = 0;
while let Some(parent) = current {
result += 1;
if result > p.elements.len() {
break;
}
current = p.elements.get(&parent).and_then(|e| e.parent);
}
result
}
fn union(a: Rectangle, b: Rectangle) -> Rectangle {
let x = a.x.min(b.x);
let y = a.y.min(b.y);
Rectangle::new(
Point::new(x, y),
Size::new(
(a.x + a.width).max(b.x + b.width) - x,
(a.y + a.height).max(b.y + b.height) - y,
),
)
}
/// Actual named shapes/message paths supply footprint measurements; activation
/// bars are intentionally excluded because current Scene derives them later.
fn footprints(
scene: &Scene,
anchors: &BTreeMap<Id, Anchor>,
seed: &BTreeMap<Id, Anchor>,
) -> BTreeMap<Id, Rectangle> {
let mut out = BTreeMap::new();
for item in scene.boxes.iter().filter(|item| !item.title.is_empty()) {
let Some(anchor) = anchors.get(&item.id) else {
continue;
};
let mut rect = item.rect;
rect.y += anchor.y - seed[&item.id].y;
out.insert(item.id, rect);
}
for line in &scene.lines {
let Some(anchor) = anchors.get(&line.id) else {
continue;
};
let delta = anchor.y - seed[&line.id].y;
let mut rect = line.label_bounds();
rect.y += delta;
for point in &line.points {
rect = union(
rect,
Rectangle::new(Point::new(point.x, point.y + delta), Size::ZERO),
);
}
out.entry(line.id)
.and_modify(|old| *old = union(*old, rect))
.or_insert(rect);
}
out
}
fn covered_bounds(
p: &Project,
owner: Id,
footprints: &BTreeMap<Id, Rectangle>,
) -> Option<Rectangle> {
footprints
.iter()
.filter(|(id, _)| inside(p, **id, owner))
.map(|(_, rect)| *rect)
.reduce(union)
}
impl Plan {
fn build(
p: &Project,
owner: Id,
baseline: &Scene,
desired: &Candidate,
) -> Result<Self, PlanError> {
let mut seed: BTreeMap<_, _> = baseline
.rows
.iter()
.map(|(id, (_, y))| {
(
*id,
Anchor {
y: *y,
role: role(occurrence(p, *id).expect("Scene occurrence")),
},
)
})
.collect();
// Operand headers are real Scene boxes, but are not entries in rows.
for item in &baseline.boxes {
if matches!(occurrence(p, item.id), Some(OccurrenceBody::Branch { .. })) {
seed.insert(
item.id,
Anchor {
y: item.rect.y,
role: AnchorRole::OperandStart,
},
);
}
}
let mut anchors = seed.clone();
for (&id, &y) in desired {
let anchor = anchors.get_mut(&id).ok_or(PlanError::UnknownAnchor(id))?;
if !y.is_finite() {
return Err(PlanError::NonFinite(id));
}
if matches!(
anchor.role,
AnchorRole::FrameStart | AnchorRole::OperandStart
) && y != anchor.y
{
return Err(PlanError::CompoundMoveNotImplemented(id));
}
anchor.y = y;
}
let mut messages = BTreeMap::new();
for line in &baseline.lines {
if let (Some(anchor), Some(first), Some(last)) = (
anchors.get(&line.id),
line.points.first(),
line.points.last(),
) {
let delta = anchor.y - seed[&line.id].y;
messages.insert(
line.id,
MessageAnchors {
send_y: first.y + delta,
receive_y: last.y + delta,
},
);
}
}
// The current lifecycle bridge needs positive birth/death intervals.
// This is an explicit unsupported crossing, not a new semantic rule.
for (&create, message) in &messages {
if let Some(OccurrenceBody::Exchange {
message_kind: MessageKind::Create,
receiver: Some(lane),
..
}) = occurrence(p, create)
{
for (&delete, finish) in &messages {
if p.elements[&create].parent == p.elements[&delete].parent
&& matches!(occurrence(p, delete), Some(OccurrenceBody::Exchange { message_kind: MessageKind::Delete, receiver: Some(other), .. }) if other == lane)
&& message.receive_y > finish.receive_y
{
return Err(PlanError::LifetimeInversionNotSupported { create, delete });
}
}
}
}
let old_footprints = footprints(baseline, &seed, &seed);
let mut drawn = footprints(baseline, &anchors, &seed);
let mut frames = BTreeMap::new();
let mut containers: Vec<_> = seed
.keys()
.copied()
.filter(|id| matches!(occurrence(p, *id), Some(OccurrenceBody::Fragment { .. })))
.collect();
containers.sort_by_key(|id| std::cmp::Reverse(depth(p, *id)));
for id in containers {
let original = old_footprints[&id];
let mut rect = original;
if let Some(covered) = covered_bounds(p, id, &drawn) {
// Keep seed extents where possible; 20px growth padding is an
// explicit experimental presentation preference, not semantics.
// Upward envelope growth would move the painted frame header
// while its fixed FrameStart/row/slot remained at the seed.
// Reject that unsupported compound change, not mere overlap.
if covered.y < rect.y {
return Err(PlanError::FixedFrameHeaderWouldMove { frame: id });
}
if covered.y + covered.height > rect.y + rect.height {
rect.height = covered.y + covered.height + 20. - rect.y;
}
}
frames.insert(id, rect);
drawn.insert(id, rect);
}
let mut scopes = baseline.sequence_bounds.clone();
for (&scope, bounds) in &mut scopes {
if let (Some(old), Some(new)) = (
covered_bounds(p, scope, &old_footprints),
covered_bounds(p, scope, &drawn),
) {
let top_padding = (old.y - bounds.0).max(0.);
let bottom_padding = (bounds.1 - old.y - old.height).max(0.);
if new.y < bounds.0 {
bounds.0 = new.y - top_padding;
}
bounds.1 = bounds.1.max(new.y + new.height + bottom_padding);
}
}
let tail = baseline.size.height - baseline.sequence_bounds[&owner].1;
let height = baseline.size.height.max(scopes[&owner].1 + tail);
for (&operand, &(start, end)) in &scopes {
if !matches!(occurrence(p, operand), Some(OccurrenceBody::Branch { .. })) {
continue;
}
let fragment = p.elements[&operand].parent.expect("operand fragment");
let caller = p.elements[&fragment]
.parent
.expect("fragment caller sequence");
let enclosing_frame = frames[&fragment];
let (caller_start, caller_end) = if caller == owner {
(85., height - 20.)
} else {
let (a, b) = scopes[&caller];
(a, if a == b { a + 26. } else { b })
};
let enclosing_start = anchors[&fragment].y.max(caller_start);
let enclosing_end = (enclosing_frame.y + enclosing_frame.height).min(caller_end);
let effective_end = if start == end { start + 26. } else { end };
// Actual lifecycle traversal cannot enter an operand that escapes
// this enclosing interval. This guards bridge feasibility only;
// it is not a generic event-spacing or sibling-ordering policy.
if start < enclosing_start || effective_end > enclosing_end {
return Err(PlanError::OperandScopeOutsideEnclosingInterval { operand, fragment });
}
}
for (&id, message) in &messages {
if matches!(
occurrence(p, id),
Some(OccurrenceBody::Exchange {
message_kind: MessageKind::Create | MessageKind::Delete,
..
})
) {
let scope = p.elements[&id].parent.expect("lifecycle sequence owner");
// Match Rendering::new's effective scope, not the insertion
// scope: it overrides the root span to 85..height-20 and gives
// empty non-root scopes the existing 26px lifetime allowance.
let (start, end) = if scope == owner {
(85., height - 20.)
} else if let Some(&(start, end)) = scopes.get(&scope) {
(start, if start == end { start + 26. } else { end })
} else {
return Err(PlanError::LifecycleAnchorOutsideRenderScope {
occurrence: id,
scope,
});
};
if message.receive_y < start || message.receive_y > end {
return Err(PlanError::LifecycleAnchorOutsideRenderScope {
occurrence: id,
scope,
});
}
}
}
let mut diagnostics = Vec::new();
for element in p
.elements
.values()
.filter(|e| e.id == owner || inside(p, e.id, owner))
{
if let Some(steps) = element.steps() {
for pair in steps.windows(2) {
if let (Some(a), Some(b)) = (anchors.get(&pair[0]), anchors.get(&pair[1])) {
if a.y > b.y {
diagnostics.push(Diagnostic::SourceOrderCrossing {
scope: element.id,
before: pair[0],
after: pair[1],
});
}
if let Some(frame) = frames.get(&pair[0]) {
if frame.y + frame.height > b.y {
diagnostics.push(Diagnostic::FrameExtendsPastFollowingAnchor {
frame: pair[0],
following: pair[1],
});
}
}
}
}
}
}
Ok(Self {
anchors,
messages,
frames,
scopes,
height,
diagnostics,
})
}
}
thread_local! {
static PLAN: RefCell<Option<Plan>> = const { RefCell::new(None) };
}
struct Scope(Option<Plan>);
impl Scope {
fn install(plan: &Plan) -> Self {
Self(PLAN.with(|slot| slot.replace(Some(plan.clone()))))
}
}
impl Drop for Scope {
fn drop(&mut self) {
PLAN.with(|slot| {
slot.replace(self.0.take());
});
}
}
pub(super) fn row_anchor(id: Id, y: &mut f32) {
PLAN.with(|slot| {
if let Some(plan) = slot.borrow().as_ref() {
*y = plan.anchors.get(&id).expect("complete Scene anchor plan").y;
}
});
}
pub(super) fn frame_bounds(id: Id, fallback: Rectangle) -> Rectangle {
PLAN.with(|slot| {
slot.borrow()
.as_ref()
.map_or(fallback, |plan| plan.frames[&id])
})
}
pub(super) fn scope_end(owner: Id, y: &mut f32, bounds: &mut BTreeMap<Id, (f32, f32)>) {
PLAN.with(|slot| {
if let Some(plan) = slot.borrow().as_ref() {
bounds.insert(owner, plan.scopes[&owner]);
*y = plan.scopes[&owner].1 + 42.; // Existing insertion-axis offset.
}
});
}
pub(super) fn scene_extent(scene: &mut Scene) {
PLAN.with(|slot| {
if let Some(plan) = slot.borrow().as_ref() {
scene.size.height = plan.height;
}
});
}
fn render(p: &Project, owner: Id, plan: &Plan) -> Scene {
let _scope = Scope::install(plan);
Scene::sequence(p, owner, owner)
}
fn drag(original: &Candidate, frame: &Scene, target: Id, delta_y: f32) -> Candidate {
assert!(delta_y.is_finite());
if delta_y == 0. {
return original.clone();
}
let mut result = original.clone();
result.insert(target, frame.rows[&target].1 + delta_y);
result
}