235 lines
8.5 KiB
Rust
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()
|
|
}
|