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