//! 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, }, Unavailable { requested_issues: Vec, automatic_issues: Vec, }, } #[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, status: PresentationStatus, geometry: Option, // Saved metadata must survive a complete automatic fallback. latent_messages: Vec, } impl ScenarioPresentation { pub(crate) fn requested(&self) -> Representation { self.requested } pub(crate) fn owner(&self) -> Option { self.owner } pub(crate) fn status(&self) -> &PresentationStatus { &self.status } pub(crate) fn geometry_representation(&self) -> Option { self.geometry .as_ref() .map(|geometry| geometry.representation) } pub(crate) fn bounds(&self) -> Option { 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 { 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 { 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 { 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() }