Skip to main content

gltforge_unity_export/
lib.rs

1pub mod build;
2pub mod error;
3pub mod export_context;
4pub mod mesh_data;
5pub mod node_data;
6pub mod submesh_data;
7
8// -------------------------------------------------------------------------- //
9
10use export_context::ExportContext;
11
12use std::ffi::CStr;
13use std::os::raw::c_char;
14use std::path::Path;
15
16// -------------------------------------------------------------------------- //
17
18/// Create a new export context.
19///
20/// Must be passed to [`gltforge_export_finish`] or [`gltforge_export_free`] to avoid a memory
21/// leak.
22#[unsafe(no_mangle)]
23pub extern "C" fn gltforge_export_begin() -> *mut ExportContext {
24    Box::into_raw(Box::new(ExportContext::new()))
25}
26
27/// Free an export context without writing any files.
28///
29/// # Safety
30/// `ctx` must be a valid pointer returned by [`gltforge_export_begin`] that has not yet been
31/// consumed by [`gltforge_export_finish`].
32#[unsafe(no_mangle)]
33pub unsafe extern "C" fn gltforge_export_free(ctx: *mut ExportContext) {
34    if !ctx.is_null() {
35        unsafe { drop(Box::from_raw(ctx)) };
36    }
37}
38
39/// Add a node (GameObject) to the export context. Returns the node index.
40///
41/// `parent_idx` is `-1` for root nodes, otherwise the index returned by a prior call.
42/// `pos`, `rot`, `scale` point to 3, 4, and 3 contiguous `f32` values respectively (Unity space).
43/// Pass null for any component to omit it from the output (glTF defaults apply).
44///
45/// # Safety
46/// `ctx` must be valid. `name_ptr` must point to `name_len` valid UTF-8 bytes, or be null.
47/// Each non-null transform pointer must point to the required number of `f32` values.
48#[unsafe(no_mangle)]
49pub unsafe extern "C" fn gltforge_export_add_node(
50    ctx: *mut ExportContext,
51    name_ptr: *const u8,
52    name_len: u32,
53    parent_idx: i32,
54    pos: *const f32,
55    rot: *const f32,
56    scale: *const f32,
57) -> u32 {
58    let ctx = unsafe { &mut *ctx };
59
60    let name = unsafe { read_name(name_ptr, name_len) };
61    let parent = if parent_idx < 0 {
62        None
63    } else {
64        Some(parent_idx as u32)
65    };
66    let translation = unsafe { read_f32s::<3>(pos) };
67    let rotation = unsafe { read_f32s::<4>(rot) };
68    let scale = unsafe { read_f32s::<3>(scale) };
69
70    ctx.add_node(name, parent, translation, rotation, scale)
71}
72
73/// Add a mesh to the export context. Returns the mesh index.
74///
75/// # Safety
76/// `ctx` must be valid. `name_ptr` must point to `name_len` valid UTF-8 bytes, or be null.
77#[unsafe(no_mangle)]
78pub unsafe extern "C" fn gltforge_export_add_mesh(
79    ctx: *mut ExportContext,
80    name_ptr: *const u8,
81    name_len: u32,
82) -> u32 {
83    let ctx = unsafe { &mut *ctx };
84    ctx.add_mesh(unsafe { read_name(name_ptr, name_len) })
85}
86
87/// Set vertex positions for mesh `mesh_idx`.
88/// `ptr` points to `f32_count` floats as tightly packed `[x, y, z]` triples (Unity space).
89///
90/// # Safety
91/// `ctx` and `ptr` must be valid. `f32_count` must be a multiple of 3.
92#[unsafe(no_mangle)]
93pub unsafe extern "C" fn gltforge_export_mesh_set_positions(
94    ctx: *mut ExportContext,
95    mesh_idx: u32,
96    ptr: *const f32,
97    f32_count: u32,
98) {
99    let ctx = unsafe { &mut *ctx };
100    let floats = unsafe { std::slice::from_raw_parts(ptr, f32_count as usize) };
101    let positions = floats.chunks_exact(3).map(|c| [c[0], c[1], c[2]]).collect();
102    ctx.set_positions(mesh_idx, positions);
103}
104
105/// Set vertex normals for mesh `mesh_idx`.
106/// `ptr` points to `f32_count` floats as tightly packed `[x, y, z]` triples (Unity space).
107///
108/// # Safety
109/// `ctx` and `ptr` must be valid. `f32_count` must be a multiple of 3.
110#[unsafe(no_mangle)]
111pub unsafe extern "C" fn gltforge_export_mesh_set_normals(
112    ctx: *mut ExportContext,
113    mesh_idx: u32,
114    ptr: *const f32,
115    f32_count: u32,
116) {
117    let ctx = unsafe { &mut *ctx };
118    let floats = unsafe { std::slice::from_raw_parts(ptr, f32_count as usize) };
119    let normals = floats.chunks_exact(3).map(|c| [c[0], c[1], c[2]]).collect();
120    ctx.set_normals(mesh_idx, normals);
121}
122
123/// Set a UV channel for mesh `mesh_idx`.
124/// `channel` is 0-based. `ptr` points to `f32_count` floats as `[u, v]` pairs (Unity space).
125///
126/// # Safety
127/// `ctx` and `ptr` must be valid. `f32_count` must be a multiple of 2.
128#[unsafe(no_mangle)]
129pub unsafe extern "C" fn gltforge_export_mesh_set_uvs(
130    ctx: *mut ExportContext,
131    mesh_idx: u32,
132    channel: u32,
133    ptr: *const f32,
134    f32_count: u32,
135) {
136    let ctx = unsafe { &mut *ctx };
137    let floats = unsafe { std::slice::from_raw_parts(ptr, f32_count as usize) };
138    let uvs = floats.chunks_exact(2).map(|c| [c[0], c[1]]).collect();
139    ctx.set_uvs(mesh_idx, channel, uvs);
140}
141
142/// Add a sub-mesh with 16-bit indices to mesh `mesh_idx`.
143///
144/// # Safety
145/// `ctx` and `ptr` must be valid. `index_count` must be a multiple of 3.
146#[unsafe(no_mangle)]
147pub unsafe extern "C" fn gltforge_export_mesh_add_submesh_u16(
148    ctx: *mut ExportContext,
149    mesh_idx: u32,
150    ptr: *const u16,
151    index_count: u32,
152) {
153    let ctx = unsafe { &mut *ctx };
154    let slice = unsafe { std::slice::from_raw_parts(ptr, index_count as usize) };
155    let indices = slice.iter().map(|&i| i as u32).collect();
156    ctx.add_submesh(mesh_idx, indices);
157}
158
159/// Add a sub-mesh with 32-bit indices to mesh `mesh_idx`.
160///
161/// # Safety
162/// `ctx` and `ptr` must be valid. `index_count` must be a multiple of 3.
163#[unsafe(no_mangle)]
164pub unsafe extern "C" fn gltforge_export_mesh_add_submesh_u32(
165    ctx: *mut ExportContext,
166    mesh_idx: u32,
167    ptr: *const u32,
168    index_count: u32,
169) {
170    let ctx = unsafe { &mut *ctx };
171    let slice = unsafe { std::slice::from_raw_parts(ptr, index_count as usize) };
172    ctx.add_submesh(mesh_idx, slice.to_vec());
173}
174
175/// Attach mesh `mesh_idx` to node `node_idx`.
176///
177/// # Safety
178/// `ctx` must be valid.
179#[unsafe(no_mangle)]
180pub unsafe extern "C" fn gltforge_export_node_set_mesh(
181    ctx: *mut ExportContext,
182    node_idx: u32,
183    mesh_idx: u32,
184) {
185    let ctx = unsafe { &mut *ctx };
186    ctx.set_node_mesh(node_idx, mesh_idx);
187}
188
189/// Build and write the `.gltf` and `.bin` files.
190/// `path` is the output `.gltf` file path (null-terminated UTF-8).
191/// Returns `1` on success, `0` on failure. Consumes and frees the context in both cases.
192///
193/// # Safety
194/// `ctx` must be a valid pointer returned by [`gltforge_export_begin`].
195/// `path` must be a valid null-terminated UTF-8 string.
196#[unsafe(no_mangle)]
197pub unsafe extern "C" fn gltforge_export_finish(
198    ctx: *mut ExportContext,
199    path: *const c_char,
200) -> u8 {
201    let result = std::panic::catch_unwind(|| {
202        let ctx = unsafe { Box::from_raw(ctx) };
203        let path_str = unsafe { CStr::from_ptr(path) }.to_str().ok()?;
204        build::write(*ctx, Path::new(path_str)).ok()
205    });
206    result.ok().flatten().map(|_| 1u8).unwrap_or(0)
207}
208
209/// Build and write a single `.glb` file (binary glTF).
210/// `path` is the output `.glb` file path (null-terminated UTF-8).
211/// Returns `1` on success, `0` on failure. Consumes and frees the context in both cases.
212///
213/// # Safety
214/// `ctx` must be a valid pointer returned by [`gltforge_export_begin`].
215/// `path` must be a valid null-terminated UTF-8 string.
216#[unsafe(no_mangle)]
217pub unsafe extern "C" fn gltforge_export_finish_glb(
218    ctx: *mut ExportContext,
219    path: *const c_char,
220) -> u8 {
221    let result = std::panic::catch_unwind(|| {
222        let ctx = unsafe { Box::from_raw(ctx) };
223        let path_str = unsafe { CStr::from_ptr(path) }.to_str().ok()?;
224        build::write_glb(*ctx, Path::new(path_str)).ok()
225    });
226    result.ok().flatten().map(|_| 1u8).unwrap_or(0)
227}
228
229// -------------------------------------------------------------------------- //
230
231unsafe fn read_name(ptr: *const u8, len: u32) -> Option<String> {
232    if ptr.is_null() || len == 0 {
233        return None;
234    }
235    let bytes = unsafe { std::slice::from_raw_parts(ptr, len as usize) };
236    std::str::from_utf8(bytes).ok().map(|s| s.to_string())
237}
238
239unsafe fn read_f32s<const N: usize>(ptr: *const f32) -> Option<[f32; N]> {
240    if ptr.is_null() {
241        return None;
242    }
243    let slice = unsafe { std::slice::from_raw_parts(ptr, N) };
244    let mut arr = [0f32; N];
245    arr.copy_from_slice(slice);
246    Some(arr)
247}