449 lines
15 KiB
Rust
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
|
|
}
|