Skip to main content

hapi_rs/
asset.rs

1//! For loading digital assets and reading their parameters.
2//! [Documentation](https://www.sidefx.com/docs/hengine/_h_a_p_i__assets.html)
3use crate::ffi::raw as ffi;
4use crate::ffi::raw::{ChoiceListType, ParmType};
5use crate::node::ManagerType;
6use crate::{
7    HapiError, errors::Result, ffi::ParmChoiceInfo, ffi::ParmInfo, node::HoudiniNode,
8    session::Session,
9};
10use log::debug;
11use std::ffi::{CStr, CString};
12use std::path::PathBuf;
13
14struct AssetParmValues {
15    int: Vec<i32>,
16    float: Vec<f32>,
17    string: Vec<String>,
18    menus: Vec<ParmChoiceInfo>,
19}
20
21/// Holds asset parameters data.
22/// Call `into_iter` to get an iterator over each parameter
23pub struct AssetParameters {
24    asset_name: CString,
25    library: AssetLibrary,
26    infos: Vec<ParmInfo>,
27    values: AssetParmValues,
28}
29
30impl<'a> IntoIterator for &'a AssetParameters {
31    type Item = AssetParm<'a>;
32    type IntoIter = AssetParmIter<'a>;
33
34    fn into_iter(self) -> Self::IntoIter {
35        AssetParmIter {
36            library_id: self.library.lib_id,
37            asset_name: &self.asset_name,
38            iter: self.infos.iter(),
39            values: &self.values,
40        }
41    }
42}
43impl<'a> AssetParameters {
44    #[must_use]
45    pub fn iter(&'a self) -> AssetParmIter<'a> {
46        <&Self as IntoIterator>::into_iter(self)
47    }
48}
49
50impl AssetParameters {
51    /// Find asset parameter by name
52    #[must_use]
53    pub fn find_parameter(&self, name: &str) -> Option<AssetParm<'_>> {
54        self.infos
55            .iter()
56            .find(|p| p.name().is_ok_and(|n| n == name))
57            .map(|info| AssetParm {
58                library_id: self.library.lib_id,
59                asset_name: &self.asset_name,
60                info,
61                values: &self.values,
62            })
63    }
64}
65
66/// Iterator over asset parameter default values
67pub struct AssetParmIter<'a> {
68    library_id: i32,
69    asset_name: &'a CStr,
70    iter: std::slice::Iter<'a, ParmInfo>,
71    values: &'a AssetParmValues,
72}
73
74impl<'a> Iterator for AssetParmIter<'a> {
75    type Item = AssetParm<'a>;
76
77    fn next(&mut self) -> Option<Self::Item> {
78        self.iter.next().map(|info| AssetParm {
79            library_id: self.library_id,
80            asset_name: self.asset_name,
81            info,
82            values: self.values,
83        })
84    }
85}
86
87/// Holds info and default value of a parameter
88pub struct AssetParm<'a> {
89    library_id: i32,
90    asset_name: &'a CStr,
91    info: &'a ParmInfo,
92    values: &'a AssetParmValues,
93}
94
95impl std::ops::Deref for AssetParm<'_> {
96    type Target = ParmInfo;
97
98    fn deref(&self) -> &Self::Target {
99        self.info
100    }
101}
102
103/// Parameter default value
104#[derive(Debug)]
105pub enum ParmValue<'a> {
106    Int(&'a [i32]),
107    Float(&'a [f32]),
108    String(&'a [String]),
109    Toggle(bool),
110    NoDefault,
111}
112
113impl<'a> AssetParm<'a> {
114    /// Get parameter default value
115    #[must_use]
116    pub fn default_value(&self) -> ParmValue<'a> {
117        use ParmType::{
118            Button, Color, Float, Int, Node, PathFile, PathFileDir, PathFileGeo, PathFileImage,
119            String, Toggle,
120        };
121        let size = self.info.size() as usize;
122        match self.info.parm_type() {
123            Int | Button => {
124                let start = self.info.int_values_index() as usize;
125                ParmValue::Int(&self.values.int[start..start + size])
126            }
127            Toggle => {
128                let start = self.info.int_values_index() as usize;
129                ParmValue::Toggle(self.values.int[start] == 1)
130            }
131            Float | Color => {
132                let start = self.info.float_values_index() as usize;
133                ParmValue::Float(&self.values.float[start..start + size])
134            }
135            String | PathFileGeo | PathFile | PathFileImage | PathFileDir | Node => {
136                let start = self.info.string_values_index() as usize;
137                ParmValue::String(&self.values.string[start..start + size])
138            }
139            _ => ParmValue::NoDefault,
140        }
141    }
142
143    /// Returns menu parameter items.
144    /// Note, dynamic(script) menus should be queried directly from a node.
145    #[must_use]
146    pub fn menu_items(&self) -> Option<&[ParmChoiceInfo]> {
147        if let ChoiceListType::None = self.choice_list_type() {
148            return None;
149        }
150        let count = self.info.choice_count() as usize;
151        let start = self.info.choice_index() as usize;
152        Some(&self.values.menus[start..start + count])
153    }
154
155    /// Get asset parameter tag name and value
156    pub fn get_tag(&self, tag_index: i32) -> Result<(String, String)> {
157        let tag_name = crate::ffi::get_asset_definition_parm_tag_name(
158            &self.info.1,
159            self.library_id,
160            self.asset_name,
161            self.info.id(),
162            tag_index,
163        )?;
164        // SAFETY: string bytes obtained from FFI are null terminated
165        let tag_c_str = CString::new(tag_name.clone())?;
166        let tag_value = crate::ffi::get_asset_definition_parm_tag_value(
167            &self.info.1,
168            self.library_id,
169            self.asset_name,
170            self.info.id(),
171            &tag_c_str,
172        )?;
173        Ok((tag_name, tag_value))
174    }
175}
176
177/// A handle to a loaded HDA file
178#[derive(Debug, Clone)]
179pub struct AssetLibrary {
180    pub(crate) lib_id: ffi::HAPI_AssetLibraryId,
181    pub(crate) session: Session,
182    pub file: Option<PathBuf>,
183}
184
185impl AssetLibrary {
186    /// Load an asset from file
187    pub fn from_file(session: Session, file: impl AsRef<std::path::Path>) -> Result<AssetLibrary> {
188        let file = file.as_ref().to_path_buf();
189        debug!("Loading library file: {}", file.display());
190        debug_assert!(session.is_valid());
191        let cs = CString::new(file.as_os_str().to_string_lossy().to_string())?;
192        let lib_id = crate::ffi::load_library_from_file(&cs, &session, true)?;
193        Ok(AssetLibrary {
194            lib_id,
195            session,
196            file: Some(file),
197        })
198    }
199
200    /// Load asset library from memory
201    pub fn from_memory(session: Session, data: &[u8]) -> Result<AssetLibrary> {
202        debug!("Loading library from memory");
203        debug_assert!(session.is_valid());
204        let data: &[i8] = unsafe { &*(std::ptr::from_ref::<[u8]>(data) as *const [i8]) };
205        let lib_id = crate::ffi::load_library_from_memory(&session, data, true)?;
206        Ok(AssetLibrary {
207            lib_id,
208            session,
209            file: None,
210        })
211    }
212
213    /// Get number of assets defined in the current library
214    pub fn get_asset_count(&self) -> Result<i32> {
215        debug_assert!(self.session.is_valid());
216        crate::ffi::get_asset_count(self.lib_id, &self.session)
217    }
218
219    /// Get asset names this library contains
220    pub fn get_asset_names(&self) -> Result<Vec<String>> {
221        debug_assert!(self.session.is_valid());
222        debug!(
223            "Retrieving asset names from: {:?}",
224            self.file
225                .as_deref()
226                .map_or("<memory bytes>".into(), |p| p.to_string_lossy())
227        );
228        let num_assets = self.get_asset_count()?;
229        crate::ffi::get_asset_names(self.lib_id, num_assets, &self.session)
230            .map(|a| a.into_iter().collect())
231    }
232
233    /// Returns the name of first asset in the library
234    pub fn get_first_name(&self) -> Result<Option<String>> {
235        debug_assert!(self.session.is_valid());
236        self.get_asset_names().map(|names| names.first().cloned())
237    }
238
239    /// Create a node for an asset. This function is a convenient form of [`Session::create_node`]
240    /// in a way that it makes sure that a correct parent network node is also created for
241    /// assets other than Object level such as Cop, Top, etc.
242    pub fn create_asset_for_node<T: AsRef<str>>(
243        &self,
244        name: T,
245        label: Option<&T>,
246    ) -> Result<HoudiniNode> {
247        // Most common HDAs are Object/asset and Sop/asset which HAPI can create directly in /obj,
248        // but for some assets type like Cop, Top a manager node must be created first
249        debug!("Trying to create a node for operator: {}", name.as_ref());
250        let Some((context, operator)) = name.as_ref().split_once('/') else {
251            return Err(HapiError::Internal(format!(
252                "Incomplete node name: {}. Name must be fully qualified",
253                name.as_ref()
254            )));
255        };
256        // Operators with namespace better be handle Houdini directly
257        if context.contains("::") {
258            return self.session.create_node(name.as_ref());
259        }
260        // There's no root network manager for Sop node types.
261        let (manager, subnet) = if context == "Sop" {
262            (None, None)
263        } else {
264            let manager_type = context.parse::<ManagerType>()?;
265            let subnet = match manager_type {
266                ManagerType::Cop => Some("img"),
267                ManagerType::Chop => Some("ch"),
268                ManagerType::Top => Some("topnet"),
269                _ => None,
270            };
271            (Some(manager_type), subnet)
272        };
273
274        // If subnet is Some, we get the manager node for this context and use it as parent.
275        let parent = match subnet {
276            Some(subnet) => {
277                let manager = manager.ok_or_else(|| {
278                    HapiError::Internal(format!(
279                        "Missing manager node type for context \"{context}\""
280                    ))
281                })?;
282                let parent = self.session.get_manager_node(manager)?;
283                Some(
284                    self.session
285                        .create_node_with(subnet, parent.handle, None, false)?
286                        .handle,
287                )
288            }
289            None => None,
290        };
291        // If passing a parent, operator name must be stripped of the context name
292        let full_name = if parent.is_some() {
293            operator
294        } else {
295            name.as_ref()
296        };
297        self.session.create_node_with(
298            full_name,
299            parent,
300            label.as_ref().map(std::convert::AsRef::as_ref),
301            false,
302        )
303    }
304
305    /// Try to create the first found asset in the library.
306    /// This is a convenience function for:
307    /// ```
308    /// use hapi_rs::session::new_in_process_session;
309    /// use hapi_rs::session::SessionOptions;
310    /// let session = new_in_process_session(Some(SessionOptions::default())).unwrap();
311    /// let lib = session.load_asset_file("../otls/hapi_geo.hda").unwrap();
312    /// let names = lib.get_asset_names().unwrap();
313    /// session.create_node(&names[0]).unwrap();
314    /// ```
315    /// Except that it also handles non Object level assets, e.g. Cop network HDA.
316    pub fn try_create_first(&self) -> Result<HoudiniNode> {
317        debug_assert!(self.session.is_valid());
318        let name = self
319            .get_first_name()?
320            .ok_or_else(|| HapiError::Internal("Library file is empty".to_string()))?;
321        self.create_asset_for_node(name, None)
322    }
323
324    /// Returns a struct holding the asset parameter information and values
325    pub fn get_asset_parms(&self, asset: impl AsRef<str>) -> Result<AssetParameters> {
326        debug_assert!(self.session.is_valid());
327        let _lock = self.session.lock();
328        debug!("Reading asset parameter list of {}", asset.as_ref());
329        let asset_name = CString::new(asset.as_ref())?;
330        let parm_count =
331            crate::ffi::get_asset_def_parm_count(self.lib_id, &asset_name, &self.session)?;
332        let infos = crate::ffi::get_asset_def_parm_info(
333            self.lib_id,
334            &asset_name,
335            parm_count.total,
336            &self.session,
337        )?
338        .into_iter()
339        .map(|info| ParmInfo::new(info, self.session.clone(), None));
340        let values = crate::ffi::get_asset_def_parm_values(
341            self.lib_id,
342            &asset_name,
343            &self.session,
344            &parm_count,
345        )?;
346        let menus = values
347            .3
348            .into_iter()
349            .map(|info| ParmChoiceInfo(info, self.session.clone()));
350        let values = AssetParmValues {
351            int: values.0,
352            float: values.1,
353            string: values.2.into_iter().collect(),
354            menus: menus.collect(),
355        };
356        Ok(AssetParameters {
357            asset_name,
358            library: self.clone(),
359            infos: infos.collect(),
360            values,
361        })
362    }
363}