Preserve resolver review and record coherent saved-view fallback policy
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
# Scenario resolver source review
|
||||
|
||||
The [review](review.md) records two lifetime-geometry losses, negative-origin extent inflation and float round-trip defects found before compilation. The original three-file implementation snapshot is preserved under [original](original/), with exact hashes; its absolute paths are historical provenance. The corrected four-file snapshot is bound by [corrected-manifest.json](corrected-manifest.json) and now available at source [68a81c3](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/commit/68a81c3f50d846e34cae96090979543878514c1e). The independent correction review verified all four files and exact 22 prepared test names, finding no remaining blocker within its stated scope. Compilation, execution and native acceptance remain separate gates.
|
||||
|
||||
The [gesture critique](gesture-critique.md) compares spacing with continuous semantic reordering. It supports the next bounded spacing-plus-explicit-Reorder prototype, subject to native discoverability and boundary-feedback evaluation; it is not user testing or release acceptance.
|
||||
|
||||
[archive-manifest.json](archive-manifest.json) records exact original copies. No compiler, tests or native input were run to create this archive. Full Project/source invariants and runtime numeric regressions must still pass on the corrected code.
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"sourceCommit": "68a81c3f50d846e34cae96090979543878514c1e",
|
||||
"scope": "Source review only; no compiler/runtime acceptance",
|
||||
"files": [
|
||||
{
|
||||
"path": "original/crates/oa-desktop/src/behavior.rs",
|
||||
"bytes": 135253,
|
||||
"sha256": "35b7feda8c221c97781729df426b7d2860740d06ab0fe0adfa833af11abd69c8"
|
||||
},
|
||||
{
|
||||
"path": "original/crates/oa-desktop/src/behavior/scenario_layout.rs",
|
||||
"bytes": 26891,
|
||||
"sha256": "e27e58fe9a3bceaa3ae9ea332c4dd3c9d150a10f1a3817dd7aaebd0d79dc9336"
|
||||
},
|
||||
{
|
||||
"path": "original/crates/oa-desktop/src/behavior/scenario_lifecycle/render.rs",
|
||||
"bytes": 16028,
|
||||
"sha256": "d31fc2443268a28adb525302b70dff640ef04ea34de0bcdd1a8bdb4c70be9435"
|
||||
},
|
||||
{
|
||||
"path": "original/manifest.json",
|
||||
"bytes": 788,
|
||||
"sha256": "dda7c92b0db3002b3c220d04d8cc6bfe32d6c4ac4092c013660d24f0c0710f0a"
|
||||
},
|
||||
{
|
||||
"path": "review.md",
|
||||
"bytes": 12284,
|
||||
"sha256": "5a5ef96af80d6d6d4ad8bf7d2143637238678659372476efcffca0b0ab8783ef"
|
||||
},
|
||||
{
|
||||
"path": "corrected-manifest.json",
|
||||
"bytes": 2702,
|
||||
"sha256": "4606e5c40cc39e1c8331cc10196c65c8536b9d7c1bdbe6799b79e531478a76a0"
|
||||
},
|
||||
{
|
||||
"path": "gesture-critique.md",
|
||||
"bytes": 8710,
|
||||
"sha256": "028481527321ce98fc030f018755ebadc18a79acdc30f227e91cd8885a517229"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"status": "v2 frozen source; no compiler/tests",
|
||||
"files": [
|
||||
{
|
||||
"path": "crates/oa-desktop/src/behavior.rs",
|
||||
"bytes": 135690,
|
||||
"sha256": "f50b57d3c73aa2b5a2ad2b5311db2c71012a55061909f8d4a484f91d7d8445c0"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-desktop/src/behavior/scenario_layout.rs",
|
||||
"bytes": 30094,
|
||||
"sha256": "2f3bff1d752cb939a6ed19e6c3b7895a227141ae3b14b846236fae828063a9a8"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-desktop/src/behavior/scenario_layout/tests.rs",
|
||||
"bytes": 42961,
|
||||
"sha256": "61027f58a24ab9cca08e691c98216b418a1f175e50a1aa37d1f413504e5b14eb"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-desktop/src/behavior/scenario_lifecycle/render.rs",
|
||||
"bytes": 16626,
|
||||
"sha256": "4351f53805b1ea43a095f15fd4beb9057fec937b7258397e0d44b01a93113086"
|
||||
}
|
||||
],
|
||||
"filter": "behavior::scenario_layout::tests::",
|
||||
"expected_test_count": 22,
|
||||
"tests": [
|
||||
"absent_empty_and_explicit_baseline_saved_values_preserve_complete_scene_and_source",
|
||||
"independent_saved_appearances_of_reused_exchange_have_exact_anchors_and_no_ambient_state",
|
||||
"authored_lanes_precede_headers_arrows_lifetimes_and_complete_bounds",
|
||||
"same_x_distinct_lifelines_are_not_self_messages_in_either_bridge",
|
||||
"self_call_receive_and_reply_send_drive_the_full_typed_execution",
|
||||
"moved_death_clips_actual_bar_and_preserves_unanswered_cue",
|
||||
"explicit_root_scope_failures_do_not_clamp_calls_or_lifecycle_but_async_can_be_negative",
|
||||
"reply_crossing_is_a_specific_typed_failure",
|
||||
"complete_scope_plan_rejects_escape_preserves_overlap_and_keeps_headers_and_followers",
|
||||
"unfinished_coordinate_is_latent_and_zero_or_signed_zero_never_rewrites_source",
|
||||
"self_lifecycle_saved_send_is_converted_once_to_the_actual_receive_marker",
|
||||
"timer_uses_implicit_effective_receiver_and_true_self_offset",
|
||||
"latent_message_becomes_exact_only_when_its_own_endpoint_is_completed",
|
||||
"wide_gap_bounds_include_all_actual_shapes_and_typed_extensions",
|
||||
"earlier_receiver_death_and_prebirth_call_are_named_typed_failures_not_disappearing_bars",
|
||||
"unsupported_expansion_duplicate_keys_and_foreign_identity_fail_without_mutating_input",
|
||||
"saved_direct_nested_message_uses_operand_scopes_without_reparenting_or_moving_followers",
|
||||
"exact_authored_lane_bits_survive_headers_and_tiny_distinct_lanes_are_not_coincident",
|
||||
"newly_skipped_sibling_scope_is_rejected_using_real_projector_diagnostics",
|
||||
"post_fragment_death_cannot_disappear_and_feasible_operand_spacing_retains_its_marker",
|
||||
"authored_send_bits_survive_baseline_delta_and_self_lifecycle_row_conversion",
|
||||
"decimal_call_anchor_is_admitted_and_exact_in_plan_line_and_typed_activation"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
# Scenario message gesture critique — source only
|
||||
|
||||
Recommendation: choose A as the first candidate to evaluate: ordinary arrow/label drag changes spacing; semantic reorder is a separately identified action. This is a proposed product policy, not established user intent or runtime acceptance. Do not ship A by simply relegating the existing gesture to its current small left-hand handle. One bounded native workflow experiment is needed for discoverability and boundary feedback before acceptance; B need not be implemented in full merely to make that comparison.
|
||||
|
||||
## Facts and correction of the initial premise
|
||||
|
||||
The requirements in harness/scenario-layout-design.md (Movement policy still to evaluate; Projection and editing lifecycle) require direct editing, explicit view identity, one checked transaction, no accidental operand reparenting, exact no-op/cancel behavior and graphical/text/API parity. They explicitly leave A versus B unsettled.
|
||||
|
||||
Current ordinary dragging already reorders: immutable695685a behavior.rs:1886–1901 captures SequencePress::Reorder for any hit whose ID has a row, falling back to the selected handle;1813 uses a three-screen-pixel threshold,1972–1998 selects on unmoved release and publishes Reorder on moved release. The current renderer readback has the same branch at1923–1938. Thus A deliberately changes an existing body/label gesture. scenario.rs:1071–1096 preserves the parent sequence and validates SetSteps with execute_batch; it is not an independent send/receive reorder command.
|
||||
|
||||
The existing handle is world rect x6..28, y=row−12..row+12 (behavior.rs:1660–1665), drawn inside camera translation and scale. At scale0.05 its body is only1.1×1.2 screen pixels. Panning can place x6 entirely outside the visible viewport even while an authored negative-x or far-right message is visible. Selection-only ↕ has no inherently distinct meaning from “move vertically.” These are concrete reasons that keeping the current handle alone does not establish usable explicit reorder.
|
||||
|
||||
## Consequential comparison
|
||||
|
||||
| Workflow | A: spacing + explicit reorder | B: body drag switches into reorder |
|
||||
| --- | --- | --- |
|
||||
| Spread two adjacent messages for readability | Directly expresses the requested presentation edit. Must show a truthful feasible boundary instead of silently clamping or moving unrelated authored anchors. | Direct while inside the interval; becomes a semantic edit at the point where the user is most likely to seek more space. |
|
||||
| Accidental crossing in a dense/Fit view | Steps and operand remain invariant; preview can reject the infeasible candidate while explaining why. | A three-pixel movement can span several rows at small zoom. A visual threshold crossing cannot establish intent to change all representations of the scenario. Requires conspicuous mode feedback and stable hysteresis even for valid changes. |
|
||||
| Deliberate reorder | Extra explicit action and migration from the current body gesture; must be discoverable and available near the selection or in a conventional command. | Preserves existing motor behavior and enables a continuous gesture. However, deciding whether final y means spacing or a new slot adds ambiguity; validation after release is too late to explain preview semantics. |
|
||||
| Two views of one owner | A spacing drag modifies only the chosen Diagram. Explicit reorder still changes shared semantics in both views. | A drag started for one layout may change shared semantics in both views. Atomic history does not make that side effect local. |
|
||||
|
||||
A is preferred because “move this arrow to improve this arrangement” has an unambiguous view-local meaning, while B infers a semantic instruction from travel distance. This inference is not supported by the movement requirement. B remains a reasonable alternative if a workflow experiment demonstrates that explicit reorder is a substantial obstacle despite a visible command, and users understand its shared-model consequences. Existing implementation alone does not decide the issue.
|
||||
|
||||
## Required semantic and interaction boundaries
|
||||
|
||||
- A preserves the existing steps and parents. B must preview a proposed whole-occurrence SetSteps plus active-view layout as one transaction, validate that combined project, and never infer operand reparenting. No exact minimum gap, collision policy or arbitrary independent send/receive order has been adopted. Same-scope vertical limits must account for actual receive geometry, not just sorted row/send y.
|
||||
- Call/Reply and Create/Delete impose real model/lifetime constraints; self-call receive is below send and activation starts there. A spacing preview must rerun the same geometry/lifecycle/activation pipeline. B cannot silently move a partner Call/Reply, creation/deletion or child occurrence to make an invalid crossing valid. Warning-bearing models need truthful warning display; successful validation alone does not demonstrate the user intended the new chronology.
|
||||
- Hit identity is not enough to define a spacing gesture. Scene::hit at1667 onward returns a call ID for an activation shoulder as well as for its arrow/label. A naive replacement of the current rows.contains_key branch would move a whole message when the user drags an activation bar. Distinguish arrow/label hit roles from activation, marker, handle and other shapes; this is a required narrow interaction seam, not a reason to redesign semantic identity.
|
||||
- Existing release cancellation covers bounds, wheel, unfocus and Escape. New previews must additionally retain Diagram/derived-view identity and view-visit context. Switching A→B→A views of one owner must invalidate the old gesture. A3px return-to-origin gesture is still a final no-op and must not erase latent intent, create a view or discard redo history.
|
||||
- Structurally admitted imported geometry can cross rows or exceed the current projector's supported scope. Neither gesture may normalize it merely on click/zero-motion release, or derive a semantic reorder from its already crossed appearance. Offer an explicit repair path when a nonzero candidate cannot be realized; a failed preview must leave accepted geometry/source intact.
|
||||
|
||||
## One bounded experiment and five failure probes
|
||||
|
||||
Use one matched native candidate with A, an explicitly named Reorder command visible with the selection, and the existing handle only as an additional shortcut. Record actual preview, resulting source/steps/layout, both views, Undo and cancel; do not claim usability from a scripted pixel trace alone. Give an unprompted “make this diagram easier to read, then swap these two messages” task to the user or an independent workflow evaluator. If reorder cannot be found without explaining ↕, or spacing looks broken at a boundary, A fails its first acceptance attempt.
|
||||
|
||||
1. Two asynchronous arrows near each other: drag body and long label slightly apart, then overshoot the neighbor at Fit zoom. Expect one local layout edit for an admitted release; a visibly rejected crossing preserves steps/source and offers the explicit reorder action. No unexplained jump or implicit neighboring-anchor rewrite.
|
||||
2. Deliberately swap two same-parent arrows, including a visible selected arrow whose world x is negative and a horizontally scrolled far-right arrow. Find and use Reorder without returning the camera to x0. Same-parent steps change once; crossing an ALT/PAR operand boundary is visibly refused.
|
||||
3. Self Call send610/receive634 with Reply send728, then a receiver Delete: space the Call and inspect actual receive/activation/Reply/lifetime geometry; attempt invalid explicit reorder across Reply/Delete. Reject unsupported candidates without losing valid spacing/source. Clicking or dragging an exposed activation shoulder must retain its documented selection behavior.
|
||||
4. Two differently arranged saved views of one owner: space one occurrence in view A; verify B's authored coordinates and semantic steps unchanged. Explicitly reorder in A and inspect shared semantics plus retained B layout. Undo once restores the complete transaction, including exact prior overrides. A stale release after A→B→A edits neither view.
|
||||
5. Start from a source-admitted crossed/latent coordinate and a live redo branch: click, Escape after motion, return exactly to origin, release outside, and switch views during capture. No source/revision/redo change. A real valid repair drag produces exactly one entry.
|
||||
|
||||
Limits: this review ran no GUI or compiler and implements no gesture. Body-or-handle behavior was checked against immutable695685a and an explicitly identified current readback; the new renderer remains in development. Candidate policy must be adopted explicitly before implementation.
|
||||
+3458
File diff suppressed because it is too large
Load Diff
+753
@@ -0,0 +1,753 @@
|
||||
//! 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),
|
||||
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 {
|
||||
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 delta = anchor.y - seed[&line.id].y;
|
||||
messages.insert(
|
||||
line.id,
|
||||
MessageAnchors {
|
||||
send_y: first.y + delta,
|
||||
receive_y: last.y + delta,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
// 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: BTreeMap<_, _> = baseline
|
||||
.boxes
|
||||
.iter()
|
||||
.filter(|item| {
|
||||
p.elements
|
||||
.get(&item.id)
|
||||
.is_some_and(|e| e.kind() == Kind::Lifeline)
|
||||
})
|
||||
.map(|item| (item.id, item.rect.x + item.rect.width / 2.))
|
||||
.collect();
|
||||
// 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 plan = Plan::build(p, owner, &baseline, &desired).map_err(|issue| vec![issue])?;
|
||||
// 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),
|
||||
);
|
||||
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
|
||||
.boxes
|
||||
.iter()
|
||||
.filter(|item| {
|
||||
p.elements
|
||||
.get(&item.id)
|
||||
.is_some_and(|e| e.kind() == Kind::Lifeline)
|
||||
})
|
||||
.map(|item| (item.id, item.rect.x + item.rect.width / 2.))
|
||||
.collect();
|
||||
Ok(ResolvedScenarioPlan {
|
||||
representation,
|
||||
owner,
|
||||
lanes: all_lanes,
|
||||
plan,
|
||||
latent_messages,
|
||||
bounds,
|
||||
scene,
|
||||
})
|
||||
}
|
||||
|
||||
fn structural_bounds(scene: &Scene) -> Rectangle {
|
||||
let mut bounds = Rectangle::new(Point::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
|
||||
}
|
||||
+437
@@ -0,0 +1,437 @@
|
||||
//! Scene bridge for local lifetime geometry; no authoring or model mutations.
|
||||
use super::{Geometry, LaneLifetime, Life, Marker, MarkerKind, Span, project_lifetimes};
|
||||
use crate::{behavior::Line, style, views::short};
|
||||
use iced::{
|
||||
Color, Point, Rectangle, Size,
|
||||
widget::canvas::{self, Frame, Path, Stroke, Text},
|
||||
};
|
||||
use oa_core::{Id, OccurrenceBody, Project, SharedBody};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
// Test-only geometry input, deliberately upstream of rows, scopes, lifecycle
|
||||
// and activations. This is not an authored placement or drag policy.
|
||||
#[cfg(test)]
|
||||
thread_local! {
|
||||
static TEST_ROW_OFFSETS: std::cell::RefCell<BTreeMap<Id, f32>> = Default::default();
|
||||
}
|
||||
#[cfg(test)]
|
||||
pub(in crate::behavior) fn apply_test_row_offset(id: Id, y: &mut f32) {
|
||||
TEST_ROW_OFFSETS.with(|offsets| {
|
||||
if let Some(offset) = offsets.borrow().get(&id) {
|
||||
*y += offset;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
struct EventMarker {
|
||||
event: Marker,
|
||||
lifeline: Id,
|
||||
center: Point,
|
||||
capsule: Rectangle,
|
||||
name: String,
|
||||
sender_x: Option<f32>,
|
||||
self_message: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub(in crate::behavior) struct Rendering {
|
||||
// Complete rendering positions, including warning-admitted unassigned lanes.
|
||||
// Scene.lanes independently retains the existing authoring eligibility rule.
|
||||
lanes: BTreeMap<Id, f32>,
|
||||
span: Option<Span>,
|
||||
lifetimes: Vec<LaneLifetime>,
|
||||
markers: Vec<EventMarker>,
|
||||
}
|
||||
|
||||
impl Rendering {
|
||||
pub(in crate::behavior) fn new(
|
||||
project: &Project,
|
||||
owner: Id,
|
||||
lanes: &BTreeMap<Id, f32>,
|
||||
rows: &BTreeMap<Id, (Id, f32)>,
|
||||
insertion_scopes: &BTreeMap<Id, (f32, f32)>,
|
||||
height: f32,
|
||||
) -> Self {
|
||||
let span = Span {
|
||||
start: 85.,
|
||||
end: height - 20.,
|
||||
};
|
||||
if !span.valid() {
|
||||
return Self::default();
|
||||
}
|
||||
// Insertion scopes deliberately start later and end earlier. Empty and
|
||||
// Create-only scenarios still have the full visible lifetime extent.
|
||||
let mut scopes = insertion_scopes.clone();
|
||||
for (scope, (start, end)) in &mut scopes {
|
||||
if *scope != owner && *start == *end && start.is_finite() {
|
||||
// An empty operand has 42 px between its insertion axis and
|
||||
// frame end. Match the existing 26 px open-activation allowance.
|
||||
*end = *start + 26.;
|
||||
}
|
||||
}
|
||||
scopes.insert(owner, (span.start, span.end));
|
||||
let lifetimes = project_lifetimes(
|
||||
project,
|
||||
owner,
|
||||
&Geometry {
|
||||
rows,
|
||||
scopes: &scopes,
|
||||
},
|
||||
);
|
||||
let lanes: BTreeMap<_, _> = lanes
|
||||
.iter()
|
||||
.filter(|(_, x)| x.is_finite())
|
||||
.map(|(id, x)| (*id, *x))
|
||||
.collect();
|
||||
let mut markers = Vec::new();
|
||||
for lane in &lifetimes {
|
||||
let Some(&x) = lanes.get(&lane.lifeline) else {
|
||||
continue;
|
||||
};
|
||||
for event in &lane.markers {
|
||||
let sender_id = project
|
||||
.elements
|
||||
.get(&event.occurrence)
|
||||
.and_then(|e| e.shared())
|
||||
.and_then(|body| match body {
|
||||
SharedBody::Occurrence {
|
||||
body:
|
||||
OccurrenceBody::Exchange {
|
||||
sender: Some(sender),
|
||||
..
|
||||
},
|
||||
..
|
||||
} => Some(*sender),
|
||||
_ => None,
|
||||
});
|
||||
markers.push(EventMarker {
|
||||
event: *event,
|
||||
lifeline: lane.lifeline,
|
||||
center: Point::new(x, event.y),
|
||||
capsule: Rectangle::new(
|
||||
Point::new(x - 70., event.y - 14.),
|
||||
Size::new(140., 28.),
|
||||
),
|
||||
name: project.named(lane.lifeline).to_owned(),
|
||||
sender_x: sender_id.and_then(|id| lanes.get(&id).copied()),
|
||||
self_message: sender_id == Some(lane.lifeline),
|
||||
});
|
||||
}
|
||||
}
|
||||
Self {
|
||||
lanes,
|
||||
span: Some(span),
|
||||
lifetimes,
|
||||
markers,
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::behavior) fn align_messages(&self, lines: &mut [Line]) {
|
||||
for marker in &self.markers {
|
||||
let Some(sender) = marker.sender_x else {
|
||||
continue;
|
||||
};
|
||||
let Some(line) = lines
|
||||
.iter_mut()
|
||||
.find(|line| line.id == marker.event.occurrence)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let x = marker.center.x;
|
||||
let y = marker.center.y;
|
||||
let self_message = marker.self_message;
|
||||
let end_x = match marker.event.kind {
|
||||
MarkerKind::Birth if sender < x => marker.capsule.x,
|
||||
MarkerKind::Birth => marker.capsule.x + marker.capsule.width,
|
||||
MarkerKind::Death => x,
|
||||
};
|
||||
line.points = if self_message {
|
||||
// The receiver is on the recorded occurrence row, including self
|
||||
// lifecycle events. The elbow stays outside the birth capsule.
|
||||
let elbow = x + 112.;
|
||||
line.label_at = Point::new(x + 12., y - 52.);
|
||||
vec![
|
||||
Point::new(x, y - 24.),
|
||||
Point::new(elbow, y - 24.),
|
||||
Point::new(elbow, y),
|
||||
Point::new(end_x, y),
|
||||
]
|
||||
} else {
|
||||
if marker.event.kind == MarkerKind::Birth {
|
||||
// Keep the label above the capsule, including right-to-left
|
||||
// messages whose existing label starts beside the receiver.
|
||||
line.label_at.y = y - 40.;
|
||||
}
|
||||
vec![Point::new(sender, y), Point::new(end_x, y)]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(
|
||||
&self,
|
||||
lifeline: Id,
|
||||
scope: Id,
|
||||
top: f32,
|
||||
bottom: f32,
|
||||
) -> Vec<Span> {
|
||||
if !top.is_finite() || !bottom.is_finite() || bottom <= top {
|
||||
return vec![];
|
||||
}
|
||||
self.lifetimes
|
||||
.iter()
|
||||
.find(|lane| lane.lifeline == lifeline)
|
||||
.into_iter()
|
||||
.flat_map(|lane| &lane.intervals)
|
||||
.filter(|interval| interval.scope == scope && interval.life == Life::Alive)
|
||||
.filter_map(|interval| {
|
||||
let start = top.max(interval.span.start);
|
||||
let end = bottom.min(interval.span.end);
|
||||
(start < end).then_some(Span { start, end })
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(in crate::behavior) fn marker_at(&self, point: Point) -> Option<Id> {
|
||||
self.markers
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|marker| match marker.event.kind {
|
||||
MarkerKind::Birth => marker.capsule.contains(point),
|
||||
MarkerKind::Death => marker_distance(marker, point) <= 1.25,
|
||||
})
|
||||
.map(|marker| marker.event.occurrence)
|
||||
}
|
||||
|
||||
pub(in crate::behavior) fn nearest_marker(&self, point: Point) -> Option<(Id, f32)> {
|
||||
if !point.x.is_finite() || !point.y.is_finite() {
|
||||
return None;
|
||||
}
|
||||
// Exact visible shapes are tested first by Scene. A low-zoom tolerance
|
||||
// must not outrank another marker's actual capsule, a header, or a label.
|
||||
self.markers
|
||||
.iter()
|
||||
.map(|marker| (marker.event.occurrence, marker_distance(marker, point)))
|
||||
.min_by(|a, b| a.1.total_cmp(&b.1).then_with(|| a.0.cmp(&b.0)))
|
||||
}
|
||||
|
||||
pub(in crate::behavior) fn lane_at(&self, point: Point, scale: f32) -> Option<Id> {
|
||||
let span = self.span?;
|
||||
if !scale.is_finite() || scale <= 0. || point.y < span.start || point.y > span.end {
|
||||
return None;
|
||||
}
|
||||
self.lanes
|
||||
.iter()
|
||||
.filter(|(_, x)| (**x - point.x).abs() < 12. / scale)
|
||||
.min_by(|a, b| (a.1 - point.x).abs().total_cmp(&(b.1 - point.x).abs()))
|
||||
.map(|(id, _)| *id)
|
||||
}
|
||||
|
||||
pub(in crate::behavior) fn draw_lanes(&self, frame: &mut Frame, selected: Id) {
|
||||
let Some(span) = self.span else {
|
||||
return;
|
||||
};
|
||||
for (&id, &x) in &self.lanes {
|
||||
frame.stroke(
|
||||
&Path::line(Point::new(x, span.start), Point::new(x, span.end)),
|
||||
Stroke {
|
||||
line_dash: canvas::LineDash {
|
||||
segments: &[2., 5.],
|
||||
offset: 0,
|
||||
},
|
||||
..Stroke::default()
|
||||
.with_width(1.)
|
||||
.with_color(if id == selected {
|
||||
Color::from_rgb8(148, 190, 187)
|
||||
} else {
|
||||
Color::from_rgb8(214, 222, 222)
|
||||
})
|
||||
},
|
||||
);
|
||||
}
|
||||
for lifetime in &self.lifetimes {
|
||||
let Some(&x) = self.lanes.get(&lifetime.lifeline) else {
|
||||
continue;
|
||||
};
|
||||
for interval in &lifetime.intervals {
|
||||
match interval.life {
|
||||
Life::Alive => frame.stroke(
|
||||
&Path::line(
|
||||
Point::new(x, interval.span.start),
|
||||
Point::new(x, interval.span.end),
|
||||
),
|
||||
Stroke::default().with_width(2.).with_color(
|
||||
if lifetime.lifeline == selected {
|
||||
style::TEAL
|
||||
} else {
|
||||
Color::from_rgb8(91, 113, 113)
|
||||
},
|
||||
),
|
||||
),
|
||||
Life::Unresolved if interval.span.end - interval.span.start >= 16. => {
|
||||
frame.fill_text(Text {
|
||||
content: "?".into(),
|
||||
position: Point::new(x + 9., interval.span.start + 2.),
|
||||
size: 11.into(),
|
||||
color: style::MUTED,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !self.lanes.is_empty() {
|
||||
frame.fill_text(Text {
|
||||
content:
|
||||
"Dotted: editing guide · Solid: local lifetime · ?: lifetime depends on path"
|
||||
.into(),
|
||||
position: Point::new(28., span.end + 4.),
|
||||
size: 10.into(),
|
||||
color: style::MUTED,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::behavior) fn draw_markers(
|
||||
&self,
|
||||
frame: &mut Frame,
|
||||
selected: Id,
|
||||
scale: f32,
|
||||
pan: iced::Vector,
|
||||
) {
|
||||
for marker in &self.markers {
|
||||
let color = if selected == marker.event.occurrence || selected == marker.lifeline {
|
||||
style::TEAL
|
||||
} else {
|
||||
style::INK
|
||||
};
|
||||
match marker.event.kind {
|
||||
MarkerKind::Birth => {
|
||||
let path = Path::rounded_rectangle(
|
||||
marker.capsule.position(),
|
||||
marker.capsule.size(),
|
||||
6.into(),
|
||||
);
|
||||
frame.fill(&path, Color::from_rgb8(243, 248, 248));
|
||||
frame.stroke(
|
||||
&path,
|
||||
Stroke::default().with_color(color).with_width(
|
||||
if selected == marker.event.occurrence {
|
||||
2.5
|
||||
} else {
|
||||
1.5
|
||||
},
|
||||
),
|
||||
);
|
||||
if let Some(clip) = birth_label_clip(pan, marker.capsule, scale, frame.size()) {
|
||||
// WGPU clipped frames start with an identity transform;
|
||||
// neither the parent's pan/scale nor its clip is inherited.
|
||||
frame.with_clip(clip, |label| {
|
||||
label.translate(pan);
|
||||
label.scale(scale);
|
||||
label.fill_text(Text {
|
||||
content: short(&marker.name, 18),
|
||||
position: marker.capsule.position() + iced::Vector::new(8., 6.),
|
||||
size: 12.into(),
|
||||
color,
|
||||
..Default::default()
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
MarkerKind::Death => {
|
||||
for (a, b) in death_strokes(marker.center) {
|
||||
frame.stroke(
|
||||
&Path::line(a, b),
|
||||
Stroke::default().with_color(color).with_width(2.5),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn birth_label_clip(
|
||||
pan: iced::Vector,
|
||||
capsule: Rectangle,
|
||||
scale: f32,
|
||||
viewport: Size,
|
||||
) -> Option<Rectangle> {
|
||||
if !pan.x.is_finite() || !pan.y.is_finite() || !scale.is_finite() || scale <= 0. {
|
||||
return None;
|
||||
}
|
||||
Rectangle::new(
|
||||
Point::new(
|
||||
pan.x + (capsule.x + 8.) * scale,
|
||||
pan.y + (capsule.y + 4.) * scale,
|
||||
),
|
||||
Size::new((capsule.width - 16.) * scale, (capsule.height - 8.) * scale),
|
||||
)
|
||||
.intersection(&Rectangle::new(Point::ORIGIN, viewport))
|
||||
}
|
||||
|
||||
fn marker_distance(marker: &EventMarker, point: Point) -> f32 {
|
||||
match marker.event.kind {
|
||||
MarkerKind::Birth => {
|
||||
let dx = (marker.capsule.x - point.x)
|
||||
.max(point.x - marker.capsule.x - marker.capsule.width)
|
||||
.max(0.);
|
||||
let dy = (marker.capsule.y - point.y)
|
||||
.max(point.y - marker.capsule.y - marker.capsule.height)
|
||||
.max(0.);
|
||||
dx.hypot(dy)
|
||||
}
|
||||
MarkerKind::Death => death_strokes(marker.center)
|
||||
.iter()
|
||||
.map(|(a, b)| crate::diagram::distance_to_segment(point, *a, *b))
|
||||
.fold(f32::INFINITY, f32::min),
|
||||
}
|
||||
}
|
||||
|
||||
fn death_strokes(center: Point) -> [(Point, Point); 2] {
|
||||
[
|
||||
(
|
||||
center + iced::Vector::new(-7., -7.),
|
||||
center + iced::Vector::new(7., 7.),
|
||||
),
|
||||
(
|
||||
center + iced::Vector::new(-7., 7.),
|
||||
center + iced::Vector::new(7., -7.),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "render_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"source_worktree": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\scenario-renderer-bridge-ytr83r-20260913",
|
||||
"base_import": "695685a",
|
||||
"status": "source-only, formatted; no compiler or test execution",
|
||||
"files": [
|
||||
{
|
||||
"path": "crates/oa-desktop/src/behavior.rs",
|
||||
"bytes": 135253,
|
||||
"sha256": "35b7feda8c221c97781729df426b7d2860740d06ab0fe0adfa833af11abd69c8"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-desktop/src/behavior/scenario_layout.rs",
|
||||
"bytes": 26891,
|
||||
"sha256": "e27e58fe9a3bceaa3ae9ea332c4dd3c9d150a10f1a3817dd7aaebd0d79dc9336"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-desktop/src/behavior/scenario_lifecycle/render.rs",
|
||||
"bytes": 16028,
|
||||
"sha256": "d31fc2443268a28adb525302b70dff640ef04ea34de0bcdd1a8bdb4c70be9435"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
# Frozen Scenario resolver correctness review
|
||||
|
||||
Scope: manifest-verified snapshot .local/scenario-resolver-review-snapshot-20260913, based on typed import695685a. Read all three production files; verified every byte count and SHA256 against manifest.json. No source changes, compiler, test execution or GUI. References below are snapshot lines unless explicitly marked unchanged peer projector. Parent reviews tests separately.
|
||||
|
||||
## P1: successful resolution can suppress sibling lifetime geometry
|
||||
|
||||
scenario_layout.rs:315–357 checks each operand scope only against its enclosing frame/caller. The unchanged scenario_lifecycle.rs Projection::sequence also requires sibling scopes to be monotone in declared branch order: it sets cursor to each previous branch.end before checking the next branch.start. The resolver does not enforce this prerequisite and does not inspect the resulting new MissingGeometry uncertainty.
|
||||
|
||||
Concrete admitted fixture: one root ALT at148, two operands each containing one asynchronous message, with two ordinary work lifelines and an accepted exchange. Actual traversal yields message rows293/475, operand scopes251..343 and433..525, frame148..567. Give only the first message send_y500. Its footprint ends500 and retains50px scope bottom padding, so first scope becomes251..550. Second remains433..525. Both pass enclosing checks; the actual projector then skips operand2 because433<550, marks MissingGeometry and makes the post-fragment lane lifetime Unresolved. There are no Calls, so the later activation guards cannot reject this. resolve returns Ok with missing lifetime geometry. This is a production defect, independent of the unsettled policy for asynchronous arrow crossings.
|
||||
|
||||
Smallest correction: validate effective sibling scope intervals in declared branch traversal order, returning a precise unsupported-geometry result when the unchanged projector cannot enter the next operand. Retain feasible nonoverlap and no-op fixtures; do not silently move the second operand or change semantic order. Author has been notified and plans this narrow guard.
|
||||
|
||||
## P1: expanded operand can erase a following lifecycle marker
|
||||
|
||||
The same missed cursor invariant extends beyond sibling checks. Start with the previous ALT and then a root Delete at631 for an existing receiver. Move only the second operand message475→700. Its scope grows433..750; the frame grows to792 using the existing92px descendant padding. The following root Delete remains631. The enclosing-operand checks still pass. The Delete is inside the root85..height−20 interval (scenario_layout.rs:359–387), and the direct lifecycle inversion check248–281 has no preceding direct lifecycle event to compare.
|
||||
|
||||
After visiting operand2, the actual projector cursor is750. Delete631 fails its y>=cursor check, producing MissingGeometry without a Death marker. No Call exists to trip the baseline-activation comparisons635–697. The resolver therefore reports success while the previously visible valid X disappears. Expected behavior for the unchanged projector is explicit unsupported geometry, or a separately justified projector change; retaining an unaligned Delete arrow without its marker is not successful realization.
|
||||
|
||||
Smallest correction: enforce the real per-scope event/fragment cursor precondition, including the position after all branches before subsequent relevant lifecycle events/fragments. Merely rejecting sibling overlaps leaves this case open. A feasible control is second message520 with Delete631, preserving the exact Death marker and all lifetimes. No blanket ban on every asynchronous crossing is implied. Parent and author were notified.
|
||||
|
||||
## P2: negative-lane extent is interpreted twice
|
||||
|
||||
Independently confirmed the parent's numeric finding. behavior.rs:1335–1345 sets scene.size.width to right−layout_left when authored lanes exist; scenario_layout.rs:724 interprets that width as an absolute right by seeding Rectangle(ORIGIN,scene.size). Lanes−500/+900 yield layout_left−660, right1140, width1800. The initial bounds instead extend to right1800, adding660 units of empty right margin before unioning real geometry. Large signed coordinates can materially worsen Fit scale and scrolling even though all geometry is retained.
|
||||
|
||||
Carry the base rectangle's horizontal origin alongside its width, or otherwise retain an unambiguous absolute-right contract. Seed structural_bounds with that rectangle. Preserve actual authored coordinates and union any extra activation/loop/marker extents; do not translate source positions to conceal this. This is an extent/Fit correctness issue, not a newly missing object.
|
||||
|
||||
## Confirmed boundaries and remaining limits
|
||||
|
||||
- Representation is explicit Derived(owner) or Saved(diagram); each resolution reads exactly that saved layout and returns the Scene with its representation. There is no global mutable layout state. Project is immutably borrowed; source/history writes are absent. Document/revision/view-visit freshness remains caller responsibility as documented.
|
||||
- Sparse messages without a drawn line remain latent and are not normalized; direct addressing is checked and expanded paths explicitly rejected. Self identity is based on sender/receiver IDs in the adapter and lifecycle marker, rather than coincident x. Parent independently found the header-center f32 roundtrip bug at558–567/702–711 and has asked the author to preserve the exact builder lane map; this report does not duplicate ownership of that fix.
|
||||
- Rendering order is complete rows/scopes/height → lifetime → lifecycle endpoint alignment → typed activation layout. Whole self-lifecycle send overrides explicitly convert to receipt(+24). Production typed activation representation is retained, not reconstructed from obsolete boxes.
|
||||
- Current baseline-segment checks are narrower than proof of the complete activation/lifetime vector. Final tests must assert nonempty full expected vectors where applicable, plus exact markers/uncertainties for the two regressions above, not only call-start existence or equal vector lengths.
|
||||
- Structural bounds include boxes, paths, labels, typed bars, open-call cues and lifecycle geometry; this is structural coverage, not guaranteed text ink/stroke containment or future viewport wiring.
|
||||
- The source diagnostics for arbitrary event crossing are not a settled gesture policy. No runtime or semantic endorsement of arbitrary nested Call/Reply crossings follows from this review. The active saved-view UI, preview transactions and native workflow remain outside this production slice.
|
||||
|
||||
No additional verified blocker was established in this bounded pass. Findings describe the original immutable snapshot; fixes require their own diff/readback and parent-owned gates.
|
||||
|
||||
## P1 follow-up: public send precision can cause a false geometry rejection
|
||||
|
||||
Independently verified parent numeric finding using explicit float32 rounding at every arithmetic operation, without compiling. scenario_layout.rs:238–243 computes a baseline-relative delta and adds it back to baseline endpoints. For seed148, authored .1f32 (.10000000149011612) becomes Plan.send .100006103515625; authored1e−6f32 becomes0. Ordinary adapter rows retain the exact authored value, so those cases produce plan/Scene disagreement even when the ordinary painted send is correct.
|
||||
|
||||
A consequential feasible reproducer is a root async/Call/Reply at automatic148/240/332, then saved async90 and Call100.1f32. Semantic order remains90<100.1<332 and all endpoints remain within the root scope. For the ordinary Call, Plan.receive becomes100.100006103515625 while its actual Scene row and typed activation start are100.0999984741211. The final exact start comparison at672–695 then rejects this valid spacing as TypedActivationOutsideLifetime. This is not merely subpixel paint drift.
|
||||
|
||||
Self Create/Delete have an additional rendered-endpoint loss: authored send64.next_down() is63.999996185302734; the receipt conversion at548–552 rounds send+24 to88, then render.rs:153–158 reconstructs send88−24 as64. The receipt is feasible (root start85); the returned line does not realize the exact authored send.
|
||||
|
||||
Smallest correction: retain explicit authored send in the complete message pair, derive receive once according to message identity/type, and apply the resolved pair to actual line endpoints after lifecycle alignment but before typed activation derivation. Do not recover public send by subtracting a rounded receive or adding a cancelled delta. Preserve both top self-loop points at send and both lower points at receive; retain lifecycle markers at the receipt.
|
||||
|
||||
Required exact-bit tests: (1) .1 and1e−6 ordinary messages assert stored send, Plan.send and actual first endpoint agree; (2) async90/Call100.1/Reply332 resolves successfully and has an explicitly nonempty exact typed activation with start100.1; (3) self Create and self Delete separately at64.next_down() assert first two y values equal stored send, final two and marker y equal correctly rounded send+24; (4) source/Document and sparse absence remain unchanged. These are source/math findings, not executed test results. Parent and author have been notified; revised snapshot review remains pending.
|
||||
|
||||
## Revised immutable v2 review — original blockers addressed at source level
|
||||
|
||||
Verified all four v2 byte counts and SHA256 values against .local/scenario-resolver-review-snapshot-20260913-v2/manifest.json. The22 #[test] definitions exactly match its ordered test-name manifest. Reviewed the correction deltas and new or strengthened numeric regressions. No remaining verified blocker in this bounded correction pass. Parent-owned compiler and runtime gates remain pending.
|
||||
|
||||
- Exact lane identity: Scene.render_lanes retains the builder map (behavior.rs:1340); resolver coincidence checks/output use it directly (scenario_layout.rs:605,775). Tests927–956 independently assert .1 bits and distinct0/1e−6 line endpoints, so header-center rounding can no longer create this false coincidence. Assigned-only Scene.lanes remains separate.
|
||||
- Lifetime geometry losses: scenario_layout.rs:728–743 compares actual projector MissingGeometry (lifeline,scope) pairs against the baseline. render.rs:173–182 selects only that reason, retaining existing semantic uncertainties. This catches both prior successful-loss cases without changing projector traversal or adopting a blanket event-crossing policy. Tests1026–1073 establish baseline numeric rows, positively exercise missing geometry, require the named rejection, and retain feasible520 with the exact Delete631 marker. No empty-set success or expected-panic swallowing was found.
|
||||
- Bounds origin: Scene.layout_origin carries layout_left, and structural_bounds seeds Rectangle(layout_origin,scene.size) at787–789. Tests check the independently expected−660..1140 and−1000160..1000240 envelopes while also requiring nonempty activation geometry containment. This resolves the duplicate negative-origin margin.
|
||||
- Public send bits: resolver631–648 explicitly replaces pairs with exact authored send and a single derived receive. behavior.rs:1398–1402 applies these y endpoints after lifecycle x/marker alignment and before typed activation derivation. align_message_anchors187–202 sets both self-loop send points and both receive points. Tests1076–1134 cover decimal/tiny ordinary values and feasible selfCreate64.next_down(); tests1138–1221 require success for async90/Call100.1/Reply332, exact Plan/Line/activation-start bits, one activation and exact Reply finish. The former valid-layout rejection is addressed.
|
||||
|
||||
Test scope limitations: the new precision regression specifically exercises selfCreate; selfDelete uses the same production branch but has no separate next_down-bit test in this22 set. Existing integer self-lifecycle test covers both kinds. The feasible ALT control asserts marker and absence of MissingGeometry, not an independently enumerated full lifetime vector; other tests compare complete nonempty scene activation/lifetime state. Stored fixture helpers run actual validation and Document/source round trips when executed; that admission has not yet run here. No claim about native paint, new gesture policy, saved-view UI freshness or full desktop compatibility follows.
|
||||
@@ -1,6 +1,6 @@
|
||||
# Scenario renderer bridge: bounded implementation and evaluators
|
||||
|
||||
Status: proposed internal implementation slice, 2026-09-13. Reviewed the [frozen layout candidate aed07176](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/aed07176ab453b981d5f9c76e504c531f6dd7f42/harness/scenario-layout-contract-candidate.md) and the complete-anchor experiment. Source symbols below refer to that pinned candidate unless stated otherwise. This note adds no renderer implementation or runtime evidence. The existing [Windows experiment receipt](experiments/behavior-anchor-plan-gates-20260913/README.md) records the separate 23-check result.
|
||||
Status: internal implementation candidate, 2026-09-13. Reviewed the [frozen layout candidate aed07176](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/aed07176ab453b981d5f9c76e504c531f6dd7f42/harness/scenario-layout-contract-candidate.md) and the complete-anchor experiment. Source symbols below refer to that pinned candidate unless stated otherwise. The initial proposal is retained below; the source-only implementation checkpoint and bounded review now follow. The existing [Windows experiment receipt](experiments/behavior-anchor-plan-gates-20260913/README.md) records the separate 23-check result.
|
||||
|
||||
## Contract versus proposal
|
||||
|
||||
@@ -39,3 +39,17 @@ Retain the experiment's independent numeric expectations and full Project/Source
|
||||
The existing 23-check experiment comprises thirteen complete-anchor checks plus ten earlier floor/lifetime checks. See the [complete-plan module](experiments/behavior-anchor-plan-20260912/mod.rs), [numeric tests](experiments/behavior-anchor-plan-20260912/tests.rs), [prepared rationale](experiments/behavior-anchor-plan-20260912/README.md), and adjacent [floor/lifetime experiment](experiments/behavior-layout-scene-20260912/README.md). Its frame padding/nonshrinking bounds are experimental preferences, not adopted constraints. It does not establish horizontal layout, saved-view camera behavior, measured operation visibility or the #21 typed activation union. The [execution receipt](experiments/behavior-anchor-plan-gates-20260913/README.md) reports 23 passed, zero failed/ignored against 4b782a71, with unchanged 574-input manifests. The prepared README's pending-execution wording is historical; that result does not establish this proposed resolver or the later typed-activation union.
|
||||
|
||||
No schema extension is required by this resolver proposal. No gesture/navigation rewrite, semantic reordering policy, expanded-reference editing or native acceptance is claimed by the pure-resolver checkpoint. The integration owner coordinates implementation and publication; #21 activation ownership remains an explicit dependency. Pinned peer source is a compatibility input, not evidence that this bridge has been integrated or accepted.
|
||||
|
||||
## Source implementation checkpoint
|
||||
|
||||
Candidate [68a81c3](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/commit/68a81c3f50d846e34cae96090979543878514c1e) contains the pure resolver and actual Scene adapter with 22 prepared tests. It retains the resolved Scene with explicit representation/owner identity, exact render-lane centers, origin-bearing bounds and latent message addresses. It does not wire saved views into workspace, camera, usage or gestures. The source is published and all 13 changed contribution files match remote blobs and sizes; no combined compile or execution is claimed at this checkpoint.
|
||||
|
||||
Typed activation import 695685a selectively consumes peer aadc6396, retaining the current label helper and Lane header, and excluding unrelated Remove keyboard wiring. Test correction 20acd32 replaces the old width-eight BoxItem oracle with an exact nonempty typed activation vector and closes the empty-vector loophole in the offset probe. HTTP test bfb4b39 independently constructs expected Scenario layouts through existing transaction, no-op, stale revision, source and restart paths; it is unexecuted at this checkpoint.
|
||||
|
||||
The [portable source review](experiments/scenario-resolver-review-20260913/README.md) preserves the rejected snapshot and precise corrections. Returning success after new actual lifetime MissingGeometry is forbidden: final versus automatic lifetime diagnostics catch lost sibling geometry and a dropped following Delete marker without duplicating projector traversal rules. Exact authored send/lane values are primary inputs; reconstructing them from header rectangles or subtract/add deltas loses float bits and can falsely reject a valid Call. Preserve endpoint pairs after lifecycle alignment and before typed activation derivation. A rectangle must retain its origin when width already includes the negative extent. These conclusions do not adopt a new chronology, gesture or codec policy.
|
||||
|
||||
## Next display policy
|
||||
|
||||
For the next workspace increment, use one complete automatic fallback only when a structurally accepted saved representation has a hard geometry failure and its automatic resolution succeeds. Keep requested Saved identity distinct from the Derived geometry provenance, preserve all authored values and show a persistent accessible explanation. Invalid/stale identity or failure of both projections yields an unavailable canvas with inspection/source actions, never an arbitrary owner or unchecked partial renderer. Successful authored geometry with source-order/frame-overlap diagnostics stays authored and receives explanatory notices. Latent messages and semantic completeness warnings remain distinct.
|
||||
|
||||
One presentation adapter must supply the actual Scene, origin-bearing bounds, identity and status to paint, hit-testing, usage and reveal. App owns document/revision/view-visit freshness; existing Scenario state owns presentation. Inspection, navigation and guarded semantic/source repair remain available. Coordinate-dependent input is refused in fallback at both controls and event admission, including a held drag released after repair. An explicit repair must never save fallback coordinates implicitly. This is a selected reversible display policy for the next implementation; native feedback, focus, stale input and persistence acceptance remain required.
|
||||
|
||||
Reference in New Issue
Block a user