Files
ARCADIA-Studio/crates/oa-desktop/src/behavior/scenario_layout/presentation.rs
T

235 lines
8.5 KiB
Rust

//! Display policy for one requested Scenario representation, without authoring.
//! The resolver remains the sole renderer. Freshness and editor admission belong
//! to the caller; a capability here never bypasses those guards.
use super::{Diagnostic, GeometryIssue, Representation, ResolvedScenarioPlan, resolve};
use crate::behavior::Scene;
use iced::Rectangle;
use oa_core::{DiagramLayout, Id, Kind, OccurrenceBody, Project, SharedBody};
use std::collections::BTreeSet;
#[cfg(test)]
mod tests;
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum PresentationStatus {
Ready,
Fallback {
requested_issues: Vec<GeometryIssue>,
},
Unavailable {
requested_issues: Vec<GeometryIssue>,
automatic_issues: Vec<GeometryIssue>,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct PresentationCapabilities {
/// Actual geometry exists for inspection, Fit and geometry-based navigation.
pub(crate) geometry_navigation: bool,
/// Still subject to document, gesture, editor and view-visit admission.
pub(crate) coordinate_input: bool,
}
pub(crate) struct ScenarioPresentation {
requested: Representation,
owner: Option<Id>,
status: PresentationStatus,
geometry: Option<ResolvedScenarioPlan>,
// Saved metadata must survive a complete automatic fallback.
latent_messages: Vec<Id>,
}
impl ScenarioPresentation {
pub(crate) fn requested(&self) -> Representation {
self.requested
}
pub(crate) fn owner(&self) -> Option<Id> {
self.owner
}
pub(crate) fn status(&self) -> &PresentationStatus {
&self.status
}
pub(crate) fn geometry_representation(&self) -> Option<Representation> {
self.geometry
.as_ref()
.map(|geometry| geometry.representation)
}
pub(crate) fn bounds(&self) -> Option<Rectangle> {
self.geometry.as_ref().map(|geometry| geometry.bounds)
}
/// Diagnostics describe the actual displayed geometry. Requested hard failures
/// are retained separately in status(), never presented as automatic warnings.
pub(crate) fn diagnostics(&self) -> &[Diagnostic] {
self.geometry
.as_ref()
.map_or(&[], |geometry| &geometry.plan.diagnostics)
}
pub(crate) fn latent_messages(&self) -> &[Id] {
&self.latent_messages
}
pub(crate) fn capabilities(&self) -> PresentationCapabilities {
PresentationCapabilities {
geometry_navigation: self.geometry.is_some(),
coordinate_input: matches!(self.status, PresentationStatus::Ready),
}
}
pub(in crate::behavior) fn scene(&self) -> Option<&Scene> {
self.geometry.as_ref().map(|geometry| &geometry.scene)
}
pub(in crate::behavior) fn into_scene(self) -> Option<Scene> {
self.geometry.map(|geometry| geometry.scene)
}
}
/// Resolve a structurally accepted Project for passive display. This is not a
/// second model validator: identity and resolver-reported structural failures
/// cannot be converted into a more permissive automatic representation.
pub(crate) fn present(
project: &Project,
requested: Representation,
selected: Id,
) -> ScenarioPresentation {
let owner = checked_owner(project, requested);
let Some(owner_id) = owner else {
let id = match requested {
Representation::Derived { owner } => owner,
Representation::Saved { diagram } => diagram,
};
return ScenarioPresentation {
requested,
owner: None,
geometry: None,
latent_messages: vec![],
status: PresentationStatus::Unavailable {
requested_issues: vec![GeometryIssue::InvalidRepresentation(id)],
automatic_issues: vec![],
},
};
};
match resolve(project, requested, selected) {
Ok(geometry) => ScenarioPresentation {
requested,
owner,
status: PresentationStatus::Ready,
latent_messages: geometry.latent_messages.clone(),
geometry: Some(geometry),
},
Err(requested_issues) => {
if matches!(requested, Representation::Derived { .. })
|| requested_issues.iter().any(structural_failure)
{
return ScenarioPresentation {
requested,
owner,
geometry: None,
latent_messages: vec![],
status: PresentationStatus::Unavailable {
requested_issues,
automatic_issues: vec![],
},
};
}
match resolve(
project,
Representation::Derived { owner: owner_id },
selected,
) {
Ok(geometry) => {
let latent_messages =
saved_latent_messages(project, requested, &geometry.scene);
ScenarioPresentation {
requested,
owner,
status: PresentationStatus::Fallback { requested_issues },
geometry: Some(geometry),
latent_messages,
}
}
Err(automatic_issues) => ScenarioPresentation {
requested,
owner,
geometry: None,
latent_messages: vec![],
status: PresentationStatus::Unavailable {
requested_issues,
automatic_issues,
},
},
}
}
}
}
pub(crate) fn checked_owner(project: &Project, requested: Representation) -> Option<Id> {
let owner = match requested {
Representation::Derived { owner } => owner,
Representation::Saved { diagram } => {
let view = project.diagrams.get(&diagram)?;
let owner = view.projection.scenario()?;
if view.id != diagram || project.elements.get(&owner)?.layer() != view.layer {
return None;
}
owner
}
};
project
.elements
.get(&owner)
.is_some_and(|element| element.kind() == Kind::Scenario)
.then_some(owner)
}
fn structural_failure(issue: &GeometryIssue) -> bool {
// These violate the existing identity/address/finite-coordinate admission
// contract. Geometry support failures below do not invalidate stored intent.
match issue {
GeometryIssue::InvalidRepresentation(_)
| GeometryIssue::UnknownAnchor(_)
| GeometryIssue::InvalidCoordinate(_)
| GeometryIssue::DuplicateCoordinate(_)
| GeometryIssue::ExpandedAddressNotSupported(_)
| GeometryIssue::NonFinite(_) => true,
GeometryIssue::LifecycleGeometryNotSupported { .. }
| GeometryIssue::CoincidentDistinctEndpoints(_)
| GeometryIssue::TypedActivationOutsideScope { .. }
| GeometryIssue::TypedActivationOutsideLifetime { .. }
| GeometryIssue::TypedActivationCrossingNotSupported { .. }
| GeometryIssue::CompoundMoveNotImplemented(_)
| GeometryIssue::FixedFrameHeaderWouldMove { .. }
| GeometryIssue::OperandScopeOutsideEnclosingInterval { .. }
| GeometryIssue::LifecycleAnchorOutsideRenderScope { .. }
| GeometryIssue::LifetimeInversionNotSupported { .. } => false,
}
}
fn saved_latent_messages(project: &Project, requested: Representation, scene: &Scene) -> Vec<Id> {
let Representation::Saved { diagram } = requested else {
return vec![];
};
let Some(DiagramLayout::Scenario(layout)) = project.diagrams[&diagram].layout.as_ref() else {
return vec![];
};
let lines: BTreeSet<_> = scene.lines.iter().map(|line| line.id).collect();
layout
.messages
.iter()
.filter_map(|message| {
let id = message.address.occurrence;
(message.address.references.is_empty()
&& scene.rows.contains_key(&id)
&& !lines.contains(&id)
&& matches!(
project
.elements
.get(&id)
.and_then(|element| element.shared()),
Some(SharedBody::Occurrence {
body: OccurrenceBody::Exchange { .. },
..
})
))
.then_some(id)
})
.collect()
}