@@ -84,9 +84,237 @@ def test_save_motions_with_joint_params(self) -> None:
8484
8585 def _verify_fbx (self , file_name : str ) -> None :
8686 # Load FBX file
87- l_character , motion , fps = pym_geometry .Character .load_fbx_with_motion (
88- file_name
89- )
87+ _ , motion , fps = pym_geometry .Character .load_fbx_with_motion (file_name )
9088 self .assertEqual (1 , len (motion ))
9189 self .assertEqual (motion [0 ].shape , self .joint_params .shape )
9290 self .assertEqual (fps , 60 )
91+
92+ def test_save_with_namespace (self ) -> None :
93+ """Test FBX save with namespace parameter."""
94+ with tempfile .NamedTemporaryFile (suffix = ".fbx" ) as temp_file :
95+ offsets = np .zeros (self .joint_params .shape [1 ])
96+ # Save with namespace
97+ pym_geometry .Character .save_fbx (
98+ path = temp_file .name ,
99+ character = self .character ,
100+ motion = self .model_params .numpy (),
101+ offsets = offsets ,
102+ fps = 60 ,
103+ fbx_namespace = "test_ns" ,
104+ )
105+ # Verify file can be loaded
106+ self ._verify_fbx (temp_file .name )
107+
108+ def test_save_with_joint_params_and_namespace (self ) -> None :
109+ """Test FBX save with joint params and namespace parameter."""
110+ with tempfile .NamedTemporaryFile (suffix = ".fbx" ) as temp_file :
111+ pym_geometry .Character .save_fbx_with_joint_params (
112+ path = temp_file .name ,
113+ character = self .character ,
114+ joint_params = self .joint_params .numpy (),
115+ fps = 60 ,
116+ fbx_namespace = "test_ns" ,
117+ )
118+ # Verify file can be loaded
119+ self ._verify_fbx (temp_file .name )
120+
121+ def test_unified_save_fbx (self ) -> None :
122+ """Test unified save function with FBX extension."""
123+ with tempfile .NamedTemporaryFile (suffix = ".fbx" ) as temp_file :
124+ offsets = np .zeros (self .joint_params .shape [1 ])
125+ # Use unified save function - should auto-detect FBX format
126+ pym_geometry .Character .save (
127+ path = temp_file .name ,
128+ character = self .character ,
129+ motion = self .model_params .numpy (),
130+ offsets = offsets ,
131+ fps = 60 ,
132+ )
133+ # Verify file can be loaded
134+ self ._verify_fbx (temp_file .name )
135+
136+ def test_unified_save_glb (self ) -> None :
137+ """Test unified save function with GLB extension."""
138+ with tempfile .NamedTemporaryFile (suffix = ".glb" ) as temp_file :
139+ offsets = np .zeros (self .joint_params .shape [1 ])
140+ # Use unified save function - should auto-detect GLTF format
141+ pym_geometry .Character .save (
142+ path = temp_file .name ,
143+ character = self .character ,
144+ motion = self .model_params .numpy (),
145+ offsets = offsets ,
146+ fps = 60 ,
147+ )
148+ # Verify file can be loaded
149+ loaded_char , loaded_motion , _loaded_offsets , loaded_fps = (
150+ pym_geometry .Character .load_gltf_with_motion (temp_file .name )
151+ )
152+ self .assertEqual (loaded_char .skeleton .size , self .character .skeleton .size )
153+ self .assertEqual (loaded_motion .shape , self .model_params .shape )
154+ self .assertEqual (loaded_fps , 60 )
155+
156+ def test_unified_save_gltf (self ) -> None :
157+ """Test unified save function with GLTF extension."""
158+ with tempfile .NamedTemporaryFile (suffix = ".gltf" ) as temp_file :
159+ offsets = np .zeros (self .joint_params .shape [1 ])
160+ # Use unified save function - should auto-detect GLTF format
161+ pym_geometry .Character .save (
162+ path = temp_file .name ,
163+ character = self .character ,
164+ motion = self .model_params .numpy (),
165+ offsets = offsets ,
166+ fps = 60 ,
167+ )
168+ # Verify file can be loaded
169+ loaded_char , loaded_motion , _loaded_offsets , loaded_fps = (
170+ pym_geometry .Character .load_gltf_with_motion (temp_file .name )
171+ )
172+ self .assertEqual (loaded_char .skeleton .size , self .character .skeleton .size )
173+ self .assertEqual (loaded_motion .shape , self .model_params .shape )
174+ self .assertEqual (loaded_fps , 60 )
175+
176+ def test_marker_sequence_fbx_roundtrip (self ) -> None :
177+ """Test saving and loading marker sequences with FBX."""
178+ with tempfile .NamedTemporaryFile (suffix = ".fbx" ) as temp_file :
179+ # Create test marker sequence
180+ nFrames = 5
181+ markers_per_frame = []
182+ for frame in range (nFrames ):
183+ frame_markers = []
184+ for i in range (3 ):
185+ marker = pym_geometry .Marker (
186+ name = f"marker_{ i } " ,
187+ pos = np .array (
188+ [float (frame + i ), float (i ), float (frame )], dtype = np .float32
189+ ),
190+ occluded = (frame % 2 == 0 and i == 2 ),
191+ )
192+ frame_markers .append (marker )
193+ markers_per_frame .append (frame_markers )
194+
195+ # Save with unified function
196+ pym_geometry .Character .save (
197+ path = temp_file .name ,
198+ character = self .character ,
199+ fps = 60 ,
200+ markers = markers_per_frame ,
201+ )
202+
203+ # Load markers using load_markers function
204+ marker_sequences = pym_geometry .load_markers (temp_file .name )
205+ self .assertEqual (len (marker_sequences ), 1 )
206+
207+ loaded_sequence = marker_sequences [0 ]
208+ self .assertEqual (len (loaded_sequence .frames ), nFrames )
209+ self .assertEqual (loaded_sequence .fps , 60.0 )
210+
211+ # Verify marker data
212+ for frame_idx , frame in enumerate (loaded_sequence .frames ):
213+ # Check marker count (marker 2 is occluded on even frames)
214+ if frame_idx % 2 == 0 :
215+ # marker_2 should be occluded
216+ visible_markers = [m for m in frame .markers if not m .occluded ]
217+ self .assertEqual (len (visible_markers ), 2 )
218+ else :
219+ # All markers visible
220+ visible_markers = [m for m in frame .markers if not m .occluded ]
221+ self .assertEqual (len (visible_markers ), 3 )
222+
223+ def test_marker_sequence_fbx_with_motion (self ) -> None :
224+ """Test saving markers and motion together in FBX."""
225+ with tempfile .NamedTemporaryFile (suffix = ".fbx" ) as temp_file :
226+ # Create test marker sequence
227+ nFrames = 3
228+ markers_per_frame = []
229+ for frame in range (nFrames ):
230+ frame_markers = []
231+ for i in range (2 ):
232+ marker = pym_geometry .Marker (
233+ name = f"marker_{ i } " ,
234+ pos = np .array ([float (i ), float (frame ), 0.0 ], dtype = np .float32 ),
235+ occluded = False ,
236+ )
237+ frame_markers .append (marker )
238+ markers_per_frame .append (frame_markers )
239+
240+ # Save with both motion and markers
241+ offsets = np .zeros (self .joint_params .shape [1 ])
242+ pym_geometry .Character .save (
243+ path = temp_file .name ,
244+ character = self .character ,
245+ motion = self .model_params [:nFrames ].numpy (),
246+ offsets = offsets ,
247+ fps = 60 ,
248+ markers = markers_per_frame ,
249+ )
250+
251+ # Load and verify markers
252+ marker_sequences = pym_geometry .load_markers (temp_file .name )
253+ self .assertEqual (len (marker_sequences ), 1 )
254+ self .assertEqual (len (marker_sequences [0 ].frames ), nFrames )
255+
256+ # Load and verify motion
257+ _loaded_char , motion , _loaded_fps = (
258+ pym_geometry .Character .load_fbx_with_motion (temp_file .name )
259+ )
260+ self .assertEqual (len (motion ), 1 )
261+ self .assertEqual (motion [0 ].shape [0 ], nFrames )
262+
263+ def test_marker_sequence_sparse_keyframes (self ) -> None :
264+ """Test that marker sequences support sparse keyframes (not all frames have markers)."""
265+ with tempfile .NamedTemporaryFile (suffix = ".fbx" ) as temp_file :
266+ # Create sparse marker sequence - only keyframe at frame 0 and 2
267+ nFrames = 3
268+ markers_per_frame = []
269+
270+ # Frame 0: has markers
271+ frame_markers_0 = [
272+ pym_geometry .Marker (
273+ name = "marker_0" ,
274+ pos = np .array ([0.0 , 0.0 , 0.0 ], dtype = np .float32 ),
275+ occluded = False ,
276+ )
277+ ]
278+ markers_per_frame .append (frame_markers_0 )
279+
280+ # Frame 1: empty - no keyframe
281+ markers_per_frame .append ([])
282+
283+ # Frame 2: has markers
284+ frame_markers_2 = [
285+ pym_geometry .Marker (
286+ name = "marker_0" ,
287+ pos = np .array ([2.0 , 2.0 , 2.0 ], dtype = np .float32 ),
288+ occluded = False ,
289+ )
290+ ]
291+ markers_per_frame .append (frame_markers_2 )
292+
293+ # Save
294+ pym_geometry .Character .save (
295+ path = temp_file .name ,
296+ character = self .character ,
297+ fps = 60 ,
298+ markers = markers_per_frame ,
299+ )
300+
301+ # Load markers
302+ marker_sequences = pym_geometry .load_markers (temp_file .name )
303+ self .assertEqual (len (marker_sequences ), 1 )
304+
305+ loaded_sequence = marker_sequences [0 ]
306+ # Should have 3 frames total (sparse support)
307+ self .assertEqual (len (loaded_sequence .frames ), nFrames )
308+
309+ def test_load_markers_empty_file (self ) -> None :
310+ """Test loading markers from a file without markers."""
311+ with tempfile .NamedTemporaryFile (suffix = ".fbx" ) as temp_file :
312+ # Save character without markers
313+ pym_geometry .Character .save (
314+ path = temp_file .name , character = self .character , fps = 60
315+ )
316+
317+ # Load markers - should return empty sequence
318+ marker_sequences = pym_geometry .load_markers (temp_file .name )
319+ self .assertEqual (len (marker_sequences ), 1 )
320+ self .assertEqual (len (marker_sequences [0 ].frames ), 0 )
0 commit comments