Skip to main content

nsi_trait/
attribute.rs

1//! Typed attribute / parameter names — compile-time witnesses for ɴsɪ
2//! `NSIParam_t` identifiers.
3//!
4//! On the C side, ɴsɪ uses a single `NSIParam_t` struct everywhere. On the
5//! Rust side that single C concept splits into two semantic roles, both of
6//! which boil down to the *same* underlying type:
7//!
8//! - [`Attribute<T>`] — properties **set on a node** (govern how the scene
9//!   looks): `P`, `Pw`, `fov`, `transformationmatrix`, `uknot`, …
10//! - [`Parameter<T>`] — optional arguments **to a function** (govern how
11//!   the call behaves): `streamformat`, `errorhandler`, `stoppedcallback`, …
12//!
13//! `Parameter<T>` is just a type alias for `Attribute<T>` — the conceptual
14//! split exists so the constants and docs read more naturally, but the
15//! machinery is identical.
16//!
17//! End-users normally don't write these types out — they just reference the
18//! exported `const`s through the parameter macros:
19//!
20//! ```text
21//! nsi::point_slice!(POSITION, &points)  // node attribute (was: P)
22//! nsi::string!(STREAM_FORMAT, "nsi")    // function parameter to NSIBegin
23//! ```
24//!
25//! Renderer- or app-specific entries are added in downstream crates without
26//! touching this one — `Attribute::new` is `const`:
27//!
28//! ```ignore
29//! pub const MY_RENDERER_THING: Attribute<f32> = Attribute::new("custom_thing");
30//! ```
31
32use core::marker::PhantomData;
33
34// ─── Geometric type aliases ─────────────────────────────────────────────────
35//
36// Scalar attribute data uses the Rust primitives directly (`f32`, `f64`,
37// `i32`, `i64`) -- no aliases. Geometric types are aliases of fixed-size
38// arrays so that slice length is a multiple of the component count at the
39// type level.
40
41/// 2D point with `f32` components — typically a parametric (u, v) coordinate.
42pub type Point2F32 = [f32; 2];
43/// 3D point with `f32` components — Cartesian position.
44pub type Point3F32 = [f32; 3];
45/// 4D point with `f32` components — rational/weighted homogeneous (xyzw).
46pub type Point4F32 = [f32; 4];
47
48/// 2D vector with `f32` components.
49pub type Vector2F32 = [f32; 2];
50/// 3D vector with `f32` components.
51pub type Vector3F32 = [f32; 3];
52
53/// 3D normal with `f32` components.
54pub type Normal3F32 = [f32; 3];
55
56/// RGB color with `f32` components.
57pub type Color3F32 = [f32; 3];
58/// RGBA color with `f32` components.
59pub type Color4F32 = [f32; 4];
60
61/// 3×3 matrix with `f32` components.
62pub type Matrix3F32 = [[f32; 3]; 3];
63/// 4×4 matrix with `f32` components.
64pub type Matrix4F32 = [[f32; 4]; 4];
65/// 4×4 matrix with `f64` components.
66pub type Matrix4F64 = [[f64; 4]; 4];
67
68// ─── Attribute<T> ───────────────────────────────────────────────────────────
69
70/// Typed name of an ɴsɪ attribute.
71///
72/// `T` is the data shape the attribute accepts. Slice-typed `T = [U]` means
73/// the attribute carries an array of `U`.
74pub struct Attribute<T: ?Sized> {
75    name: &'static str,
76    _t: PhantomData<fn() -> T>,
77}
78
79// Manual derives so generic bounds don't require T: Clone/Copy/Debug — T is
80// only ever used in PhantomData<fn() -> T> which is already Copy/Send/Sync.
81impl<T: ?Sized> Clone for Attribute<T> {
82    fn clone(&self) -> Self {
83        *self
84    }
85}
86impl<T: ?Sized> Copy for Attribute<T> {}
87impl<T: ?Sized> core::fmt::Debug for Attribute<T> {
88    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
89        f.debug_struct("Attribute")
90            .field("name", &self.name)
91            .finish()
92    }
93}
94
95impl<T: ?Sized> Attribute<T> {
96    /// Create a new typed attribute name. `const`-callable so consumers can
97    /// declare attribute constants in their own crates.
98    pub const fn new(name: &'static str) -> Self {
99        Self {
100            name,
101            _t: PhantomData,
102        }
103    }
104
105    /// The wire-side string identifier this attribute uses.
106    #[inline]
107    pub const fn name(&self) -> &'static str {
108        self.name
109    }
110
111    /// Interned, null-terminated identifier suitable for C-FFI use. The
112    /// underlying [`Ustr`](ustr::Ustr) caches the interning so repeated calls
113    /// for the same name reuse the same allocation -- there is no per-call
114    /// `CString` dance.
115    ///
116    /// Available with the `ustr` feature.
117    #[cfg(feature = "ustr")]
118    #[inline]
119    pub fn ustr(&self) -> ustr::Ustr {
120        ustr::Ustr::from(self.name)
121    }
122
123    /// Raw C-FFI pointer to the null-terminated identifier. The lifetime is
124    /// effectively `'static` because [`ustr::Ustr`] never frees its strings.
125    /// Pass this directly to C functions expecting a `const char*`.
126    ///
127    /// Available with the `ustr` feature.
128    #[cfg(feature = "ustr")]
129    #[inline]
130    pub fn as_c_ptr(&self) -> *const core::ffi::c_char {
131        self.ustr().as_char_ptr()
132    }
133}
134
135// SAFETY: Attribute<T> contains only a `&'static str` and PhantomData.
136// PhantomData<fn() -> T> is variant-correct (covariant in T) and Send + Sync
137// regardless of T, so the auto-derive would also be Send + Sync — but we make
138// it explicit so users don't worry about T's bounds.
139unsafe impl<T: ?Sized> Send for Attribute<T> {}
140unsafe impl<T: ?Sized> Sync for Attribute<T> {}
141
142/// Typed name of an ɴsɪ function-parameter — alias of [`Attribute<T>`].
143///
144/// Used for the optional arguments passed to ɴsɪ calls (`NSIBegin`,
145/// `NSIRenderControl`, `NSIEvaluate`, …) that govern *how the call behaves*,
146/// as opposed to [`Attribute<T>`] which is set on nodes to govern *how the
147/// scene looks*. The C side uses a single `NSIParam_t` struct for both, so
148/// `Parameter<T>` and `Attribute<T>` are the same Rust type — the alias
149/// exists so consumers and docs can name the role precisely.
150pub type Parameter<T> = Attribute<T>;
151
152// ─── Standard ɴsɪ attribute names ───────────────────────────────────────────
153//
154// Rust constant identifiers are derived from the **new** wire-name convention
155// (see the `naming-convention.md` chapter in the ɴsɪ spec). Mapping rule:
156// take the new name, replace `-` and `.` with `_`, uppercase. Examples:
157//
158//   new wire        Rust const
159//   --------        ----------
160//   field-of-view   FIELD_OF_VIEW
161//   u.count         U_COUNT
162//   trim-curves.    TRIM_CURVES_*
163//   callback.error  CALLBACK_ERROR
164//
165// The string literals below intentionally still hold the **old** wire names
166// (e.g. `"fov"`, `"nu"`, `"errorhandler"`) so the constants work against a
167// pre-rename 3DelightNSI. Once the renderer ships the new names, only the
168// string literals here need to change -- the public Rust API stays stable.
169
170// Camera --------------------------------------------------------------------
171
172/// `field-of-view` (currently `fov`) — perspective camera FOV in degrees.
173pub const FIELD_OF_VIEW: Attribute<f32> = Attribute::new("fov");
174
175// Screen --------------------------------------------------------------------
176
177/// `resolution` — pixel resolution of a `screen` node, `[width, height]`.
178pub const RESOLUTION: Attribute<[i32]> = Attribute::new("resolution");
179/// `oversampling` — pixel oversampling rate.
180pub const OVERSAMPLING: Attribute<i32> = Attribute::new("oversampling");
181
182// Transform / shading -------------------------------------------------------
183
184/// `matrix` (currently `transformationmatrix`) — 4×4 row-major matrix (`f64`).
185pub const MATRIX: Attribute<Matrix4F64> =
186    Attribute::new("transformationmatrix");
187/// `filename` (currently `shaderfilename`) — OSL shader filename.
188pub const FILENAME: Attribute<&'static str> = Attribute::new("shaderfilename");
189
190// Common geometry attrs -----------------------------------------------------
191
192/// `position` (currently `P`) — Cartesian control points / vertices.
193///
194/// Slice of 3-component f32 points; total component count is divisible by 3
195/// at the type level.
196pub const POSITION: Attribute<[Point3F32]> = Attribute::new("P");
197/// `weighted-position` (currently `Pw`) — rational (weighted homogeneous)
198/// control points: xyzw.
199pub const WEIGHTED_POSITION: Attribute<[Point4F32]> = Attribute::new("Pw");
200
201// NURBS surface intrinsics --------------------------------------------------
202
203/// `u.count` (currently `nu`) — control-point count along *u*.
204pub const U_COUNT: Attribute<i32> = Attribute::new("nu");
205/// `v.count` (currently `nv`) — control-point count along *v*.
206pub const V_COUNT: Attribute<i32> = Attribute::new("nv");
207/// `u.order` (currently `uorder`) — order along *u* (degree + 1, ≥ 2).
208pub const U_ORDER: Attribute<i32> = Attribute::new("uorder");
209/// `v.order` (currently `vorder`) — order along *v* (degree + 1, ≥ 2).
210pub const V_ORDER: Attribute<i32> = Attribute::new("vorder");
211/// `u.knot` (currently `uknot`) — knot vector along *u*; length = `nu + uorder`.
212pub const U_KNOT: Attribute<[f32]> = Attribute::new("uknot");
213/// `v.knot` (currently `vknot`) — knot vector along *v*; length = `nv + vorder`.
214pub const V_KNOT: Attribute<[f32]> = Attribute::new("vknot");
215
216// NURBS trim curves ---------------------------------------------------------
217
218/// `trim-curves.loop-count` (currently `trimcurves.nloops`) — number of
219/// trim loops on a NURBS surface.
220pub const TRIM_CURVES_LOOP_COUNT: Attribute<i32> =
221    Attribute::new("trimcurves.nloops");
222/// `trim-curves.curve-count` (currently `trimcurves.ncurves`) — curves per loop.
223pub const TRIM_CURVES_CURVE_COUNT: Attribute<[i32]> =
224    Attribute::new("trimcurves.ncurves");
225/// `trim-curves.cv-count` (currently `trimcurves.n`) — control-point count
226/// per trim curve.
227pub const TRIM_CURVES_CV_COUNT: Attribute<[i32]> =
228    Attribute::new("trimcurves.n");
229/// `trim-curves.order` — order per trim curve (degree + 1).
230pub const TRIM_CURVES_ORDER: Attribute<[i32]> =
231    Attribute::new("trimcurves.order");
232/// `trim-curves.knot` — concatenated knots; total length = Σ(`n[i] + order[i]`).
233pub const TRIM_CURVES_KNOT: Attribute<[f32]> =
234    Attribute::new("trimcurves.knot");
235/// `trim-curves.min` — parametric start per trim curve.
236pub const TRIM_CURVES_MIN: Attribute<[f32]> = Attribute::new("trimcurves.min");
237/// `trim-curves.max` — parametric end per trim curve.
238pub const TRIM_CURVES_MAX: Attribute<[f32]> = Attribute::new("trimcurves.max");
239/// `trim-curves.u` — concatenated *u* control values; length = Σ`n[i]`.
240pub const TRIM_CURVES_U: Attribute<[f32]> = Attribute::new("trimcurves.u");
241/// `trim-curves.v` — concatenated *v* control values; length = Σ`n[i]`.
242pub const TRIM_CURVES_V: Attribute<[f32]> = Attribute::new("trimcurves.v");
243/// `trim-curves.w` — concatenated weights; length = Σ`n[i]`.
244pub const TRIM_CURVES_W: Attribute<[f32]> = Attribute::new("trimcurves.w");
245/// `trim-curves.sense` — one per loop. `0` = keep inside, `1` = keep outside (hole).
246pub const TRIM_CURVES_SENSE: Attribute<[i32]> =
247    Attribute::new("trimcurves.sense");
248
249// Globals / render-control --------------------------------------------------
250
251/// `bucket-order` (currently `bucketorder`) — bucket traversal pattern
252/// (`"horizontal"`, `"spiral"`, …).
253pub const BUCKET_ORDER: Attribute<&'static str> = Attribute::new("bucketorder");
254
255// ─── Function-level parameters (Parameter<T>) ───────────────────────────────
256//
257// These govern how an ɴsɪ *call* behaves rather than how a node looks. The
258// underlying type is the same -- `Parameter<T>` is just an alias for
259// `Attribute<T>` -- the split is purely for readability at call sites.
260
261/// `stream.format` (currently `streamformat`) — output stream format for
262/// `NSIBegin` (`"nsi"`, `"binarynsi"`, `"autonsi"`).
263pub const STREAM_FORMAT: Parameter<&'static str> =
264    Parameter::new("streamformat");
265/// `stream.filename` (currently `streamfilename`) — output file path when
266/// `NSIBegin` is invoked in stream-to-file mode.
267pub const STREAM_FILENAME: Parameter<&'static str> =
268    Parameter::new("streamfilename");
269/// `stream.path-replacement` (currently `streampathreplace`) — substitution
270/// pairs applied to paths in the output stream.
271pub const STREAM_PATH_REPLACEMENT: Parameter<&'static str> =
272    Parameter::new("streampathreplace");
273/// `callback.error` (currently `errorhandler`) — error-handler callback
274/// registered through `NSIBegin`.
275pub const CALLBACK_ERROR: Parameter<&'static str> =
276    Parameter::new("errorhandler");
277/// `callback.stop` (currently `stoppedcallback`) — callback fired when an
278/// interactive render stops.
279pub const CALLBACK_STOP: Parameter<&'static str> =
280    Parameter::new("stoppedcallback");
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285
286    #[test]
287    fn names_match_wire_strings() {
288        assert_eq!(FIELD_OF_VIEW.name(), "fov");
289        assert_eq!(POSITION.name(), "P");
290        assert_eq!(WEIGHTED_POSITION.name(), "Pw");
291        assert_eq!(U_COUNT.name(), "nu");
292        assert_eq!(V_COUNT.name(), "nv");
293        assert_eq!(U_ORDER.name(), "uorder");
294        assert_eq!(U_KNOT.name(), "uknot");
295        assert_eq!(TRIM_CURVES_LOOP_COUNT.name(), "trimcurves.nloops");
296        assert_eq!(TRIM_CURVES_SENSE.name(), "trimcurves.sense");
297    }
298
299    #[test]
300    fn attribute_is_send_sync() {
301        fn assert_send_sync<T: Send + Sync>() {}
302        assert_send_sync::<Attribute<f32>>();
303        assert_send_sync::<Attribute<[Point3F32]>>();
304        assert_send_sync::<Attribute<&'static str>>();
305    }
306
307    #[test]
308    fn attribute_is_copy() {
309        let p = POSITION;
310        let p2 = p; // Copy
311        assert_eq!(p.name(), p2.name());
312    }
313
314    #[cfg(feature = "ustr")]
315    #[test]
316    fn ustr_round_trip_and_null_terminated() {
317        // Same Attribute → same Ustr (caching).
318        let u1 = POSITION.ustr();
319        let u2 = POSITION.ustr();
320        assert_eq!(u1, u2);
321        assert_eq!(u1.as_str(), "P");
322
323        // Raw C pointer ends in NUL; readable with CStr.
324        let cptr = POSITION.as_c_ptr();
325        // SAFETY: Ustr always returns a valid null-terminated C string.
326        let cstr = unsafe { core::ffi::CStr::from_ptr(cptr) };
327        assert_eq!(cstr.to_str().unwrap(), "P");
328    }
329}