nsi_toolbelt/
lib.rs

1#![cfg_attr(feature = "nightly", doc(cfg(feature = "toolbelt")))]
2//! Convenience methods for an ɴsɪ context.
3//!
4//! Names of methods that create nodes are nouns. Methods than modify
5//! the node graph afterwards use verbs.
6//!
7//! Where ergonomically advised, creation methods names carry postfixes
8//! that specify the type of node being created, such as `shader`.
9use nsi_core as nsi;
10use ultraviolet as uv;
11//use uv::{DVec3, DMat4};
12
13/// Generates a random handle if `handle` is `None` or falls through,
14/// otherwise.
15#[doc(hidden)]
16#[cfg(debug_assertions)]
17pub fn generate_or_use_handle(
18    handle: Option<&str>,
19    prefix: Option<&str>,
20) -> String {
21    match handle {
22        Some(handle) => handle.to_string(),
23        None => {
24            if let Some(prefix) = prefix {
25                String::from(prefix) + "_" + &petname::petname(3, "_")
26            } else {
27                petname::petname(3, "_")
28            }
29        }
30    }
31}
32
33#[doc(hidden)]
34#[cfg(not(debug_assertions))]
35pub fn generate_or_use_handle(
36    handle: Option<&str>,
37    _prefix: Option<&str>,
38) -> String {
39    match handle {
40        Some(handle) => handle.to_string(),
41        None => {
42            use rand::{
43                distributions::Alphanumeric, rngs::SmallRng, Rng, SeedableRng,
44            };
45            use std::iter;
46            let mut rng = SmallRng::from_entropy();
47
48            iter::repeat(())
49                .map(|()| rng.sample(Alphanumeric) as char)
50                .take(20)
51                .collect()
52        }
53    }
54}
55
56/// Append node `handle` to node `to`.
57///
58/// # Arguments
59/// * `to` – Node to connect to downstream.
60///
61/// * `slot` – Slot on target node to connect to. If [`None`], `"objects"` is
62///   used.
63///
64/// * `handle` – Handle of node to append.
65///
66/// Returns (`to`, `handle`).
67/// # Example
68/// ```
69/// # use nsi_core as nsi;
70/// # use nsi_toolbelt::{append, scaling};
71/// # let ctx = nsi::Context::new(None).unwrap();
72/// // Create a scaling transform node and append to the scene root.
73/// let scale = append(
74///     &ctx,
75///     ".root",
76///     // Use "objects" slot.
77///     None,
78///     // Append the node "tetrahedron", which we created earlier,
79///     // to the scale node.
80///     append(
81///         &ctx,
82///         &scaling(&ctx, None, &[10., 10., 10.]),
83///         // Use "objects" slot.
84///         None,
85///         "tetrahedron",
86///     )
87///     .0,
88/// );
89/// ```
90#[inline]
91pub fn append<'a, 'b, 'c>(
92    ctx: &'a nsi::Context,
93    to: &'b str,
94    slot: Option<&str>,
95    handle: &'c str,
96) -> (&'b str, &'c str)
97where
98    'a: 'b,
99    'a: 'c,
100{
101    ctx.connect(handle, None, to, slot.unwrap_or("objects"), None);
102
103    (to, handle)
104}
105
106/// Insert node `handle` in-between `to` and `from`.
107///
108/// # Arguments
109/// * `to` – Node to connect to downstream.
110///
111/// * `to_slot` – Slot on `to` node to connect to. If [`None`], `"objects"` is
112///   used.    .
113///
114/// * `handle` – Handle of node to insert.
115///
116/// * `handle_slot` – Slot on `handle` node to connect to. If [`None`],
117///   `"objects"` is used.
118///
119/// * `from` – Node to connect to upstream.
120///
121/// Returns (`to`, `handle`).
122/// # Example
123/// ```
124/// # use nsi_core as nsi;
125/// # use nsi_toolbelt::insert;
126/// # let ctx = nsi::Context::new(None).unwrap();
127/// // Insert the node "tetrahedron" between the ".root" and
128/// // "terahedron_attrib" nodes.
129/// insert(
130///     &ctx,
131///     ".root",
132///     None,
133///     "tetrahedron",
134///     Some("geometryattributes"),
135///     "terahedron_attrib",
136/// );
137/// ```
138#[inline]
139pub fn insert<'a, 'b, 'c>(
140    ctx: &'a nsi::Context,
141    to: &'b str,
142    to_slot: Option<&str>,
143    handle: &'c str,
144    handle_slot: Option<&str>,
145    from: &str,
146) -> (&'b str, &'c str)
147where
148    'a: 'b,
149    'a: 'c,
150{
151    append(ctx, handle, handle_slot, from);
152    append(ctx, to, to_slot, handle)
153}
154
155/// The same as [`create()`](nsi::context::Context::create()) but
156/// with support for automatic handle generation.
157///
158/// If `handle` is [`None`] a random handle is generated.
159///
160/// Returns `handle` for convenience.
161#[inline]
162pub fn node<'a>(
163    ctx: &nsi::Context<'a>,
164    handle: Option<&str>,
165    node_type: &str,
166    args: Option<&nsi::ArgSlice<'_, 'a>>,
167) -> String {
168    let handle = generate_or_use_handle(handle, Some(node_type));
169
170    ctx.create(handle.as_str(), node_type, None);
171
172    if let Some(args) = args {
173        ctx.set_attribute(handle.as_str(), args);
174    }
175
176    handle
177}
178
179/// Create a scaling transform node.
180///
181/// If `handle` is [`None`] a random handle is generated.
182///
183/// Returns `handle` for convenience.
184#[inline]
185pub fn scaling(
186    ctx: &nsi::Context,
187    handle: Option<&str>,
188    scale: &[f64; 3],
189) -> String {
190    let handle = generate_or_use_handle(handle, Some("scaling"));
191    ctx.create(handle.as_str(), nsi::node::TRANSFORM, None);
192
193    ctx.set_attribute(
194        handle.as_str(),
195        &[nsi::double_matrix!(
196            "transformationmatrix",
197            uv::DMat4::from_nonuniform_scale(uv::DVec3::from(scale)).as_array()
198        )],
199    );
200
201    handle
202}
203
204/// Create a translation transform node.
205///
206/// If `handle` is [`None`] a random handle is generated.
207///
208/// Returns `handle` for convenience.
209#[inline]
210pub fn translation(
211    ctx: &nsi::Context,
212    handle: Option<&str>,
213    translate: &[f64; 3],
214) -> String {
215    let handle = generate_or_use_handle(handle, Some("translation"));
216    ctx.create(handle.as_str(), nsi::node::TRANSFORM, None);
217
218    ctx.set_attribute(
219        handle.as_str(),
220        &[nsi::double_matrix!(
221            "transformationmatrix",
222            uv::DMat4::from_translation(uv::DVec3::from(translate)).as_array()
223        )],
224    );
225
226    handle
227}
228
229/// Create a translation transform node.
230///
231/// If `handle` is [`None`] a random handle is generated.
232///
233/// The `angle` is specified in degrees.
234///
235/// Returns `handle` for convenience.
236pub fn rotation(
237    ctx: &nsi::Context,
238    handle: Option<&str>,
239    angle: f64,
240    axis: &[f64; 3],
241) -> String {
242    let handle = generate_or_use_handle(handle, Some("rotation"));
243    ctx.create(handle.as_str(), nsi::node::TRANSFORM, None);
244
245    ctx.set_attribute(
246        handle.as_str(),
247        &[nsi::double_matrix!(
248            "transformationmatrix",
249            uv::DMat4::from_angle_plane(
250                (angle * core::f64::consts::TAU / 90.0) as _,
251                uv::DBivec3::from_normalized_axis(
252                    uv::DVec3::from(axis).normalized()
253                )
254            )
255            .transposed()
256            .as_array()
257        )],
258    );
259
260    handle
261}
262
263/// **Convenience method; not part of the official ɴsɪ API.**
264pub fn look_at_camera(
265    ctx: &nsi::Context,
266    handle: Option<&str>,
267    eye: &[f64; 3],
268    to: &[f64; 3],
269    up: &[f64; 3],
270) {
271    let handle = generate_or_use_handle(handle, Some("look_at"));
272    ctx.create(handle.as_str(), nsi::node::TRANSFORM, None);
273
274    ctx.set_attribute(
275        handle.as_str(),
276        &[nsi::double_matrix!(
277            "transformationmatrix",
278            uv::DMat4::look_at(
279                uv::DVec3::from(eye),
280                uv::DVec3::from(to),
281                uv::DVec3::from(up),
282            )
283            .inversed()
284            .as_array()
285        )],
286    );
287}
288
289/// Creates a transformation matrix that can be used to position
290/// a camera. Its view will contains the perspective-projected
291/// bounding box under the specified field-of-view and aspect ratio
292/// (*with*÷*height*).
293/// # Arguments
294/// * `direction` – The axis the camera should be looking along. Does *not* need
295///   to be normalized.
296/// * `up` – A direction to look
297/// * `bounding_box` – Axis-aligned bounding box in the form `[x_min, y_min,
298///   z_min, x_max, y_max, z_max]`.
299pub fn look_at_bounding_box_perspective_camera(
300    ctx: &nsi::Context,
301    handle: Option<&str>,
302    direction: &[f64; 3],
303    up: &[f64; 3],
304    vertical_fov: f32,
305    aspect_ratio: Option<f32>,
306    bounding_box: &[f64; 6],
307) -> String {
308    // FIXME with a && chain once https://github.com/rust-lang/rust/issues/53667
309    // arrives in stable.
310    let vertical_fov = if let Some(aspect_ratio) = aspect_ratio {
311        if aspect_ratio < 1.0 {
312            // Portrait.
313            2.0 * (aspect_ratio
314                * (0.5 * vertical_fov * core::f32::consts::PI / 180.0).tan())
315            .atan()
316        } else {
317            vertical_fov * core::f32::consts::PI / 180.0
318        }
319    } else {
320        vertical_fov * core::f32::consts::PI / 180.0
321    } as f64;
322
323    //println!("{}", vertical_fov);
324
325    // Make a cube from the bounds.
326    let cube = [
327        uv::DVec3::new(bounding_box[0], bounding_box[1], bounding_box[2]),
328        uv::DVec3::new(bounding_box[0], bounding_box[4], bounding_box[2]),
329        uv::DVec3::new(bounding_box[0], bounding_box[1], bounding_box[5]),
330        uv::DVec3::new(bounding_box[3], bounding_box[4], bounding_box[5]),
331        uv::DVec3::new(bounding_box[3], bounding_box[1], bounding_box[5]),
332        uv::DVec3::new(bounding_box[3], bounding_box[4], bounding_box[2]),
333    ];
334
335    let bounding_box_center = 0.5 * (cube[0] + cube[3]);
336
337    //println!("{:?}", bounding_box_center);
338
339    let bounding_sphere_radius = cube
340        .iter()
341        .fold(0.0f64, |max, point| {
342            max.max((bounding_box_center - *point).mag_sq())
343        })
344        .sqrt();
345
346    let distance = bounding_sphere_radius / (vertical_fov * 0.5).sin();
347
348    //println!("{}", distance);
349
350    let handle = generate_or_use_handle(handle, Some("look_at"));
351
352    ctx.create(handle.as_str(), nsi::node::TRANSFORM, None);
353
354    ctx.set_attribute(
355        handle.as_str(),
356        &[nsi::double_matrix!(
357            "transformationmatrix",
358            uv::DMat4::look_at(
359                bounding_box_center
360                    - distance * uv::DVec3::from(direction).normalized(),
361                bounding_box_center,
362                uv::DVec3::from(up)
363            )
364            .inversed()
365            .as_array()
366        )],
367    );
368
369    handle
370}