Resolve saved Scenario geometry through explicit complete Scene plans
This commit is contained in:
@@ -21,6 +21,9 @@ pub(crate) mod creation;
|
|||||||
mod form_session;
|
mod form_session;
|
||||||
pub(crate) mod scenario;
|
pub(crate) mod scenario;
|
||||||
mod scenario_activation;
|
mod scenario_activation;
|
||||||
|
// Pure saved-geometry bridge; workspace integration is a separate checkpoint.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
mod scenario_layout;
|
||||||
mod scenario_lifecycle;
|
mod scenario_lifecycle;
|
||||||
pub(crate) mod source_draft;
|
pub(crate) mod source_draft;
|
||||||
pub(crate) mod state_canvas;
|
pub(crate) mod state_canvas;
|
||||||
@@ -1172,12 +1175,15 @@ struct Scene {
|
|||||||
boxes: Vec<BoxItem>,
|
boxes: Vec<BoxItem>,
|
||||||
lines: Vec<Line>,
|
lines: Vec<Line>,
|
||||||
size: Size,
|
size: Size,
|
||||||
|
layout_origin: Point,
|
||||||
selected: Id,
|
selected: Id,
|
||||||
scale: f32,
|
scale: f32,
|
||||||
camera_pan: iced::Vector,
|
camera_pan: iced::Vector,
|
||||||
interaction: Option<(scenario::Context, scenario::CanvasTool, bool)>,
|
interaction: Option<(scenario::Context, scenario::CanvasTool, bool)>,
|
||||||
pending_placement: Option<(scenario::Slot, Option<Id>)>,
|
pending_placement: Option<(scenario::Slot, Option<Id>)>,
|
||||||
lanes: BTreeMap<Id, f32>,
|
lanes: BTreeMap<Id, f32>,
|
||||||
|
// Exact render coordinates include unassigned lanes; do not derive from cards.
|
||||||
|
render_lanes: BTreeMap<Id, f32>,
|
||||||
slots: Vec<SequenceSlot>,
|
slots: Vec<SequenceSlot>,
|
||||||
rows: BTreeMap<Id, (Id, f32)>,
|
rows: BTreeMap<Id, (Id, f32)>,
|
||||||
activations: Vec<scenario_activation::Segment>,
|
activations: Vec<scenario_activation::Segment>,
|
||||||
@@ -1228,16 +1234,27 @@ impl SequenceInput {
|
|||||||
}
|
}
|
||||||
impl Scene {
|
impl Scene {
|
||||||
fn sequence(p: &Project, owner: Id, selected: Id) -> Self {
|
fn sequence(p: &Project, owner: Id, selected: Id) -> Self {
|
||||||
|
Self::sequence_geometry(p, owner, selected, None, None)
|
||||||
|
}
|
||||||
|
fn sequence_geometry(
|
||||||
|
p: &Project,
|
||||||
|
owner: Id,
|
||||||
|
selected: Id,
|
||||||
|
lane_override: Option<&BTreeMap<Id, f32>>,
|
||||||
|
plan: Option<&scenario_layout::Plan>,
|
||||||
|
) -> Self {
|
||||||
let mut scene = Self {
|
let mut scene = Self {
|
||||||
scale: 1.,
|
scale: 1.,
|
||||||
camera_pan: iced::Vector::ZERO,
|
camera_pan: iced::Vector::ZERO,
|
||||||
boxes: vec![],
|
boxes: vec![],
|
||||||
lines: vec![],
|
lines: vec![],
|
||||||
size: Size::new(700., 200.),
|
size: Size::new(700., 200.),
|
||||||
|
layout_origin: Point::ORIGIN,
|
||||||
selected,
|
selected,
|
||||||
interaction: None,
|
interaction: None,
|
||||||
pending_placement: None,
|
pending_placement: None,
|
||||||
lanes: BTreeMap::new(),
|
lanes: BTreeMap::new(),
|
||||||
|
render_lanes: BTreeMap::new(),
|
||||||
slots: vec![],
|
slots: vec![],
|
||||||
rows: BTreeMap::new(),
|
rows: BTreeMap::new(),
|
||||||
activations: vec![],
|
activations: vec![],
|
||||||
@@ -1308,12 +1325,32 @@ impl Scene {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
let lane_x = lanes
|
let mut lane_x = lanes
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, (id, ..))| (*id, 160. + i as f32 * 240.))
|
.map(|(i, (id, ..))| (*id, 160. + i as f32 * 240.))
|
||||||
.collect::<BTreeMap<_, _>>();
|
.collect::<BTreeMap<_, _>>();
|
||||||
|
if let Some(values) = lane_override {
|
||||||
|
for (id, x) in values {
|
||||||
|
if let Some(lane) = lane_x.get_mut(id) {
|
||||||
|
*lane = *x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scene.render_lanes = lane_x.clone();
|
||||||
scene.size.width = (lanes.len() as f32 * 240. + 80.).max(680.);
|
scene.size.width = (lanes.len() as f32 * 240. + 80.).max(680.);
|
||||||
|
let layout_left = if lane_override.is_some() {
|
||||||
|
lane_x.values().fold(0_f32, |left, x| left.min(*x - 160.))
|
||||||
|
} else {
|
||||||
|
0.
|
||||||
|
};
|
||||||
|
scene.layout_origin.x = layout_left;
|
||||||
|
if lane_override.is_some() {
|
||||||
|
scene.size.width = lane_x
|
||||||
|
.values()
|
||||||
|
.fold(scene.size.width, |right, x| right.max(*x + 240.))
|
||||||
|
- layout_left;
|
||||||
|
}
|
||||||
if scenario {
|
if scenario {
|
||||||
scene.lanes = lane_x
|
scene.lanes = lane_x
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1329,8 +1366,26 @@ impl Scene {
|
|||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
let mut y = if scenario { 148. } else { 120. };
|
let mut y = if scenario { 148. } else { 120. };
|
||||||
scene.occurrences(p, owner, scenario, &lane_x, &mut y, 0);
|
scene.occurrences(p, owner, scenario, &lane_x, &mut y, 0, plan);
|
||||||
scene.size.height = (y + 45.).max(210.);
|
if layout_left != 0. {
|
||||||
|
for item in &mut scene.boxes {
|
||||||
|
let body = scenario_layout::occurrence(p, item.id);
|
||||||
|
if matches!(
|
||||||
|
body,
|
||||||
|
Some(
|
||||||
|
OccurrenceBody::Branch { .. }
|
||||||
|
| OccurrenceBody::ScenarioUse { .. }
|
||||||
|
| OccurrenceBody::Control { .. }
|
||||||
|
| OccurrenceBody::SequenceReference { .. }
|
||||||
|
| OccurrenceBody::Exchange { .. }
|
||||||
|
)
|
||||||
|
) || (plan.is_none() && matches!(body, Some(OccurrenceBody::Fragment { .. })))
|
||||||
|
{
|
||||||
|
item.rect.x += layout_left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scene.size.height = plan.map_or((y + 45.).max(210.), |plan| plan.height);
|
||||||
if scenario {
|
if scenario {
|
||||||
scene.lifecycle = scenario_lifecycle::render::Rendering::new(
|
scene.lifecycle = scenario_lifecycle::render::Rendering::new(
|
||||||
p,
|
p,
|
||||||
@@ -1341,6 +1396,9 @@ impl Scene {
|
|||||||
scene.size.height,
|
scene.size.height,
|
||||||
);
|
);
|
||||||
scene.lifecycle.align_messages(&mut scene.lines);
|
scene.lifecycle.align_messages(&mut scene.lines);
|
||||||
|
if let Some(plan) = plan {
|
||||||
|
plan.align_message_anchors(&mut scene.lines);
|
||||||
|
}
|
||||||
scene.layout_activations(p, &lane_x);
|
scene.layout_activations(p, &lane_x);
|
||||||
}
|
}
|
||||||
for (id, name, detail) in lanes {
|
for (id, name, detail) in lanes {
|
||||||
@@ -1371,6 +1429,7 @@ impl Scene {
|
|||||||
}
|
}
|
||||||
scene
|
scene
|
||||||
}
|
}
|
||||||
|
#[allow(clippy::too_many_arguments)] // Explicit complete plan replaces ambient geometry state.
|
||||||
fn occurrences(
|
fn occurrences(
|
||||||
&mut self,
|
&mut self,
|
||||||
p: &Project,
|
p: &Project,
|
||||||
@@ -1379,6 +1438,7 @@ impl Scene {
|
|||||||
lanes: &BTreeMap<Id, f32>,
|
lanes: &BTreeMap<Id, f32>,
|
||||||
y: &mut f32,
|
y: &mut f32,
|
||||||
depth: usize,
|
depth: usize,
|
||||||
|
plan: Option<&scenario_layout::Plan>,
|
||||||
) {
|
) {
|
||||||
let steps = p
|
let steps = p
|
||||||
.elements
|
.elements
|
||||||
@@ -1418,6 +1478,9 @@ impl Scene {
|
|||||||
}
|
}
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
scenario_lifecycle::render::apply_test_row_offset(*id, y);
|
scenario_lifecycle::render::apply_test_row_offset(*id, y);
|
||||||
|
if let Some(plan) = plan {
|
||||||
|
*y = plan.anchors[id].y;
|
||||||
|
}
|
||||||
if ordered {
|
if ordered {
|
||||||
let slot = scenario::Slot {
|
let slot = scenario::Slot {
|
||||||
parent: owner,
|
parent: owner,
|
||||||
@@ -1434,7 +1497,7 @@ impl Scene {
|
|||||||
OccurrenceBody::Fragment { operator, .. } => {
|
OccurrenceBody::Fragment { operator, .. } => {
|
||||||
let top = *y;
|
let top = *y;
|
||||||
*y += 55.;
|
*y += 55.;
|
||||||
self.occurrences(p, *id, scenario, lanes, y, depth + 1);
|
self.occurrences(p, *id, scenario, lanes, y, depth + 1, plan);
|
||||||
let rect = Rectangle::new(
|
let rect = Rectangle::new(
|
||||||
Point::new(18. + depth as f32 * 10., top),
|
Point::new(18. + depth as f32 * 10., top),
|
||||||
Size::new(
|
Size::new(
|
||||||
@@ -1442,6 +1505,7 @@ impl Scene {
|
|||||||
(*y - top).max(95.),
|
(*y - top).max(95.),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
let rect = plan.map_or(rect, |plan| plan.frames[id]);
|
||||||
self.boxes.insert(
|
self.boxes.insert(
|
||||||
0,
|
0,
|
||||||
BoxItem {
|
BoxItem {
|
||||||
@@ -1473,7 +1537,7 @@ impl Scene {
|
|||||||
marker: Some(Kind::Occurrence),
|
marker: Some(Kind::Occurrence),
|
||||||
});
|
});
|
||||||
*y += if scenario { 90. } else { 62. };
|
*y += if scenario { 90. } else { 62. };
|
||||||
self.occurrences(p, *id, scenario, lanes, y, depth + 1);
|
self.occurrences(p, *id, scenario, lanes, y, depth + 1, plan);
|
||||||
}
|
}
|
||||||
OccurrenceBody::Exchange {
|
OccurrenceBody::Exchange {
|
||||||
exchange,
|
exchange,
|
||||||
@@ -1501,7 +1565,7 @@ impl Scene {
|
|||||||
source.and_then(|id| lanes.get(&id)),
|
source.and_then(|id| lanes.get(&id)),
|
||||||
target.and_then(|id| lanes.get(&id)),
|
target.and_then(|id| lanes.get(&id)),
|
||||||
) {
|
) {
|
||||||
let points = if a == b {
|
let points = if source == target {
|
||||||
vec![
|
vec![
|
||||||
Point::new(*a, *y),
|
Point::new(*a, *y),
|
||||||
Point::new(a + 75., *y),
|
Point::new(a + 75., *y),
|
||||||
@@ -1626,6 +1690,10 @@ impl Scene {
|
|||||||
}
|
}
|
||||||
if ordered {
|
if ordered {
|
||||||
self.sequence_bounds.insert(owner, (sequence_top, *y - 42.));
|
self.sequence_bounds.insert(owner, (sequence_top, *y - 42.));
|
||||||
|
if let Some(plan) = plan {
|
||||||
|
self.sequence_bounds.insert(owner, plan.scopes[&owner]);
|
||||||
|
*y = plan.scopes[&owner].1 + 42.;
|
||||||
|
}
|
||||||
let slot = scenario::Slot {
|
let slot = scenario::Slot {
|
||||||
parent: owner,
|
parent: owner,
|
||||||
before: None,
|
before: None,
|
||||||
|
|||||||
@@ -0,0 +1,817 @@
|
|||||||
|
//! Pure complete Scenario geometry. No persistence, gesture or fallback policy.
|
||||||
|
//! The only renderer is Scene::sequence_geometry; this module resolves its inputs.
|
||||||
|
use super::*;
|
||||||
|
use oa_core::{DiagramLayout, ScenarioLayout};
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
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)]
|
||||||
|
pub(super) struct Anchor {
|
||||||
|
pub(super) 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)]
|
||||||
|
pub(super) enum GeometryIssue {
|
||||||
|
UnknownAnchor(Id),
|
||||||
|
LifecycleGeometryNotSupported { lifeline: Id, scope: Id },
|
||||||
|
InvalidRepresentation(Id),
|
||||||
|
InvalidCoordinate(Id),
|
||||||
|
DuplicateCoordinate(Id),
|
||||||
|
ExpandedAddressNotSupported(Id),
|
||||||
|
CoincidentDistinctEndpoints(Id),
|
||||||
|
TypedActivationOutsideScope { call: Id, scope: Id },
|
||||||
|
TypedActivationOutsideLifetime { call: Id, receiver: Id },
|
||||||
|
TypedActivationCrossingNotSupported { call: Id, event: 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)]
|
||||||
|
pub(super) struct Plan {
|
||||||
|
pub(super) anchors: BTreeMap<Id, Anchor>,
|
||||||
|
messages: BTreeMap<Id, MessageAnchors>,
|
||||||
|
pub(super) frames: BTreeMap<Id, Rectangle>,
|
||||||
|
pub(super) scopes: BTreeMap<Id, (f32, f32)>,
|
||||||
|
pub(super) height: f32,
|
||||||
|
diagnostics: Vec<Diagnostic>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) 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
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::behavior) 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 include_ancestors(p: &Project, id: Id, rect: Rectangle, covered: &mut BTreeMap<Id, Rectangle>) {
|
||||||
|
let mut current = p.elements.get(&id).and_then(|e| e.parent);
|
||||||
|
for _ in 0..p.elements.len() {
|
||||||
|
let Some(parent) = current else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
covered
|
||||||
|
.entry(parent)
|
||||||
|
.and_modify(|old| *old = union(*old, rect))
|
||||||
|
.or_insert(rect);
|
||||||
|
current = p.elements.get(&parent).and_then(|e| e.parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn coverage(p: &Project, footprints: &BTreeMap<Id, Rectangle>) -> BTreeMap<Id, Rectangle> {
|
||||||
|
let mut covered = BTreeMap::new();
|
||||||
|
for (&id, &rect) in footprints {
|
||||||
|
include_ancestors(p, id, rect, &mut covered);
|
||||||
|
}
|
||||||
|
covered
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Plan {
|
||||||
|
/// Authored endpoint values are primary inputs, not deltas reconstructed from
|
||||||
|
/// rounded rows. Lifecycle alignment owns x and marker attachment; this final
|
||||||
|
/// y alignment precedes typed activation derivation and preserves send bits.
|
||||||
|
pub(super) fn align_message_anchors(&self, lines: &mut [Line]) {
|
||||||
|
for line in lines {
|
||||||
|
let Some(message) = self.messages.get(&line.id) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let previous_send = line.points.first().map_or(message.send_y, |point| point.y);
|
||||||
|
line.label_at.y += message.send_y - previous_send;
|
||||||
|
let last = line.points.len().saturating_sub(1);
|
||||||
|
for (index, point) in line.points.iter_mut().enumerate() {
|
||||||
|
point.y = if index == last || (last == 3 && index == 2) {
|
||||||
|
message.receive_y
|
||||||
|
} else {
|
||||||
|
message.send_y
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(
|
||||||
|
p: &Project,
|
||||||
|
owner: Id,
|
||||||
|
baseline: &Scene,
|
||||||
|
desired: &Candidate,
|
||||||
|
) -> Result<Self, GeometryIssue> {
|
||||||
|
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(GeometryIssue::UnknownAnchor(id))?;
|
||||||
|
if !y.is_finite() {
|
||||||
|
return Err(GeometryIssue::NonFinite(id));
|
||||||
|
}
|
||||||
|
if matches!(
|
||||||
|
anchor.role,
|
||||||
|
AnchorRole::FrameStart | AnchorRole::OperandStart
|
||||||
|
) && y != anchor.y
|
||||||
|
{
|
||||||
|
return Err(GeometryIssue::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 message = if desired.contains_key(&line.id) {
|
||||||
|
let self_message = matches!(occurrence(p, line.id), Some(OccurrenceBody::Exchange {
|
||||||
|
sender: Some(sender), receiver, message_kind, ..
|
||||||
|
}) if matches!(message_kind, MessageKind::Timer { .. }) || *receiver == Some(*sender));
|
||||||
|
let lifecycle = matches!(
|
||||||
|
occurrence(p, line.id),
|
||||||
|
Some(OccurrenceBody::Exchange {
|
||||||
|
message_kind: MessageKind::Create | MessageKind::Delete,
|
||||||
|
..
|
||||||
|
})
|
||||||
|
);
|
||||||
|
if self_message && lifecycle {
|
||||||
|
MessageAnchors {
|
||||||
|
send_y: anchor.y - 24.,
|
||||||
|
receive_y: anchor.y,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
MessageAnchors {
|
||||||
|
send_y: anchor.y,
|
||||||
|
receive_y: if self_message {
|
||||||
|
anchor.y + 24.
|
||||||
|
} else {
|
||||||
|
anchor.y
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
MessageAnchors {
|
||||||
|
send_y: first.y,
|
||||||
|
receive_y: last.y,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
messages.insert(line.id, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Track actual semantic lifecycle order per receiver and scope. In particular,
|
||||||
|
// a later recreation must not be compared with an earlier deletion.
|
||||||
|
for element in p
|
||||||
|
.elements
|
||||||
|
.values()
|
||||||
|
.filter(|e| e.id == owner || inside(p, e.id, owner))
|
||||||
|
{
|
||||||
|
let Some(steps) = element.steps() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let mut last = BTreeMap::new();
|
||||||
|
for id in steps {
|
||||||
|
if let (
|
||||||
|
Some(message),
|
||||||
|
Some(OccurrenceBody::Exchange {
|
||||||
|
receiver: Some(lane),
|
||||||
|
message_kind: MessageKind::Create | MessageKind::Delete,
|
||||||
|
..
|
||||||
|
}),
|
||||||
|
) = (messages.get(id), occurrence(p, *id))
|
||||||
|
{
|
||||||
|
if let Some((previous, previous_y)) =
|
||||||
|
last.insert(*lane, (*id, message.receive_y))
|
||||||
|
{
|
||||||
|
if previous_y > message.receive_y {
|
||||||
|
return Err(GeometryIssue::LifetimeInversionNotSupported {
|
||||||
|
create: previous,
|
||||||
|
delete: *id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let old_footprints = footprints(baseline, &seed, &seed);
|
||||||
|
let mut drawn = footprints(baseline, &anchors, &seed);
|
||||||
|
let old_coverage = coverage(p, &old_footprints);
|
||||||
|
let mut new_coverage = coverage(p, &drawn);
|
||||||
|
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) = new_coverage.get(&id).copied() {
|
||||||
|
// Preserve automatic extents and their existing descendant padding.
|
||||||
|
// 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(GeometryIssue::FixedFrameHeaderWouldMove { frame: id });
|
||||||
|
}
|
||||||
|
if covered.y + covered.height > rect.y + rect.height {
|
||||||
|
let old_covered = old_coverage[&id];
|
||||||
|
let padding =
|
||||||
|
(original.y + original.height - old_covered.y - old_covered.height).max(0.);
|
||||||
|
rect.height = covered.y + covered.height + padding - rect.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
frames.insert(id, rect);
|
||||||
|
drawn.insert(id, rect);
|
||||||
|
include_ancestors(p, id, rect, &mut new_coverage);
|
||||||
|
}
|
||||||
|
let mut scopes = baseline.sequence_bounds.clone();
|
||||||
|
for (&scope, bounds) in &mut scopes {
|
||||||
|
if let (Some(old), Some(new)) = (
|
||||||
|
old_coverage.get(&scope).copied(),
|
||||||
|
new_coverage.get(&scope).copied(),
|
||||||
|
) {
|
||||||
|
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(GeometryIssue::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(GeometryIssue::LifecycleAnchorOutsideRenderScope {
|
||||||
|
occurrence: id,
|
||||||
|
scope,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
if message.receive_y < start || message.receive_y > end {
|
||||||
|
return Err(GeometryIssue::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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An appearance identity, never inferred from selection or from the owner alone.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub(super) enum Representation {
|
||||||
|
Derived { owner: Id },
|
||||||
|
Saved { diagram: Id },
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Complete immutable geometry produced from one Project borrow. Scene is retained
|
||||||
|
/// with its plan so a caller cannot accidentally render this plan against another
|
||||||
|
/// Project or representation. Document/revision freshness belongs to that caller.
|
||||||
|
pub(super) struct ResolvedScenarioPlan {
|
||||||
|
pub(super) representation: Representation,
|
||||||
|
pub(super) owner: Id,
|
||||||
|
pub(super) lanes: BTreeMap<Id, f32>,
|
||||||
|
pub(super) plan: Plan,
|
||||||
|
pub(super) latent_messages: Vec<Id>,
|
||||||
|
pub(super) bounds: Rectangle,
|
||||||
|
pub(super) scene: Scene,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn resolve(
|
||||||
|
p: &Project,
|
||||||
|
representation: Representation,
|
||||||
|
selected: Id,
|
||||||
|
) -> Result<ResolvedScenarioPlan, Vec<GeometryIssue>> {
|
||||||
|
let (owner, layout) = match representation {
|
||||||
|
Representation::Derived { owner } => (owner, None),
|
||||||
|
Representation::Saved { diagram } => {
|
||||||
|
let view = p
|
||||||
|
.diagrams
|
||||||
|
.get(&diagram)
|
||||||
|
.ok_or_else(|| vec![GeometryIssue::InvalidRepresentation(diagram)])?;
|
||||||
|
let owner = view
|
||||||
|
.projection
|
||||||
|
.scenario()
|
||||||
|
.ok_or_else(|| vec![GeometryIssue::InvalidRepresentation(diagram)])?;
|
||||||
|
if p.elements
|
||||||
|
.get(&owner)
|
||||||
|
.is_none_or(|e| e.layer() != view.layer)
|
||||||
|
{
|
||||||
|
return Err(vec![GeometryIssue::InvalidRepresentation(diagram)]);
|
||||||
|
}
|
||||||
|
let layout = view
|
||||||
|
.layout
|
||||||
|
.as_ref()
|
||||||
|
.map(|DiagramLayout::Scenario(layout)| layout);
|
||||||
|
(owner, layout)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if p.elements
|
||||||
|
.get(&owner)
|
||||||
|
.is_none_or(|e| e.kind() != Kind::Scenario)
|
||||||
|
{
|
||||||
|
return Err(vec![GeometryIssue::InvalidRepresentation(owner)]);
|
||||||
|
}
|
||||||
|
let empty = ScenarioLayout::default();
|
||||||
|
let layout = layout.unwrap_or(&empty);
|
||||||
|
let mut lanes = BTreeMap::new();
|
||||||
|
let mut issues = Vec::new();
|
||||||
|
for lane in &layout.lanes {
|
||||||
|
if p.elements
|
||||||
|
.get(&lane.lifeline)
|
||||||
|
.is_none_or(|e| e.kind() != Kind::Lifeline || e.parent != Some(owner))
|
||||||
|
{
|
||||||
|
issues.push(GeometryIssue::UnknownAnchor(lane.lifeline));
|
||||||
|
}
|
||||||
|
if !lane.x.is_finite() || lane.x.abs() > oa_core::MAX_COORDINATE {
|
||||||
|
issues.push(GeometryIssue::InvalidCoordinate(lane.lifeline));
|
||||||
|
}
|
||||||
|
if lanes.insert(lane.lifeline, lane.x).is_some() {
|
||||||
|
issues.push(GeometryIssue::DuplicateCoordinate(lane.lifeline));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !issues.is_empty() {
|
||||||
|
return Err(issues);
|
||||||
|
}
|
||||||
|
// The actual automatic traversal runs first, with authored lane centers already
|
||||||
|
// applied. It supplies every untouched anchor, shape, insertion scope and label.
|
||||||
|
let baseline = Scene::sequence_geometry(
|
||||||
|
p,
|
||||||
|
owner,
|
||||||
|
selected,
|
||||||
|
(!lanes.is_empty()).then_some(&lanes),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
let mut desired = BTreeMap::new();
|
||||||
|
let mut seen = std::collections::BTreeSet::new();
|
||||||
|
let mut latent_messages = Vec::new();
|
||||||
|
let line_ids: std::collections::BTreeSet<_> =
|
||||||
|
baseline.lines.iter().map(|line| line.id).collect();
|
||||||
|
for message in &layout.messages {
|
||||||
|
let id = message.address.occurrence;
|
||||||
|
if !seen.insert(id) {
|
||||||
|
issues.push(GeometryIssue::DuplicateCoordinate(id));
|
||||||
|
}
|
||||||
|
if !message.address.references.is_empty() {
|
||||||
|
issues.push(GeometryIssue::ExpandedAddressNotSupported(id));
|
||||||
|
}
|
||||||
|
if !message.send_y.is_finite() || message.send_y.abs() > oa_core::MAX_COORDINATE {
|
||||||
|
issues.push(GeometryIssue::InvalidCoordinate(id));
|
||||||
|
}
|
||||||
|
let Some(OccurrenceBody::Exchange {
|
||||||
|
sender,
|
||||||
|
receiver,
|
||||||
|
message_kind,
|
||||||
|
..
|
||||||
|
}) = occurrence(p, id)
|
||||||
|
else {
|
||||||
|
issues.push(GeometryIssue::UnknownAnchor(id));
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if !baseline.rows.contains_key(&id) {
|
||||||
|
issues.push(GeometryIssue::UnknownAnchor(id));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !line_ids.contains(&id) {
|
||||||
|
latent_messages.push(id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Legacy lifecycle rows denote receipt, whereas the saved field denotes send.
|
||||||
|
let row = if sender.is_some()
|
||||||
|
&& sender == receiver
|
||||||
|
&& matches!(message_kind, MessageKind::Create | MessageKind::Delete)
|
||||||
|
{
|
||||||
|
message.send_y + 24.
|
||||||
|
} else {
|
||||||
|
message.send_y
|
||||||
|
};
|
||||||
|
desired.insert(id, row);
|
||||||
|
}
|
||||||
|
let actual_lanes = &baseline.render_lanes;
|
||||||
|
// A coincident pair is not a self message. Do not invent an arrow or lane order.
|
||||||
|
for line in &baseline.lines {
|
||||||
|
if let Some(OccurrenceBody::Exchange {
|
||||||
|
sender: Some(sender),
|
||||||
|
receiver,
|
||||||
|
message_kind,
|
||||||
|
..
|
||||||
|
}) = occurrence(p, line.id)
|
||||||
|
{
|
||||||
|
let target = if matches!(message_kind, MessageKind::Timer { .. }) {
|
||||||
|
Some(*sender)
|
||||||
|
} else {
|
||||||
|
*receiver
|
||||||
|
};
|
||||||
|
if target.is_some_and(|target| target != *sender)
|
||||||
|
&& target
|
||||||
|
.is_some_and(|target| actual_lanes.get(sender) == actual_lanes.get(&target))
|
||||||
|
{
|
||||||
|
issues.push(GeometryIssue::CoincidentDistinctEndpoints(line.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !issues.is_empty() {
|
||||||
|
return Err(issues);
|
||||||
|
}
|
||||||
|
let mut plan = Plan::build(p, owner, &baseline, &desired).map_err(|issue| vec![issue])?;
|
||||||
|
// Keep the schema's exact send value; send+24 is performed once. In particular,
|
||||||
|
// a self Create/Delete receive row must never round-trip back into saved send.
|
||||||
|
for message in &layout.messages {
|
||||||
|
let id = message.address.occurrence;
|
||||||
|
if let Some(anchors) = plan.messages.get_mut(&id) {
|
||||||
|
let self_message = matches!(occurrence(p, id), Some(OccurrenceBody::Exchange {
|
||||||
|
sender: Some(sender), receiver, message_kind, ..
|
||||||
|
}) if matches!(message_kind, MessageKind::Timer { .. }) || *receiver == Some(*sender));
|
||||||
|
*anchors = MessageAnchors {
|
||||||
|
send_y: message.send_y,
|
||||||
|
receive_y: if self_message {
|
||||||
|
message.send_y + 24.
|
||||||
|
} else {
|
||||||
|
message.send_y
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Typed bars require a positive receiver interval. This is a narrow renderer
|
||||||
|
// limitation, not a global ban on reordered/negative asynchronous anchors.
|
||||||
|
for (&id, message) in &plan.messages {
|
||||||
|
match occurrence(p, id) {
|
||||||
|
Some(OccurrenceBody::Exchange {
|
||||||
|
message_kind: MessageKind::Call,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
let scope = baseline.rows[&id].0;
|
||||||
|
let (start, end) = if scope == owner {
|
||||||
|
(85., plan.height - 20.)
|
||||||
|
} else {
|
||||||
|
plan.scopes[&scope]
|
||||||
|
};
|
||||||
|
if desired.contains_key(&id) && (message.send_y < start || message.receive_y >= end)
|
||||||
|
{
|
||||||
|
issues.push(GeometryIssue::TypedActivationOutsideScope { call: id, scope });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(OccurrenceBody::Exchange {
|
||||||
|
message_kind: MessageKind::Reply,
|
||||||
|
reply_to: Some(call),
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
if let Some(start) = plan.messages.get(call) {
|
||||||
|
if (desired.contains_key(&id) || desired.contains_key(call))
|
||||||
|
&& message.send_y <= start.receive_y
|
||||||
|
{
|
||||||
|
issues.push(GeometryIssue::TypedActivationCrossingNotSupported {
|
||||||
|
call: *call,
|
||||||
|
event: id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !issues.is_empty() {
|
||||||
|
return Err(issues);
|
||||||
|
}
|
||||||
|
for segment in &baseline.activations {
|
||||||
|
if baseline
|
||||||
|
.rows
|
||||||
|
.get(&segment.call)
|
||||||
|
.is_none_or(|(scope, _)| *scope != segment.scope)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(start) = plan.messages.get(&segment.call) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let event = match segment.finish {
|
||||||
|
scenario_activation::Finish::Reply(event)
|
||||||
|
| scenario_activation::Finish::Terminated(event) => Some(event),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
if let Some(event) = event {
|
||||||
|
if (desired.contains_key(&event) || desired.contains_key(&segment.call))
|
||||||
|
&& plan.anchors[&event].y <= start.receive_y
|
||||||
|
{
|
||||||
|
issues.push(GeometryIssue::TypedActivationCrossingNotSupported {
|
||||||
|
call: segment.call,
|
||||||
|
event,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !issues.is_empty() {
|
||||||
|
return Err(issues);
|
||||||
|
}
|
||||||
|
let scene = Scene::sequence_geometry(
|
||||||
|
p,
|
||||||
|
owner,
|
||||||
|
selected,
|
||||||
|
(!lanes.is_empty()).then_some(&lanes),
|
||||||
|
Some(&plan),
|
||||||
|
);
|
||||||
|
// Compare the actual projector, whose cursor rules include sibling scopes and
|
||||||
|
// lifecycle/fragment events. Retain existing incomplete-model uncertainty;
|
||||||
|
// reject only newly missing geometry introduced by this presentation.
|
||||||
|
let previous_missing = baseline.lifecycle.missing_geometry();
|
||||||
|
for (lifeline, scope) in scene
|
||||||
|
.lifecycle
|
||||||
|
.missing_geometry()
|
||||||
|
.difference(&previous_missing)
|
||||||
|
{
|
||||||
|
issues.push(GeometryIssue::LifecycleGeometryNotSupported {
|
||||||
|
lifeline: *lifeline,
|
||||||
|
scope: *scope,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if !issues.is_empty() {
|
||||||
|
return Err(issues);
|
||||||
|
}
|
||||||
|
for segment in &baseline.activations {
|
||||||
|
let Some(seed) = baseline
|
||||||
|
.lines
|
||||||
|
.iter()
|
||||||
|
.find(|line| line.id == segment.call)
|
||||||
|
.and_then(|line| line.points.last())
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if segment.rect.y != seed.y || baseline.rows[&segment.call].0 != segment.scope {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let receive = plan.messages[&segment.call].receive_y;
|
||||||
|
if !desired.is_empty()
|
||||||
|
&& !scene.activations.iter().any(|current| {
|
||||||
|
current.call == segment.call
|
||||||
|
&& current.scope == segment.scope
|
||||||
|
&& current.rect.y == receive
|
||||||
|
})
|
||||||
|
{
|
||||||
|
issues.push(GeometryIssue::TypedActivationOutsideLifetime {
|
||||||
|
call: segment.call,
|
||||||
|
receiver: segment.receiver,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !issues.is_empty() {
|
||||||
|
return Err(issues);
|
||||||
|
}
|
||||||
|
let bounds = structural_bounds(&scene);
|
||||||
|
let all_lanes = scene.render_lanes.clone();
|
||||||
|
Ok(ResolvedScenarioPlan {
|
||||||
|
representation,
|
||||||
|
owner,
|
||||||
|
lanes: all_lanes,
|
||||||
|
plan,
|
||||||
|
latent_messages,
|
||||||
|
bounds,
|
||||||
|
scene,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn structural_bounds(scene: &Scene) -> Rectangle {
|
||||||
|
let mut bounds = Rectangle::new(scene.layout_origin, scene.size);
|
||||||
|
for item in &scene.boxes {
|
||||||
|
bounds = union(bounds, item.rect);
|
||||||
|
}
|
||||||
|
for line in &scene.lines {
|
||||||
|
if !line.label.is_empty() {
|
||||||
|
bounds = union(bounds, line.label_bounds());
|
||||||
|
}
|
||||||
|
for point in &line.points {
|
||||||
|
// Arrow wings extend beyond the centerline in either direction.
|
||||||
|
bounds = union(
|
||||||
|
bounds,
|
||||||
|
Rectangle::new(Point::new(point.x - 8., point.y - 8.), Size::new(16., 16.)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for segment in &scene.activations {
|
||||||
|
bounds = union(bounds, segment.rect);
|
||||||
|
}
|
||||||
|
for point in &scene.open_calls {
|
||||||
|
bounds = union(
|
||||||
|
bounds,
|
||||||
|
Rectangle::new(Point::new(point.x - 8., point.y - 8.), Size::new(16., 16.)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(lifetime) = scene.lifecycle.bounds() {
|
||||||
|
bounds = union(bounds, lifetime);
|
||||||
|
}
|
||||||
|
bounds
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -31,6 +31,7 @@ struct EventMarker {
|
|||||||
capsule: Rectangle,
|
capsule: Rectangle,
|
||||||
name: String,
|
name: String,
|
||||||
sender_x: Option<f32>,
|
sender_x: Option<f32>,
|
||||||
|
self_message: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, PartialEq)]
|
#[derive(Debug, Default, Clone, PartialEq)]
|
||||||
@@ -89,7 +90,7 @@ impl Rendering {
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
for event in &lane.markers {
|
for event in &lane.markers {
|
||||||
let sender_x = project
|
let sender_id = project
|
||||||
.elements
|
.elements
|
||||||
.get(&event.occurrence)
|
.get(&event.occurrence)
|
||||||
.and_then(|e| e.shared())
|
.and_then(|e| e.shared())
|
||||||
@@ -101,7 +102,7 @@ impl Rendering {
|
|||||||
..
|
..
|
||||||
},
|
},
|
||||||
..
|
..
|
||||||
} => lanes.get(sender).copied(),
|
} => Some(*sender),
|
||||||
_ => None,
|
_ => None,
|
||||||
});
|
});
|
||||||
markers.push(EventMarker {
|
markers.push(EventMarker {
|
||||||
@@ -113,7 +114,8 @@ impl Rendering {
|
|||||||
Size::new(140., 28.),
|
Size::new(140., 28.),
|
||||||
),
|
),
|
||||||
name: project.named(lane.lifeline).to_owned(),
|
name: project.named(lane.lifeline).to_owned(),
|
||||||
sender_x,
|
sender_x: sender_id.and_then(|id| lanes.get(&id).copied()),
|
||||||
|
self_message: sender_id == Some(lane.lifeline),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,7 +140,7 @@ impl Rendering {
|
|||||||
};
|
};
|
||||||
let x = marker.center.x;
|
let x = marker.center.x;
|
||||||
let y = marker.center.y;
|
let y = marker.center.y;
|
||||||
let self_message = sender == x;
|
let self_message = marker.self_message;
|
||||||
let end_x = match marker.event.kind {
|
let end_x = match marker.event.kind {
|
||||||
MarkerKind::Birth if sender < x => marker.capsule.x,
|
MarkerKind::Birth if sender < x => marker.capsule.x,
|
||||||
MarkerKind::Birth => marker.capsule.x + marker.capsule.width,
|
MarkerKind::Birth => marker.capsule.x + marker.capsule.width,
|
||||||
@@ -166,6 +168,48 @@ impl Rendering {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Geometry loss is distinct from warning-admitted semantic uncertainty.
|
||||||
|
/// The saved bridge compares these exact projector diagnostics with baseline.
|
||||||
|
pub(in crate::behavior) fn missing_geometry(&self) -> std::collections::BTreeSet<(Id, Id)> {
|
||||||
|
self.lifetimes
|
||||||
|
.iter()
|
||||||
|
.flat_map(|lane| {
|
||||||
|
lane.uncertainties
|
||||||
|
.iter()
|
||||||
|
.filter(|issue| issue.reason == super::UncertaintyReason::MissingGeometry)
|
||||||
|
.map(|issue| (lane.lifeline, issue.scope))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Complete structural lifetime bounds, including the birth capsule and death mark.
|
||||||
|
pub(in crate::behavior) fn bounds(&self) -> Option<Rectangle> {
|
||||||
|
let mut bounds = self.span.and_then(|span| {
|
||||||
|
self.lanes
|
||||||
|
.values()
|
||||||
|
.map(|x| {
|
||||||
|
Rectangle::new(
|
||||||
|
Point::new(*x, span.start),
|
||||||
|
Size::new(0., span.end - span.start),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.reduce(crate::behavior::scenario_layout::union)
|
||||||
|
});
|
||||||
|
for marker in &self.markers {
|
||||||
|
let rect = match marker.event.kind {
|
||||||
|
MarkerKind::Birth => marker.capsule,
|
||||||
|
MarkerKind::Death => Rectangle::new(
|
||||||
|
Point::new(marker.center.x - 10., marker.center.y - 10.),
|
||||||
|
Size::new(20., 20.),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
bounds = Some(bounds.map_or(rect, |old| {
|
||||||
|
crate::behavior::scenario_layout::union(old, rect)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
bounds
|
||||||
|
}
|
||||||
|
|
||||||
pub(in crate::behavior) fn alive_spans(
|
pub(in crate::behavior) fn alive_spans(
|
||||||
&self,
|
&self,
|
||||||
lifeline: Id,
|
lifeline: Id,
|
||||||
|
|||||||
Reference in New Issue
Block a user