Skip to main content

kcl_lib/named_views/
resolver.rs

1use indexmap::IndexMap;
2use kcl_api::artifact::NamedViewArtifact;
3use uuid::Uuid;
4
5use super::Artifact;
6use super::ArtifactCameraView;
7use super::ArtifactGraph;
8use super::ArtifactId;
9use super::ModuleId;
10use super::ModulePath;
11use super::Operation;
12use super::visibility;
13
14/// A declared view's identity and source-file metadata.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct NamedViewInfo {
17    pub artifact_id: ArtifactId,
18    pub name: String,
19    pub module_id: ModuleId,
20    pub module_path: ModulePath,
21}
22
23/// Selects a view within one execution's output.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum ViewTarget {
26    /// The visibility produced by the program's hide operations.
27    Default,
28    /// An exact display name within its declaring module.
29    Named { name: String, module_id: ModuleId },
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
33pub enum ViewResolutionError {
34    #[error("No module path is available for named-view module {module_id}")]
35    MissingModulePath { module_id: ModuleId },
36    #[error("No named view matches the requested selection")]
37    NotFound,
38    /// The supplied graph contains multiple matches for the complete target.
39    #[error("The requested selection matches multiple named views")]
40    Ambiguous { candidates: Vec<NamedViewInfo> },
41}
42
43/// The data a consumer of Named Views needs to apply a selected view.
44#[derive(Debug, Clone, PartialEq)]
45pub enum ResolvedView {
46    /// Default is populated programmatically and for a valid execution.
47    /// There is no explicit camera -- use what the camera settings are
48    Default { visibility: IndexMap<Uuid, bool> },
49    /// A declared view with its authored camera settings.
50    Named {
51        visibility: IndexMap<Uuid, bool>,
52        camera_intent: Box<ArtifactCameraView>,
53    },
54}
55
56impl ResolvedView {
57    /// Complete decisions keyed by engine object UUID. A value of `true` hides the object.
58    pub fn visibility(&self) -> &IndexMap<Uuid, bool> {
59        match self {
60            Self::Default { visibility } | Self::Named { visibility, .. } => visibility,
61        }
62    }
63}
64
65/// Resolves named views from explicit execution data without engine calls.
66#[derive(Debug)]
67pub struct NamedViewResolver<'a> {
68    artifact_graph: &'a ArtifactGraph,
69    operations: &'a IndexMap<ModuleId, Vec<Operation>>,
70    named_views: Vec<(&'a NamedViewArtifact, NamedViewInfo)>,
71}
72
73impl<'a> NamedViewResolver<'a> {
74    /// Validates module paths before constructing complete named-view metadata.
75    pub fn new(
76        artifact_graph: &'a ArtifactGraph,
77        operations: &'a IndexMap<ModuleId, Vec<Operation>>,
78        filenames: &IndexMap<ModuleId, ModulePath>,
79    ) -> Result<Self, ViewResolutionError> {
80        let mut named_views = Vec::new();
81        for artifact in artifact_graph.values() {
82            let Artifact::NamedView(view) = artifact else {
83                continue;
84            };
85            let module_id = view.declaring_module_id;
86            let module_path = filenames
87                .get(&module_id)
88                .ok_or(ViewResolutionError::MissingModulePath { module_id })?;
89            named_views.push((
90                view,
91                NamedViewInfo {
92                    artifact_id: view.id,
93                    name: view.name.clone(),
94                    module_id,
95                    module_path: module_path.clone(),
96                },
97            ));
98        }
99        Ok(Self {
100            artifact_graph,
101            operations,
102            named_views,
103        })
104    }
105
106    /// Returns declared views in graph order.
107    ///
108    /// - Views from imported modules are included.
109    /// - Repeated display names remain separate entries.
110    /// - The computed default is excluded because it is not declared.
111    pub fn list(&self) -> Vec<NamedViewInfo> {
112        self.named_views.iter().map(|(_, info)| info.clone()).collect()
113    }
114
115    pub fn resolve(&self, target: &ViewTarget) -> Result<ResolvedView, ViewResolutionError> {
116        match target {
117            ViewTarget::Default => Ok(ResolvedView::Default {
118                visibility: visibility::visibility_for_default(self.artifact_graph, self.operations),
119            }),
120            ViewTarget::Named { name, module_id } => {
121                let candidates: Vec<_> = self
122                    .named_views
123                    .iter()
124                    .filter(|(_, info)| info.name == *name && info.module_id == *module_id)
125                    .collect();
126
127                match candidates.as_slice() {
128                    [] => Err(ViewResolutionError::NotFound),
129                    [(view, _)] => Ok(ResolvedView::Named {
130                        visibility: visibility::visibility_for_view(self.artifact_graph, view),
131                        camera_intent: Box::new(view.camera.clone()),
132                    }),
133                    _ => Err(ViewResolutionError::Ambiguous {
134                        candidates: candidates.iter().map(|(_, info)| info.clone()).collect(),
135                    }),
136                }
137            }
138        }
139    }
140}