//! 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; #[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, messages: BTreeMap, frames: BTreeMap, scopes: BTreeMap, height: f32, diagnostics: Vec, } 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, seed: &BTreeMap, ) -> BTreeMap { 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, ) -> Option { 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 { 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> = const { RefCell::new(None) }; } struct Scope(Option); 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) { 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 }