1use std::collections::{BTreeMap, HashMap, HashSet};
2use std::fmt;
3
4use glam::{Quat, Vec3A};
5use mmd_anim_runtime::{
6 AnimationClip, BoneAnimationBinding, BoneIndex, InterpolationScalar, InterpolationVector3,
7 MorphAnimationBinding, MorphIndex, MorphKeyframe, MorphTrack, MovableBoneKeyframe,
8 MovableBoneTrack,
9};
10use serde::Serialize;
11
12use crate::binary::{
13 ByteReader, write_f32_le as push_f32, write_i32_le as push_i32, write_u32_le as push_u32,
14};
15use crate::error::ImportError;
16use crate::pmx::PmxParsedModel;
17use crate::sjis::{decode_sjis, decode_sjis_trim_nul, encode_sjis, encode_sjis_prefix_fit};
18use crate::vmd::{VmdParsedAnimation, VmdParsedBoneFrame, VmdParsedMorphFrame};
19
20#[derive(Debug, Clone, Serialize)]
21#[serde(rename_all = "camelCase")]
22pub struct PmmParsedManifest {
23 pub signature: &'static str,
24 pub version: String,
25 pub parsed_version: Option<u32>,
26 pub byte_length: usize,
27 pub project_settings: PmmProjectSettings,
28 pub timeline: PmmTimeline,
29 pub display_state: PmmDisplayState,
30 pub header_text_entries: Vec<PmmHeaderTextEntry>,
31 pub model_slots: Vec<PmmModelSlot>,
32 pub document_summary: Option<PmmDocumentSummary>,
33 pub document_global_summary: Option<PmmDocumentGlobalSummary>,
34 pub project_graph: Option<PmmProjectGraph>,
35 pub asset_summary: PmmAssetSummary,
36 pub asset_references: Vec<PmmAssetReference>,
37 pub model_assets: Vec<PmmSceneAsset>,
38 pub accessory_assets: Vec<PmmSceneAsset>,
39 pub motion_assets: Vec<PmmSceneAsset>,
40 pub audio_assets: Vec<PmmSceneAsset>,
41 pub image_assets: Vec<PmmSceneAsset>,
42 pub video_assets: Vec<PmmSceneAsset>,
43 pub model_paths: Vec<String>,
44 pub accessory_paths: Vec<String>,
45 pub motion_paths: Vec<String>,
46 pub audio_paths: Vec<String>,
47 pub image_paths: Vec<String>,
48 pub video_paths: Vec<String>,
49 pub diagnostics: Vec<PmmParserDiagnostic>,
50 #[serde(skip)]
51 source_bytes: Vec<u8>,
52}
53
54#[derive(Debug, Clone, Serialize)]
55#[serde(rename_all = "camelCase")]
56pub struct PmmProjectSettings {
57 pub screen_width: Option<u32>,
58 pub screen_height: Option<u32>,
59 pub timeline_frame_count: Option<u32>,
60 pub frame_rate: Option<f32>,
61}
62
63#[derive(Debug, Clone, Serialize)]
64#[serde(rename_all = "camelCase")]
65pub struct PmmTimeline {
66 pub start_frame: Option<u32>,
67 pub end_frame_exclusive: Option<u32>,
68 pub frame_count: Option<u32>,
69 pub frame_rate: Option<f32>,
70 pub duration_seconds: Option<f32>,
71}
72
73#[derive(Debug, Clone, Serialize)]
74#[serde(rename_all = "camelCase")]
75pub struct PmmDisplayState {
76 pub layout: &'static str,
77 pub model_slot_flags: Vec<u8>,
78 pub model_slot_flag_entries: Vec<PmmModelSlotFlagEntry>,
79 pub document_expand_flags: Option<PmmDocumentExpandFlags>,
80 pub selected_model_index: Option<u8>,
81 pub document_model_count: Option<u8>,
82 pub declared_model_slot_count: Option<u8>,
83 pub model_slot_count: usize,
84 pub non_zero_model_slot_count: usize,
85 pub accessory_slot_count: Option<u8>,
86 pub active_model_slot_indices: Vec<usize>,
87 pub empty_model_slot_indices: Vec<usize>,
88 pub model_slot_flag_counts: BTreeMap<u8, usize>,
89}
90
91#[derive(Debug, Clone, Serialize)]
92#[serde(rename_all = "camelCase")]
93pub struct PmmDocumentExpandFlags {
94 pub editing_cla: bool,
95 pub camera_panel: bool,
96 pub light_panel: bool,
97 pub accessory_panel: bool,
98 pub bone_panel: bool,
99 pub morph_panel: bool,
100 pub self_shadow_panel: bool,
101}
102
103#[derive(Debug, Clone, Serialize)]
104#[serde(rename_all = "camelCase")]
105pub struct PmmModelSlotFlagEntry {
106 pub slot_index: usize,
107 pub flag: u8,
108 pub active: bool,
109}
110
111#[derive(Debug, Clone, Serialize)]
112#[serde(rename_all = "camelCase")]
113pub struct PmmHeaderTextEntry {
114 pub index: usize,
115 pub offset: usize,
116 pub offset_end: usize,
117 pub text: String,
118 pub text_bytes: Vec<u8>,
119}
120
121#[derive(Debug, Clone, Serialize)]
122#[serde(rename_all = "camelCase")]
123pub struct PmmModelSlot {
124 pub slot_index: usize,
125 pub display_slot_index: Option<usize>,
126 pub offset: usize,
127 pub offset_end: usize,
128 pub model_path_offset: usize,
129 pub trailing_zero_padding_bytes: usize,
130 pub next_non_zero_offset: Option<usize>,
131 pub name: String,
132 pub name_bytes: Vec<u8>,
133 pub english_name: String,
134 pub english_name_bytes: Vec<u8>,
135 pub model_path: String,
136 pub normalized_path: String,
137 pub asset_reference_index: Option<usize>,
138 pub confidence: &'static str,
139}
140
141#[derive(Debug, Clone, Serialize)]
142#[serde(rename_all = "camelCase")]
143pub struct PmmAssetSummary {
144 pub reference_count: usize,
145 pub high_confidence_count: usize,
146 pub medium_confidence_count: usize,
147 pub low_confidence_count: usize,
148 pub kind_counts: BTreeMap<String, usize>,
149 pub extension_counts: BTreeMap<String, usize>,
150 pub confidence_counts: BTreeMap<String, usize>,
151}
152
153#[derive(Debug, Clone, Serialize)]
154#[serde(rename_all = "camelCase")]
155pub struct PmmDocumentSummary {
156 pub source: &'static str,
157 pub selected_model_index: u8,
158 pub model_count: usize,
159 pub counts: PmmDocumentCounts,
160 pub models: Vec<PmmDocumentModelSummary>,
161}
162
163#[derive(Debug, Clone, Serialize)]
164#[serde(rename_all = "camelCase")]
165pub struct PmmDocumentCounts {
166 pub models: usize,
167 pub bones: usize,
168 pub morphs: usize,
169 pub initial_bone_keyframes: usize,
170 pub bone_keyframes: usize,
171 pub initial_morph_keyframes: usize,
172 pub morph_keyframes: usize,
173 pub initial_model_keyframes: usize,
174 pub model_keyframes: usize,
175}
176
177#[derive(Debug, Clone, Serialize)]
178#[serde(rename_all = "camelCase")]
179pub struct PmmDocumentGlobalSummary {
180 pub source: &'static str,
181 pub offset: usize,
182 pub offset_end: usize,
183 pub camera: PmmDocumentTrackSummary,
184 pub light: PmmDocumentTrackSummary,
185 pub accessories: PmmDocumentAccessoryBlockSummary,
186 pub settings: PmmDocumentSettingsSummary,
187 pub gravity: PmmDocumentTrackSummary,
188 pub self_shadow: PmmDocumentTrackSummary,
189}
190
191#[derive(Debug, Clone, Serialize)]
192#[serde(rename_all = "camelCase")]
193pub struct PmmProjectSceneSettings {
194 pub offset: usize,
195 pub offset_end: usize,
196 pub current_frame_index: i32,
197 pub current_frame_index_in_text_field: i32,
198 pub begin_frame_index_enabled: bool,
199 pub end_frame_index_enabled: bool,
200 pub begin_frame_index: i32,
201 pub end_frame_index: i32,
202 pub preferred_fps: f32,
203 pub loop_enabled: bool,
204 pub audio_enabled: bool,
205 pub audio_path: String,
206 pub audio_asset_reference_index: Option<usize>,
207 pub background_video_enabled: bool,
208 pub background_video_path: String,
209 pub background_video_asset_reference_index: Option<usize>,
210 pub background_video_offset: [i32; 2],
211 pub background_video_scale_factor: f32,
212 pub background_image_enabled: bool,
213 pub background_image_path: String,
214 pub background_image_asset_reference_index: Option<usize>,
215 pub background_image_offset: [i32; 2],
216 pub background_image_scale_factor: f32,
217}
218
219#[derive(Debug, Clone, Serialize)]
220#[serde(rename_all = "camelCase")]
221pub struct PmmProjectAssetBinding {
222 pub scope: &'static str,
223 pub asset_kind: &'static str,
224 pub owner_index: Option<usize>,
225 pub document_index: Option<u8>,
226 pub owner_name: Option<String>,
227 pub path: String,
228 pub path_offset: Option<usize>,
229 pub asset_reference_index: Option<usize>,
230 pub asset_reference_offset: Option<usize>,
231 pub asset_reference_end_offset: Option<usize>,
232 pub asset_reference_confidence: Option<&'static str>,
233}
234
235#[derive(Debug, Clone, Serialize)]
236#[serde(rename_all = "camelCase")]
237pub struct PmmProjectExportBlocker {
238 pub code: &'static str,
239 pub severity: &'static str,
240 pub message: String,
241 pub scope: Option<&'static str>,
242 pub kind: Option<&'static str>,
243 pub count: Option<usize>,
244 pub coverage_ratio: Option<f32>,
245}
246
247#[derive(Debug, Clone, Serialize)]
248#[serde(rename_all = "camelCase")]
249pub struct PmmProjectExportReadiness {
250 pub lossless_parsed_byte_export_supported: bool,
251 pub semantic_graph_export_supported: bool,
252 pub source_byte_preservation_required: bool,
253 pub blocker_count: usize,
254 pub blockers: Vec<PmmProjectExportBlocker>,
255}
256
257#[derive(Debug, Clone, Serialize)]
258#[serde(rename_all = "camelCase")]
259pub struct PmmProjectGraph {
260 pub source: &'static str,
261 pub version: String,
262 pub parsed_version: Option<u32>,
263 pub timeline: PmmTimeline,
264 pub display_state: PmmDisplayState,
265 pub asset_summary: PmmAssetSummary,
266 pub asset_references: Vec<PmmAssetReference>,
267 pub models: Vec<PmmDocumentModelSummary>,
268 pub global: PmmDocumentGlobalSummary,
269 pub model_counts: PmmDocumentCounts,
270 pub accessory_count: usize,
271 pub accessory_keyframes: usize,
272 pub track_references: Vec<PmmProjectTrackReference>,
273 pub keyframe_references: Vec<PmmProjectKeyframeReference>,
274 pub byte_coverage: PmmProjectByteCoverage,
275 pub scene_settings: PmmProjectSceneSettings,
276 pub asset_bindings: Vec<PmmProjectAssetBinding>,
277 pub export_readiness: PmmProjectExportReadiness,
278}
279
280#[derive(Debug, Clone, Serialize)]
281#[serde(rename_all = "camelCase")]
282pub struct PmmProjectTrackReference {
283 pub scope: &'static str,
284 pub track_kind: &'static str,
285 pub owner_index: Option<usize>,
286 pub document_index: Option<u8>,
287 pub owner_name: Option<String>,
288 pub initial_keyframes: usize,
289 pub keyframes: usize,
290 pub initial_keyframes_offset: Option<usize>,
291 pub keyframe_count_offset: Option<usize>,
292 pub keyframes_offset: usize,
293 pub keyframes_end_offset: usize,
294 pub state_offset: Option<usize>,
295 pub state_end_offset: Option<usize>,
296}
297
298#[derive(Debug, Clone, Serialize)]
299#[serde(rename_all = "camelCase")]
300pub struct PmmProjectKeyframeReference {
301 pub scope: &'static str,
302 pub track_kind: &'static str,
303 pub owner_index: Option<usize>,
304 pub document_index: Option<u8>,
305 pub owner_name: Option<String>,
306 pub initial: bool,
307 pub keyframe_index: Option<i32>,
308 pub frame_index: i32,
309 pub previous_keyframe_index: i32,
310 pub next_keyframe_index: i32,
311 pub offset: usize,
312 pub byte_length: usize,
313 pub payload_offset: usize,
314 pub payload_byte_length: usize,
315}
316
317#[derive(Debug, Clone, Serialize)]
318#[serde(rename_all = "camelCase")]
319pub struct PmmProjectByteRange {
320 pub scope: &'static str,
321 pub kind: &'static str,
322 pub owner_index: Option<usize>,
323 pub document_index: Option<u8>,
324 pub name: Option<String>,
325 pub offset: usize,
326 pub offset_end: usize,
327 pub byte_length: usize,
328}
329
330#[derive(Debug, Clone, Serialize)]
331#[serde(rename_all = "camelCase")]
332pub struct PmmProjectByteCoverage {
333 pub offset: usize,
334 pub offset_end: usize,
335 pub byte_length: usize,
336 pub covered_byte_length: usize,
337 pub coverage_ratio: f32,
338 pub gap_count: usize,
339 pub gaps: Vec<PmmProjectByteRange>,
340 pub range_count: usize,
341 pub ranges: Vec<PmmProjectByteRange>,
342}
343
344#[derive(Debug, Clone, Serialize)]
345#[serde(rename_all = "camelCase")]
346pub struct PmmDocumentTrackSummary {
347 pub offset: usize,
348 pub offset_end: usize,
349 pub initial_keyframes: usize,
350 pub keyframes: usize,
351 pub initial_keyframe: Option<PmmDocumentKeyframeSummary>,
352 pub keyframe_summaries: Vec<PmmDocumentKeyframeSummary>,
353 pub keyframe_count_offset: usize,
354 pub keyframes_offset: usize,
355 pub keyframes_end_offset: usize,
356 pub state_offset: Option<usize>,
357 pub state_end_offset: Option<usize>,
358}
359
360#[derive(Debug, Clone, Serialize)]
361#[serde(
362 rename_all = "camelCase",
363 rename_all_fields = "camelCase",
364 tag = "kind"
365)]
366pub enum PmmDocumentKeyframeSummary {
367 Camera {
368 index: Option<i32>,
369 frame_index: i32,
370 previous_keyframe_index: i32,
371 next_keyframe_index: i32,
372 distance: f32,
373 look_at: [f32; 3],
374 angle: [f32; 3],
375 parent_model_index: i32,
376 parent_model_bone_index: i32,
377 interpolation: [u8; 24],
378 perspective_view: bool,
379 fov: i32,
380 selected: bool,
381 distance_offset: usize,
382 look_at_offset: usize,
383 look_at_byte_length: usize,
384 angle_offset: usize,
385 angle_byte_length: usize,
386 parent_model_index_offset: usize,
387 parent_model_bone_index_offset: usize,
388 interpolation_offset: usize,
389 interpolation_byte_length: usize,
390 perspective_view_offset: usize,
391 fov_offset: usize,
392 selected_offset: usize,
393 offset: usize,
394 byte_length: usize,
395 payload_offset: usize,
396 payload_byte_length: usize,
397 payload_bytes: Vec<u8>,
398 },
399 Light {
400 index: Option<i32>,
401 frame_index: i32,
402 previous_keyframe_index: i32,
403 next_keyframe_index: i32,
404 color: [f32; 3],
405 direction: [f32; 3],
406 selected: bool,
407 color_offset: usize,
408 color_byte_length: usize,
409 direction_offset: usize,
410 direction_byte_length: usize,
411 selected_offset: usize,
412 offset: usize,
413 byte_length: usize,
414 payload_offset: usize,
415 payload_byte_length: usize,
416 payload_bytes: Vec<u8>,
417 },
418 Gravity {
419 index: Option<i32>,
420 frame_index: i32,
421 previous_keyframe_index: i32,
422 next_keyframe_index: i32,
423 noise_enabled: bool,
424 noise: i32,
425 acceleration: f32,
426 direction: [f32; 3],
427 selected: bool,
428 noise_enabled_offset: usize,
429 noise_offset: usize,
430 acceleration_offset: usize,
431 direction_offset: usize,
432 direction_byte_length: usize,
433 selected_offset: usize,
434 offset: usize,
435 byte_length: usize,
436 payload_offset: usize,
437 payload_byte_length: usize,
438 payload_bytes: Vec<u8>,
439 },
440 SelfShadow {
441 index: Option<i32>,
442 frame_index: i32,
443 previous_keyframe_index: i32,
444 next_keyframe_index: i32,
445 mode: u8,
446 distance: f32,
447 selected: bool,
448 mode_offset: usize,
449 distance_offset: usize,
450 selected_offset: usize,
451 offset: usize,
452 byte_length: usize,
453 payload_offset: usize,
454 payload_byte_length: usize,
455 payload_bytes: Vec<u8>,
456 },
457}
458
459#[derive(Debug, Clone, Copy)]
460struct PmmDocumentBaseKeyframeSummary {
461 index: Option<i32>,
462 frame_index: i32,
463 previous_keyframe_index: i32,
464 next_keyframe_index: i32,
465}
466
467#[derive(Debug, Clone, Serialize)]
468#[serde(rename_all = "camelCase")]
469pub struct PmmDocumentAccessoryKeyframeSummary {
470 pub index: Option<i32>,
471 pub frame_index: i32,
472 pub previous_keyframe_index: i32,
473 pub next_keyframe_index: i32,
474 pub visible: bool,
475 pub opacity: f32,
476 pub parent_model_index: i32,
477 pub parent_model_bone_index: i32,
478 pub translation: [f32; 3],
479 pub orientation: [f32; 3],
480 pub scale_factor: f32,
481 pub shadow_enabled: bool,
482 pub selected: bool,
483 pub packed_opacity_visible_offset: usize,
484 pub parent_model_index_offset: usize,
485 pub parent_model_bone_index_offset: usize,
486 pub translation_offset: usize,
487 pub translation_byte_length: usize,
488 pub orientation_offset: usize,
489 pub orientation_byte_length: usize,
490 pub scale_factor_offset: usize,
491 pub shadow_enabled_offset: usize,
492 pub selected_offset: usize,
493 pub offset: usize,
494 pub byte_length: usize,
495 pub payload_offset: usize,
496 pub payload_byte_length: usize,
497 pub payload_bytes: Vec<u8>,
498}
499
500#[derive(Debug, Clone, Serialize)]
501#[serde(rename_all = "camelCase")]
502pub struct PmmDocumentBoneKeyframeSummary {
503 pub index: Option<i32>,
504 pub frame_index: i32,
505 pub previous_keyframe_index: i32,
506 pub next_keyframe_index: i32,
507 pub interpolation: [u8; 16],
508 pub translation: [f32; 3],
509 pub orientation: [f32; 4],
510 pub physics_disabled: bool,
511 pub selected: bool,
512 pub offset: usize,
513 pub byte_length: usize,
514 pub payload_offset: usize,
515 pub payload_byte_length: usize,
516 pub payload_bytes: Vec<u8>,
517}
518
519#[derive(Debug, Clone, Serialize)]
520#[serde(rename_all = "camelCase")]
521pub struct PmmDocumentMorphKeyframeSummary {
522 pub index: Option<i32>,
523 pub frame_index: i32,
524 pub previous_keyframe_index: i32,
525 pub next_keyframe_index: i32,
526 pub weight: f32,
527 pub selected: bool,
528 pub offset: usize,
529 pub byte_length: usize,
530 pub payload_offset: usize,
531 pub payload_byte_length: usize,
532 pub payload_bytes: Vec<u8>,
533}
534
535#[derive(Debug, Clone, Serialize)]
536#[serde(rename_all = "camelCase")]
537pub struct PmmDocumentModelKeyframeSummary {
538 pub index: Option<i32>,
539 pub frame_index: i32,
540 pub previous_keyframe_index: i32,
541 pub next_keyframe_index: i32,
542 pub visible: bool,
543 pub constraint_states: Vec<bool>,
544 pub outside_parent_indices: Vec<PmmDocumentOutsideParentIndexSummary>,
545 pub self_shadow_enabled: bool,
546 pub visible_offset: usize,
547 pub constraint_state_count: usize,
548 pub constraint_states_offset: usize,
549 pub constraint_states_byte_length: usize,
550 pub outside_parent_index_count: usize,
551 pub outside_parent_indices_offset: usize,
552 pub outside_parent_indices_byte_length: usize,
553 pub self_shadow_enabled_offset: usize,
554 pub offset: usize,
555 pub byte_length: usize,
556 pub payload_offset: usize,
557 pub payload_byte_length: usize,
558 pub payload_bytes: Vec<u8>,
559}
560
561#[derive(Debug, Clone, Serialize)]
562#[serde(rename_all = "camelCase")]
563pub struct PmmDocumentOutsideParentIndexSummary {
564 pub parent_model_index: i32,
565 pub parent_model_bone_index: i32,
566}
567
568#[derive(Debug, Clone, Serialize)]
569#[serde(rename_all = "camelCase")]
570pub struct PmmDocumentBoneStateSummary {
571 pub translation: [f32; 3],
572 pub orientation: [f32; 4],
573 pub dirty: bool,
574 pub physics_disabled: bool,
575 pub selected: bool,
576}
577
578#[derive(Debug, Clone, Serialize)]
579#[serde(rename_all = "camelCase")]
580pub struct PmmDocumentMorphStateSummary {
581 pub weight: f32,
582}
583
584#[derive(Debug, Clone, Serialize)]
585#[serde(rename_all = "camelCase")]
586pub struct PmmDocumentConstraintStateSummary {
587 pub enabled: bool,
588}
589
590#[derive(Debug, Clone, Serialize)]
593#[serde(rename_all = "camelCase")]
594pub struct PmmDocumentOutsideParentStateSummary {
595 pub parent_model_index: i32,
596 pub parent_model_bone_index: i32,
597 pub subject_bone_index: i32,
598 pub target_model_index: i32,
599}
600
601#[derive(Debug, Clone, Serialize)]
602#[serde(rename_all = "camelCase")]
603pub struct PmmDocumentAccessoryBlockSummary {
604 pub offset: usize,
605 pub offset_end: usize,
606 pub selected_accessory_index: u8,
607 pub horizontal_scroll: i32,
608 pub accessory_count: usize,
609 pub keyframes: usize,
610 pub accessories: Vec<PmmDocumentAccessorySummary>,
611}
612
613#[derive(Debug, Clone, Serialize)]
614#[serde(rename_all = "camelCase")]
615pub struct PmmDocumentAccessorySummary {
616 pub slot_index: usize,
617 pub document_accessory_index: u8,
618 pub offset: usize,
619 pub offset_end: usize,
620 pub path_offset: usize,
621 pub name: String,
622 pub path: String,
623 pub asset_reference_index: Option<usize>,
624 pub draw_order_index: u8,
625 pub keyframes: usize,
626 pub initial_keyframe: PmmDocumentAccessoryKeyframeSummary,
627 pub keyframe_summaries: Vec<PmmDocumentAccessoryKeyframeSummary>,
628 pub keyframe_count_offset: usize,
629 pub keyframes_offset: usize,
630 pub keyframes_end_offset: usize,
631 pub state_offset: usize,
632 pub state_end_offset: usize,
633 pub visible: bool,
634 pub opacity: f32,
635 pub parent_model_index: i32,
636 pub parent_model_bone_index: i32,
637 pub scale_factor: f32,
638 pub shadow_enabled: bool,
639 pub add_blend_enabled: bool,
640}
641
642#[derive(Debug, Clone, Serialize)]
643#[serde(rename_all = "camelCase")]
644pub struct PmmDocumentSettingsSummary {
645 pub offset: usize,
646 pub offset_end: usize,
647 pub current_frame_index: i32,
648 pub current_frame_index_offset: usize,
649 pub horizontal_scroll: i32,
650 pub horizontal_scroll_thumb: i32,
651 pub editing_mode: i32,
652 pub camera_look_mode: u8,
653 pub loop_enabled: bool,
654 pub begin_frame_index_enabled: bool,
655 pub begin_frame_index_enabled_offset: usize,
656 pub end_frame_index_enabled: bool,
657 pub end_frame_index_enabled_offset: usize,
658 pub begin_frame_index: i32,
659 pub begin_frame_index_offset: usize,
660 pub end_frame_index: i32,
661 pub end_frame_index_offset: usize,
662 pub audio_enabled: bool,
663 pub audio_path: String,
664 pub audio_path_offset: usize,
665 pub background_video_offset: [i32; 2],
666 pub background_video_scale_factor: f32,
667 pub background_video_path: String,
668 pub background_video_path_offset: usize,
669 pub background_video_enabled: bool,
670 pub background_image_offset: [i32; 2],
671 pub background_image_scale_factor: f32,
672 pub background_image_path: String,
673 pub background_image_path_offset: usize,
674 pub background_image_enabled: bool,
675 pub information_shown: bool,
676 pub grid_and_axis_shown: bool,
677 pub ground_shadow_shown: bool,
678 pub preferred_fps: f32,
679 pub screen_capture_mode: i32,
680 pub accessory_index_after_models: i32,
681 pub ground_shadow_brightness: f32,
682 pub translucent_ground_shadow_enabled: bool,
683 pub physics_simulation_mode: u8,
684 pub edge_color: [f32; 3],
685 pub black_background_enabled: bool,
686 pub camera_look_at_model_index: i32,
687 pub camera_look_at_model_bone_index: i32,
688 pub unknown_matrix_offset: usize,
689 pub unknown_matrix_end_offset: usize,
690 pub following_look_at_enabled: bool,
691 pub physics_ground_enabled: bool,
692 pub current_frame_index_in_text_field: i32,
693 pub current_frame_index_in_text_field_offset: usize,
694 pub model_selection_footer_present: bool,
695 pub model_selection_footer_offset: Option<usize>,
696 pub model_selection_footer_end_offset: Option<usize>,
697}
698
699#[derive(Debug, Clone, Serialize)]
700#[serde(rename_all = "camelCase")]
701pub struct PmmDocumentModelSummary {
702 pub slot_index: usize,
703 pub document_model_index: u8,
704 pub offset: usize,
705 pub offset_end: usize,
706 pub path_offset: usize,
707 pub name: String,
708 pub english_name: String,
709 pub path: String,
710 pub asset_reference_index: Option<usize>,
711 pub bone_count: usize,
712 pub bone_names: Vec<String>,
713 pub morph_count: usize,
714 pub morph_names: Vec<String>,
715 pub constraint_bone_count: usize,
716 pub outside_parent_subject_bone_count: usize,
717 pub draw_order_index: u8,
718 pub transform_order_index: u8,
719 pub selected_bone_index: i32,
720 pub selected_morph_indices: [i32; 4],
721 pub vertical_scroll: i32,
722 pub sections: PmmDocumentModelSections,
723 pub initial_bone_keyframes: usize,
724 pub initial_bone_keyframe_summaries: Vec<PmmDocumentBoneKeyframeSummary>,
725 pub bone_keyframes: usize,
726 pub bone_keyframe_summaries: Vec<PmmDocumentBoneKeyframeSummary>,
727 pub initial_morph_keyframes: usize,
728 pub initial_morph_keyframe_summaries: Vec<PmmDocumentMorphKeyframeSummary>,
729 pub morph_keyframes: usize,
730 pub morph_keyframe_summaries: Vec<PmmDocumentMorphKeyframeSummary>,
731 pub initial_model_keyframes: usize,
732 pub model_keyframes: usize,
733 pub initial_model_keyframe: PmmDocumentModelKeyframeSummary,
734 pub model_keyframe_summaries: Vec<PmmDocumentModelKeyframeSummary>,
735 pub last_frame_index: i32,
736 pub visible: bool,
737 pub blend_enabled: bool,
738 pub edge_width: f32,
739 pub self_shadow_enabled: bool,
740 pub bone_state_summaries: Vec<PmmDocumentBoneStateSummary>,
741 pub morph_state_summaries: Vec<PmmDocumentMorphStateSummary>,
742 pub constraint_state_summaries: Vec<PmmDocumentConstraintStateSummary>,
743 pub outside_parent_state_summaries: Vec<PmmDocumentOutsideParentStateSummary>,
744}
745
746#[derive(Debug, Clone, PartialEq, Eq)]
747pub struct PmmDocumentClipBuildError {
748 message: String,
749}
750
751impl PmmDocumentClipBuildError {
752 fn new(message: impl Into<String>) -> Self {
753 Self {
754 message: message.into(),
755 }
756 }
757}
758
759impl fmt::Display for PmmDocumentClipBuildError {
760 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
761 formatter.write_str(&self.message)
762 }
763}
764
765impl std::error::Error for PmmDocumentClipBuildError {}
766
767pub fn build_pmm_document_model_clip(
775 model: &PmmDocumentModelSummary,
776) -> Result<AnimationClip, PmmDocumentClipBuildError> {
777 if model.initial_bone_keyframe_summaries.len() != model.bone_count {
778 return Err(PmmDocumentClipBuildError::new(format!(
779 "PMM model {} initial bone keyframe count {} does not match bone count {}",
780 model.name,
781 model.initial_bone_keyframe_summaries.len(),
782 model.bone_count
783 )));
784 }
785 if model.initial_morph_keyframe_summaries.len() != model.morph_count {
786 return Err(PmmDocumentClipBuildError::new(format!(
787 "PMM model {} initial morph keyframe count {} does not match morph count {}",
788 model.name,
789 model.initial_morph_keyframe_summaries.len(),
790 model.morph_count
791 )));
792 }
793
794 for (bone_index, initial) in model.initial_bone_keyframe_summaries.iter().enumerate() {
795 if let Some(index) = initial.index {
796 return Err(PmmDocumentClipBuildError::new(format!(
797 "PMM bone {bone_index} initial keyframe must not have explicit index {index}"
798 )));
799 }
800 }
801 for (morph_index, initial) in model.initial_morph_keyframe_summaries.iter().enumerate() {
802 if let Some(index) = initial.index {
803 return Err(PmmDocumentClipBuildError::new(format!(
804 "PMM morph {morph_index} initial keyframe must not have explicit index {index}"
805 )));
806 }
807 }
808
809 let additional_bones = indexed_bone_keyframes(model)?;
810 let mut bone_tracks = Vec::with_capacity(model.bone_count);
811 let mut consumed_bone_keyframes = HashSet::with_capacity(additional_bones.len());
812 for (bone_index, initial) in model.initial_bone_keyframe_summaries.iter().enumerate() {
813 let summaries = collect_bone_keyframe_chain(initial, &additional_bones, bone_index)?;
814 let mut keyframes = Vec::with_capacity(summaries.len());
815 let mut frames = HashSet::with_capacity(summaries.len());
816 for summary in summaries {
817 if let Some(index) = summary.index
818 && !consumed_bone_keyframes.insert(index)
819 {
820 return Err(PmmDocumentClipBuildError::new(format!(
821 "PMM additional bone keyframe index {index} is shared by multiple tracks"
822 )));
823 }
824 let frame = pmm_frame_index(summary.frame_index, "bone", bone_index)?;
825 if !frames.insert(frame) {
826 return Err(PmmDocumentClipBuildError::new(format!(
827 "PMM bone {bone_index} has duplicate frame {frame}"
828 )));
829 }
830 let (position_interpolation, rotation_interpolation) =
831 decode_pmm_bone_interpolation(summary.interpolation);
832 let position = Vec3A::from_array(summary.translation);
833 let rotation = Quat::from_array(summary.orientation);
834 if !position.is_finite() {
835 return Err(PmmDocumentClipBuildError::new(format!(
836 "PMM bone {bone_index} frame {frame} has non-finite translation"
837 )));
838 }
839 if !rotation.is_finite() || rotation.length_squared() <= f32::EPSILON {
840 return Err(PmmDocumentClipBuildError::new(format!(
841 "PMM bone {bone_index} frame {frame} has invalid orientation"
842 )));
843 }
844 keyframes.push(MovableBoneKeyframe {
845 frame,
846 position,
847 rotation,
848 position_interpolation,
849 rotation_interpolation,
850 });
851 }
852 bone_tracks.push(BoneAnimationBinding {
853 bone: BoneIndex(bone_index as u32),
854 track: MovableBoneTrack::from_keyframes(keyframes),
855 });
856 }
857 let disconnected_bone_keyframes = additional_bones
862 .iter()
863 .filter(|(index, summary)| {
864 !consumed_bone_keyframes.contains(index)
865 && (summary.previous_keyframe_index != 0 || summary.next_keyframe_index != 0)
866 })
867 .count();
868 if disconnected_bone_keyframes != 0 {
869 return Err(PmmDocumentClipBuildError::new(format!(
870 "PMM model {} has {disconnected_bone_keyframes} disconnected additional bone keyframe(s)",
871 model.name
872 )));
873 }
874
875 let additional_morphs = indexed_morph_keyframes(model)?;
876 let mut morph_tracks = Vec::with_capacity(model.morph_count);
877 let mut consumed_morph_keyframes = HashSet::with_capacity(additional_morphs.len());
878 for (morph_index, initial) in model.initial_morph_keyframe_summaries.iter().enumerate() {
879 let summaries = collect_morph_keyframe_chain(initial, &additional_morphs, morph_index)?;
880 let mut keyframes = Vec::with_capacity(summaries.len());
881 let mut frames = HashSet::with_capacity(summaries.len());
882 for summary in summaries {
883 if let Some(index) = summary.index
884 && !consumed_morph_keyframes.insert(index)
885 {
886 return Err(PmmDocumentClipBuildError::new(format!(
887 "PMM additional morph keyframe index {index} is shared by multiple tracks"
888 )));
889 }
890 let frame = pmm_frame_index(summary.frame_index, "morph", morph_index)?;
891 if !frames.insert(frame) {
892 return Err(PmmDocumentClipBuildError::new(format!(
893 "PMM morph {morph_index} has duplicate frame {frame}"
894 )));
895 }
896 if !summary.weight.is_finite() {
897 return Err(PmmDocumentClipBuildError::new(format!(
898 "PMM morph {morph_index} frame {frame} has non-finite weight"
899 )));
900 }
901 keyframes.push(MorphKeyframe::new(frame, summary.weight));
902 }
903 morph_tracks.push(MorphAnimationBinding {
904 morph: MorphIndex(morph_index as u32),
905 track: MorphTrack::from_keyframes(keyframes),
906 });
907 }
908 let disconnected_morph_keyframes = additional_morphs
909 .iter()
910 .filter(|(index, summary)| {
911 !consumed_morph_keyframes.contains(index)
912 && (summary.previous_keyframe_index != 0 || summary.next_keyframe_index != 0)
913 })
914 .count();
915 if disconnected_morph_keyframes != 0 {
916 return Err(PmmDocumentClipBuildError::new(format!(
917 "PMM model {} has {disconnected_morph_keyframes} disconnected additional morph keyframe(s)",
918 model.name
919 )));
920 }
921 Ok(AnimationClip::new_full(bone_tracks, morph_tracks, None))
922}
923
924fn indexed_bone_keyframes(
925 model: &PmmDocumentModelSummary,
926) -> Result<HashMap<i32, &PmmDocumentBoneKeyframeSummary>, PmmDocumentClipBuildError> {
927 let mut indexed = HashMap::with_capacity(model.bone_keyframe_summaries.len());
928 for summary in &model.bone_keyframe_summaries {
929 let index = summary.index.ok_or_else(|| {
930 PmmDocumentClipBuildError::new("PMM additional bone keyframe is missing its index")
931 })?;
932 if index < 0 {
933 return Err(PmmDocumentClipBuildError::new(format!(
934 "PMM additional bone keyframe index {index} must be non-negative"
935 )));
936 }
937 if (index as usize) < model.bone_count {
938 return Err(PmmDocumentClipBuildError::new(format!(
939 "PMM additional bone keyframe index {index} overlaps initial bone track index namespace; expected index >= {}",
940 model.bone_count
941 )));
942 }
943 if indexed.insert(index, summary).is_some() {
944 return Err(PmmDocumentClipBuildError::new(format!(
945 "PMM duplicate additional bone keyframe index {index}"
946 )));
947 }
948 }
949 Ok(indexed)
950}
951
952fn indexed_morph_keyframes(
953 model: &PmmDocumentModelSummary,
954) -> Result<HashMap<i32, &PmmDocumentMorphKeyframeSummary>, PmmDocumentClipBuildError> {
955 let mut indexed = HashMap::with_capacity(model.morph_keyframe_summaries.len());
956 for summary in &model.morph_keyframe_summaries {
957 let index = summary.index.ok_or_else(|| {
958 PmmDocumentClipBuildError::new("PMM additional morph keyframe is missing its index")
959 })?;
960 if index < 0 {
961 return Err(PmmDocumentClipBuildError::new(format!(
962 "PMM additional morph keyframe index {index} must be non-negative"
963 )));
964 }
965 if (index as usize) < model.morph_count {
966 return Err(PmmDocumentClipBuildError::new(format!(
967 "PMM additional morph keyframe index {index} overlaps initial morph track index namespace; expected index >= {}",
968 model.morph_count
969 )));
970 }
971 if indexed.insert(index, summary).is_some() {
972 return Err(PmmDocumentClipBuildError::new(format!(
973 "PMM duplicate additional morph keyframe index {index}"
974 )));
975 }
976 }
977 Ok(indexed)
978}
979
980fn collect_bone_keyframe_chain<'a>(
981 initial: &'a PmmDocumentBoneKeyframeSummary,
982 indexed: &HashMap<i32, &'a PmmDocumentBoneKeyframeSummary>,
983 bone_index: usize,
984) -> Result<Vec<&'a PmmDocumentBoneKeyframeSummary>, PmmDocumentClipBuildError> {
985 let mut result = vec![initial];
986 let mut visited = HashSet::new();
987 let mut next_index = initial.next_keyframe_index;
988 let mut expected_previous_index = bone_index as i32;
989 while next_index != 0 {
990 let summary = indexed.get(&next_index).copied().ok_or_else(|| {
991 PmmDocumentClipBuildError::new(format!(
992 "PMM bone {bone_index} keyframe chain references missing index {next_index}"
993 ))
994 })?;
995 if !visited.insert(next_index) {
996 return Err(PmmDocumentClipBuildError::new(format!(
997 "PMM bone {bone_index} keyframe chain contains a cycle at index {next_index}"
998 )));
999 }
1000 if summary.previous_keyframe_index != expected_previous_index {
1001 return Err(PmmDocumentClipBuildError::new(format!(
1002 "PMM bone {bone_index} keyframe index {next_index} has previous index {}; expected {expected_previous_index}",
1003 summary.previous_keyframe_index
1004 )));
1005 }
1006 result.push(summary);
1007 expected_previous_index = next_index;
1008 next_index = summary.next_keyframe_index;
1009 }
1010 Ok(result)
1011}
1012
1013fn collect_morph_keyframe_chain<'a>(
1014 initial: &'a PmmDocumentMorphKeyframeSummary,
1015 indexed: &HashMap<i32, &'a PmmDocumentMorphKeyframeSummary>,
1016 morph_index: usize,
1017) -> Result<Vec<&'a PmmDocumentMorphKeyframeSummary>, PmmDocumentClipBuildError> {
1018 let mut result = vec![initial];
1019 let mut visited = HashSet::new();
1020 let mut next_index = initial.next_keyframe_index;
1021 let mut expected_previous_index = morph_index as i32;
1022 while next_index != 0 {
1023 let summary = indexed.get(&next_index).copied().ok_or_else(|| {
1024 PmmDocumentClipBuildError::new(format!(
1025 "PMM morph {morph_index} keyframe chain references missing index {next_index}"
1026 ))
1027 })?;
1028 if !visited.insert(next_index) {
1029 return Err(PmmDocumentClipBuildError::new(format!(
1030 "PMM morph {morph_index} keyframe chain contains a cycle at index {next_index}"
1031 )));
1032 }
1033 if summary.previous_keyframe_index != expected_previous_index {
1034 return Err(PmmDocumentClipBuildError::new(format!(
1035 "PMM morph {morph_index} keyframe index {next_index} has previous index {}; expected {expected_previous_index}",
1036 summary.previous_keyframe_index
1037 )));
1038 }
1039 result.push(summary);
1040 expected_previous_index = next_index;
1041 next_index = summary.next_keyframe_index;
1042 }
1043 Ok(result)
1044}
1045
1046fn pmm_frame_index(
1047 frame_index: i32,
1048 track_kind: &str,
1049 track_index: usize,
1050) -> Result<u32, PmmDocumentClipBuildError> {
1051 u32::try_from(frame_index).map_err(|_| {
1052 PmmDocumentClipBuildError::new(format!(
1053 "PMM {track_kind} {track_index} has negative frame index {frame_index}"
1054 ))
1055 })
1056}
1057
1058fn decode_pmm_bone_interpolation(
1059 interpolation: [u8; 16],
1060) -> (InterpolationVector3, InterpolationScalar) {
1061 fn scalar(values: &[u8]) -> InterpolationScalar {
1062 InterpolationScalar {
1063 x1: values[0].min(127),
1064 y1: values[1].min(127),
1065 x2: values[2].min(127),
1066 y2: values[3].min(127),
1067 }
1068 }
1069 (
1070 InterpolationVector3 {
1071 x: scalar(&interpolation[0..4]),
1072 y: scalar(&interpolation[4..8]),
1073 z: scalar(&interpolation[8..12]),
1074 },
1075 scalar(&interpolation[12..16]),
1076 )
1077}
1078
1079#[derive(Debug, Clone, Serialize)]
1080#[serde(rename_all = "camelCase")]
1081pub struct PmmDocumentModelSections {
1082 pub initial_bone_keyframes_offset: usize,
1083 pub bone_keyframe_count_offset: usize,
1084 pub bone_keyframes_offset: usize,
1085 pub bone_keyframes_end_offset: usize,
1086 pub initial_morph_keyframes_offset: usize,
1087 pub morph_keyframe_count_offset: usize,
1088 pub morph_keyframes_offset: usize,
1089 pub morph_keyframes_end_offset: usize,
1090 pub initial_model_keyframe_offset: usize,
1091 pub model_keyframe_count_offset: usize,
1092 pub model_keyframes_offset: usize,
1093 pub model_keyframes_end_offset: usize,
1094 pub bone_states_offset: usize,
1095 pub morph_states_offset: usize,
1096 pub constraint_states_offset: usize,
1097 pub outside_parent_states_offset: usize,
1098}
1099
1100#[derive(Debug, Clone, Serialize)]
1101#[serde(rename_all = "camelCase")]
1102pub struct PmmAssetReference {
1103 pub path: String,
1104 pub normalized_path: String,
1105 pub file_name: String,
1106 pub extension: String,
1107 pub kind: &'static str,
1108 pub offset: usize,
1109 pub offset_end: usize,
1110 pub confidence: &'static str,
1111}
1112
1113#[derive(Debug, Clone, Serialize)]
1114#[serde(rename_all = "camelCase")]
1115pub struct PmmSceneAsset {
1116 pub reference_index: usize,
1117 pub kind_index: usize,
1118 pub path: String,
1119 pub normalized_path: String,
1120 pub file_name: String,
1121 pub extension: String,
1122 pub offset: usize,
1123 pub offset_end: usize,
1124 pub confidence: &'static str,
1125}
1126
1127#[derive(Debug, Clone, Serialize)]
1128#[serde(rename_all = "camelCase")]
1129pub struct PmmParserDiagnostic {
1130 pub level: &'static str,
1131 pub code: &'static str,
1132 pub message: String,
1133}
1134
1135#[allow(clippy::too_many_arguments)]
1136fn make_keyframe_reference(
1137 scope: &'static str,
1138 track_kind: &'static str,
1139 owner_index: Option<usize>,
1140 document_index: Option<u8>,
1141 owner_name: Option<String>,
1142 initial: bool,
1143 keyframe_index: Option<i32>,
1144 frame_index: i32,
1145 previous_keyframe_index: i32,
1146 next_keyframe_index: i32,
1147 offset: usize,
1148 byte_length: usize,
1149 payload_offset: usize,
1150 payload_byte_length: usize,
1151) -> PmmProjectKeyframeReference {
1152 PmmProjectKeyframeReference {
1153 scope,
1154 track_kind,
1155 owner_index,
1156 document_index,
1157 owner_name,
1158 initial,
1159 keyframe_index,
1160 frame_index,
1161 previous_keyframe_index,
1162 next_keyframe_index,
1163 offset,
1164 byte_length,
1165 payload_offset,
1166 payload_byte_length,
1167 }
1168}
1169
1170fn make_global_track_ref(
1171 track: &PmmDocumentTrackSummary,
1172 kind: &'static str,
1173) -> PmmProjectTrackReference {
1174 let initial_kf_offset = track.initial_keyframe.as_ref().map(|kf| match kf {
1175 PmmDocumentKeyframeSummary::Camera { offset, .. } => *offset,
1176 PmmDocumentKeyframeSummary::Light { offset, .. } => *offset,
1177 PmmDocumentKeyframeSummary::Gravity { offset, .. } => *offset,
1178 PmmDocumentKeyframeSummary::SelfShadow { offset, .. } => *offset,
1179 });
1180 PmmProjectTrackReference {
1181 scope: "global",
1182 track_kind: kind,
1183 owner_index: None,
1184 document_index: None,
1185 owner_name: None,
1186 initial_keyframes: track.initial_keyframes,
1187 keyframes: track.keyframes,
1188 initial_keyframes_offset: initial_kf_offset,
1189 keyframe_count_offset: Some(track.keyframe_count_offset),
1190 keyframes_offset: track.keyframes_offset,
1191 keyframes_end_offset: track.keyframes_end_offset,
1192 state_offset: track.state_offset,
1193 state_end_offset: track.state_end_offset,
1194 }
1195}
1196
1197fn append_global_keyframe_refs(
1198 refs: &mut Vec<PmmProjectKeyframeReference>,
1199 track: &PmmDocumentTrackSummary,
1200 kind: &'static str,
1201) {
1202 if let Some(kf) = &track.initial_keyframe {
1203 let (index, frame, prev, next, off, blen, poff, plen) = match kf {
1204 PmmDocumentKeyframeSummary::Camera {
1205 index,
1206 frame_index,
1207 previous_keyframe_index,
1208 next_keyframe_index,
1209 offset,
1210 byte_length,
1211 payload_offset,
1212 payload_byte_length,
1213 ..
1214 } => (
1215 *index,
1216 *frame_index,
1217 *previous_keyframe_index,
1218 *next_keyframe_index,
1219 *offset,
1220 *byte_length,
1221 *payload_offset,
1222 *payload_byte_length,
1223 ),
1224 PmmDocumentKeyframeSummary::Light {
1225 index,
1226 frame_index,
1227 previous_keyframe_index,
1228 next_keyframe_index,
1229 offset,
1230 byte_length,
1231 payload_offset,
1232 payload_byte_length,
1233 ..
1234 } => (
1235 *index,
1236 *frame_index,
1237 *previous_keyframe_index,
1238 *next_keyframe_index,
1239 *offset,
1240 *byte_length,
1241 *payload_offset,
1242 *payload_byte_length,
1243 ),
1244 PmmDocumentKeyframeSummary::Gravity {
1245 index,
1246 frame_index,
1247 previous_keyframe_index,
1248 next_keyframe_index,
1249 offset,
1250 byte_length,
1251 payload_offset,
1252 payload_byte_length,
1253 ..
1254 } => (
1255 *index,
1256 *frame_index,
1257 *previous_keyframe_index,
1258 *next_keyframe_index,
1259 *offset,
1260 *byte_length,
1261 *payload_offset,
1262 *payload_byte_length,
1263 ),
1264 PmmDocumentKeyframeSummary::SelfShadow {
1265 index,
1266 frame_index,
1267 previous_keyframe_index,
1268 next_keyframe_index,
1269 offset,
1270 byte_length,
1271 payload_offset,
1272 payload_byte_length,
1273 ..
1274 } => (
1275 *index,
1276 *frame_index,
1277 *previous_keyframe_index,
1278 *next_keyframe_index,
1279 *offset,
1280 *byte_length,
1281 *payload_offset,
1282 *payload_byte_length,
1283 ),
1284 };
1285 refs.push(make_keyframe_reference(
1286 "global", kind, None, None, None, true, index, frame, prev, next, off, blen, poff, plen,
1287 ));
1288 }
1289 for kf in &track.keyframe_summaries {
1290 let (index, frame, prev, next, off, blen, poff, plen) = match kf {
1291 PmmDocumentKeyframeSummary::Camera {
1292 index,
1293 frame_index,
1294 previous_keyframe_index,
1295 next_keyframe_index,
1296 offset,
1297 byte_length,
1298 payload_offset,
1299 payload_byte_length,
1300 ..
1301 } => (
1302 *index,
1303 *frame_index,
1304 *previous_keyframe_index,
1305 *next_keyframe_index,
1306 *offset,
1307 *byte_length,
1308 *payload_offset,
1309 *payload_byte_length,
1310 ),
1311 PmmDocumentKeyframeSummary::Light {
1312 index,
1313 frame_index,
1314 previous_keyframe_index,
1315 next_keyframe_index,
1316 offset,
1317 byte_length,
1318 payload_offset,
1319 payload_byte_length,
1320 ..
1321 } => (
1322 *index,
1323 *frame_index,
1324 *previous_keyframe_index,
1325 *next_keyframe_index,
1326 *offset,
1327 *byte_length,
1328 *payload_offset,
1329 *payload_byte_length,
1330 ),
1331 PmmDocumentKeyframeSummary::Gravity {
1332 index,
1333 frame_index,
1334 previous_keyframe_index,
1335 next_keyframe_index,
1336 offset,
1337 byte_length,
1338 payload_offset,
1339 payload_byte_length,
1340 ..
1341 } => (
1342 *index,
1343 *frame_index,
1344 *previous_keyframe_index,
1345 *next_keyframe_index,
1346 *offset,
1347 *byte_length,
1348 *payload_offset,
1349 *payload_byte_length,
1350 ),
1351 PmmDocumentKeyframeSummary::SelfShadow {
1352 index,
1353 frame_index,
1354 previous_keyframe_index,
1355 next_keyframe_index,
1356 offset,
1357 byte_length,
1358 payload_offset,
1359 payload_byte_length,
1360 ..
1361 } => (
1362 *index,
1363 *frame_index,
1364 *previous_keyframe_index,
1365 *next_keyframe_index,
1366 *offset,
1367 *byte_length,
1368 *payload_offset,
1369 *payload_byte_length,
1370 ),
1371 };
1372 refs.push(make_keyframe_reference(
1373 "global", kind, None, None, None, false, index, frame, prev, next, off, blen, poff,
1374 plen,
1375 ));
1376 }
1377}
1378
1379fn make_global_track_byte_range(
1380 track: &PmmDocumentTrackSummary,
1381 kind: &'static str,
1382) -> PmmProjectByteRange {
1383 let byte_length = track.offset_end - track.offset;
1384 PmmProjectByteRange {
1385 scope: "global",
1386 kind,
1387 owner_index: None,
1388 document_index: None,
1389 name: None,
1390 offset: track.offset,
1391 offset_end: track.offset_end,
1392 byte_length,
1393 }
1394}
1395
1396fn build_project_track_references(
1397 doc: &PmmDocumentSummary,
1398 glob: &PmmDocumentGlobalSummary,
1399) -> Vec<PmmProjectTrackReference> {
1400 let mut track_references: Vec<PmmProjectTrackReference> = Vec::new();
1401 for model in &doc.models {
1402 let s = &model.sections;
1403 track_references.push(PmmProjectTrackReference {
1404 scope: "model",
1405 track_kind: "bone",
1406 owner_index: Some(model.slot_index),
1407 document_index: Some(model.document_model_index),
1408 owner_name: Some(model.name.clone()),
1409 initial_keyframes: model.initial_bone_keyframes,
1410 keyframes: model.bone_keyframes,
1411 initial_keyframes_offset: Some(s.initial_bone_keyframes_offset),
1412 keyframe_count_offset: Some(s.bone_keyframe_count_offset),
1413 keyframes_offset: s.bone_keyframes_offset,
1414 keyframes_end_offset: s.bone_keyframes_end_offset,
1415 state_offset: None,
1416 state_end_offset: None,
1417 });
1418 track_references.push(PmmProjectTrackReference {
1419 scope: "model",
1420 track_kind: "morph",
1421 owner_index: Some(model.slot_index),
1422 document_index: Some(model.document_model_index),
1423 owner_name: Some(model.name.clone()),
1424 initial_keyframes: model.initial_morph_keyframes,
1425 keyframes: model.morph_keyframes,
1426 initial_keyframes_offset: Some(s.initial_morph_keyframes_offset),
1427 keyframe_count_offset: Some(s.morph_keyframe_count_offset),
1428 keyframes_offset: s.morph_keyframes_offset,
1429 keyframes_end_offset: s.morph_keyframes_end_offset,
1430 state_offset: None,
1431 state_end_offset: None,
1432 });
1433 track_references.push(PmmProjectTrackReference {
1434 scope: "model",
1435 track_kind: "model",
1436 owner_index: Some(model.slot_index),
1437 document_index: Some(model.document_model_index),
1438 owner_name: Some(model.name.clone()),
1439 initial_keyframes: model.initial_model_keyframes,
1440 keyframes: model.model_keyframes,
1441 initial_keyframes_offset: Some(s.initial_model_keyframe_offset),
1442 keyframe_count_offset: Some(s.model_keyframe_count_offset),
1443 keyframes_offset: s.model_keyframes_offset,
1444 keyframes_end_offset: s.model_keyframes_end_offset,
1445 state_offset: None,
1446 state_end_offset: None,
1447 });
1448 }
1449 track_references.push(make_global_track_ref(&glob.camera, "camera"));
1450 track_references.push(make_global_track_ref(&glob.light, "light"));
1451 track_references.push(make_global_track_ref(&glob.gravity, "gravity"));
1452 track_references.push(make_global_track_ref(&glob.self_shadow, "selfShadow"));
1453 for acc in &glob.accessories.accessories {
1454 track_references.push(PmmProjectTrackReference {
1455 scope: "accessory",
1456 track_kind: "accessory",
1457 owner_index: Some(acc.slot_index),
1458 document_index: Some(acc.document_accessory_index),
1459 owner_name: Some(acc.name.clone()),
1460 initial_keyframes: 1,
1461 keyframes: acc.keyframes,
1462 initial_keyframes_offset: Some(acc.initial_keyframe.offset),
1463 keyframe_count_offset: Some(acc.keyframe_count_offset),
1464 keyframes_offset: acc.keyframes_offset,
1465 keyframes_end_offset: acc.keyframes_end_offset,
1466 state_offset: Some(acc.state_offset),
1467 state_end_offset: Some(acc.state_end_offset),
1468 });
1469 }
1470 track_references
1471}
1472
1473fn build_project_keyframe_references(
1474 doc: &PmmDocumentSummary,
1475 glob: &PmmDocumentGlobalSummary,
1476) -> Vec<PmmProjectKeyframeReference> {
1477 let mut keyframe_references: Vec<PmmProjectKeyframeReference> = Vec::new();
1480
1481 for model in &doc.models {
1483 for kf in &model.initial_bone_keyframe_summaries {
1485 keyframe_references.push(make_keyframe_reference(
1486 "model",
1487 "bone",
1488 Some(model.slot_index),
1489 Some(model.document_model_index),
1490 Some(model.name.clone()),
1491 true,
1492 kf.index,
1493 kf.frame_index,
1494 kf.previous_keyframe_index,
1495 kf.next_keyframe_index,
1496 kf.offset,
1497 kf.byte_length,
1498 kf.payload_offset,
1499 kf.payload_byte_length,
1500 ));
1501 }
1502 for kf in &model.bone_keyframe_summaries {
1503 keyframe_references.push(make_keyframe_reference(
1504 "model",
1505 "bone",
1506 Some(model.slot_index),
1507 Some(model.document_model_index),
1508 Some(model.name.clone()),
1509 false,
1510 kf.index,
1511 kf.frame_index,
1512 kf.previous_keyframe_index,
1513 kf.next_keyframe_index,
1514 kf.offset,
1515 kf.byte_length,
1516 kf.payload_offset,
1517 kf.payload_byte_length,
1518 ));
1519 }
1520 for kf in &model.initial_morph_keyframe_summaries {
1522 keyframe_references.push(make_keyframe_reference(
1523 "model",
1524 "morph",
1525 Some(model.slot_index),
1526 Some(model.document_model_index),
1527 Some(model.name.clone()),
1528 true,
1529 kf.index,
1530 kf.frame_index,
1531 kf.previous_keyframe_index,
1532 kf.next_keyframe_index,
1533 kf.offset,
1534 kf.byte_length,
1535 kf.payload_offset,
1536 kf.payload_byte_length,
1537 ));
1538 }
1539 for kf in &model.morph_keyframe_summaries {
1540 keyframe_references.push(make_keyframe_reference(
1541 "model",
1542 "morph",
1543 Some(model.slot_index),
1544 Some(model.document_model_index),
1545 Some(model.name.clone()),
1546 false,
1547 kf.index,
1548 kf.frame_index,
1549 kf.previous_keyframe_index,
1550 kf.next_keyframe_index,
1551 kf.offset,
1552 kf.byte_length,
1553 kf.payload_offset,
1554 kf.payload_byte_length,
1555 ));
1556 }
1557 keyframe_references.push(make_keyframe_reference(
1559 "model",
1560 "model",
1561 Some(model.slot_index),
1562 Some(model.document_model_index),
1563 Some(model.name.clone()),
1564 true,
1565 model.initial_model_keyframe.index,
1566 model.initial_model_keyframe.frame_index,
1567 model.initial_model_keyframe.previous_keyframe_index,
1568 model.initial_model_keyframe.next_keyframe_index,
1569 model.initial_model_keyframe.offset,
1570 model.initial_model_keyframe.byte_length,
1571 model.initial_model_keyframe.payload_offset,
1572 model.initial_model_keyframe.payload_byte_length,
1573 ));
1574 for kf in &model.model_keyframe_summaries {
1575 keyframe_references.push(make_keyframe_reference(
1576 "model",
1577 "model",
1578 Some(model.slot_index),
1579 Some(model.document_model_index),
1580 Some(model.name.clone()),
1581 false,
1582 kf.index,
1583 kf.frame_index,
1584 kf.previous_keyframe_index,
1585 kf.next_keyframe_index,
1586 kf.offset,
1587 kf.byte_length,
1588 kf.payload_offset,
1589 kf.payload_byte_length,
1590 ));
1591 }
1592 }
1593
1594 append_global_keyframe_refs(&mut keyframe_references, &glob.camera, "camera");
1597 append_global_keyframe_refs(&mut keyframe_references, &glob.light, "light");
1598 append_global_keyframe_refs(&mut keyframe_references, &glob.gravity, "gravity");
1599 append_global_keyframe_refs(&mut keyframe_references, &glob.self_shadow, "selfShadow");
1600
1601 for acc in &glob.accessories.accessories {
1603 keyframe_references.push(make_keyframe_reference(
1604 "accessory",
1605 "accessory",
1606 Some(acc.slot_index),
1607 Some(acc.document_accessory_index),
1608 Some(acc.name.clone()),
1609 true,
1610 acc.initial_keyframe.index,
1611 acc.initial_keyframe.frame_index,
1612 acc.initial_keyframe.previous_keyframe_index,
1613 acc.initial_keyframe.next_keyframe_index,
1614 acc.initial_keyframe.offset,
1615 acc.initial_keyframe.byte_length,
1616 acc.initial_keyframe.payload_offset,
1617 acc.initial_keyframe.payload_byte_length,
1618 ));
1619 for kf in &acc.keyframe_summaries {
1620 keyframe_references.push(make_keyframe_reference(
1621 "accessory",
1622 "accessory",
1623 Some(acc.slot_index),
1624 Some(acc.document_accessory_index),
1625 Some(acc.name.clone()),
1626 false,
1627 kf.index,
1628 kf.frame_index,
1629 kf.previous_keyframe_index,
1630 kf.next_keyframe_index,
1631 kf.offset,
1632 kf.byte_length,
1633 kf.payload_offset,
1634 kf.payload_byte_length,
1635 ));
1636 }
1637 }
1638
1639 keyframe_references
1640}
1641
1642fn build_project_byte_coverage(
1643 doc: &PmmDocumentSummary,
1644 glob: &PmmDocumentGlobalSummary,
1645) -> PmmProjectByteCoverage {
1646 let mut bc_ranges: Vec<PmmProjectByteRange> = Vec::new();
1652
1653 for model in &doc.models {
1655 let bl = model.offset_end - model.offset;
1656 bc_ranges.push(PmmProjectByteRange {
1657 scope: "model",
1658 kind: "model",
1659 owner_index: Some(model.slot_index),
1660 document_index: Some(model.document_model_index),
1661 name: Some(model.name.clone()),
1662 offset: model.offset,
1663 offset_end: model.offset_end,
1664 byte_length: bl,
1665 });
1666 }
1667
1668 {
1670 let off = glob.offset;
1671 let end = glob.offset_end;
1672 bc_ranges.push(PmmProjectByteRange {
1673 scope: "global",
1674 kind: "root",
1675 owner_index: None,
1676 document_index: None,
1677 name: None,
1678 offset: off,
1679 offset_end: end,
1680 byte_length: end - off,
1681 });
1682 }
1683
1684 bc_ranges.push(make_global_track_byte_range(&glob.camera, "camera"));
1686 bc_ranges.push(make_global_track_byte_range(&glob.light, "light"));
1687 bc_ranges.push(make_global_track_byte_range(&glob.gravity, "gravity"));
1688 bc_ranges.push(make_global_track_byte_range(
1689 &glob.self_shadow,
1690 "selfShadow",
1691 ));
1692
1693 {
1695 let blk = &glob.accessories;
1696 let bl = blk.offset_end - blk.offset;
1697 bc_ranges.push(PmmProjectByteRange {
1698 scope: "global",
1699 kind: "accessoryBlock",
1700 owner_index: None,
1701 document_index: None,
1702 name: None,
1703 offset: blk.offset,
1704 offset_end: blk.offset_end,
1705 byte_length: bl,
1706 });
1707 }
1708
1709 for acc in &glob.accessories.accessories {
1711 let bl = acc.offset_end - acc.offset;
1712 bc_ranges.push(PmmProjectByteRange {
1713 scope: "accessory",
1714 kind: "accessory",
1715 owner_index: Some(acc.slot_index),
1716 document_index: Some(acc.document_accessory_index),
1717 name: Some(acc.name.clone()),
1718 offset: acc.offset,
1719 offset_end: acc.offset_end,
1720 byte_length: bl,
1721 });
1722 }
1723
1724 let mut merged: Vec<(usize, usize)> =
1727 bc_ranges.iter().map(|r| (r.offset, r.offset_end)).collect();
1728 merged.sort_by_key(|&(s, _)| s);
1729 let mut merged_intervals: Vec<(usize, usize)> = Vec::new();
1730 for (start, end) in merged {
1731 if let Some(last) = merged_intervals.last_mut() {
1732 if start <= last.1 {
1733 last.1 = last.1.max(end);
1734 } else {
1735 merged_intervals.push((start, end));
1736 }
1737 } else {
1738 merged_intervals.push((start, end));
1739 }
1740 }
1741 let covered_byte_length: usize = merged_intervals.iter().map(|&(s, e)| e - s).sum();
1742
1743 let bc_offset = bc_ranges.iter().map(|r| r.offset).min().unwrap_or(0);
1744 let bc_offset_end = bc_ranges.iter().map(|r| r.offset_end).max().unwrap_or(0);
1745 let bc_byte_length = bc_offset_end - bc_offset;
1746
1747 let mut gaps: Vec<PmmProjectByteRange> = Vec::new();
1748 let mut cursor = bc_offset;
1749 for (gstart, gend) in &merged_intervals {
1750 if cursor < *gstart {
1751 let goff = cursor;
1752 let gend_ = *gstart;
1753 let glen = gend_ - goff;
1754 gaps.push(PmmProjectByteRange {
1755 scope: "unknown",
1756 kind: "gap",
1757 owner_index: None,
1758 document_index: None,
1759 name: None,
1760 offset: goff,
1761 offset_end: gend_,
1762 byte_length: glen,
1763 });
1764 }
1765 cursor = cursor.max(*gend);
1766 }
1767 if cursor < bc_offset_end {
1768 let goff = cursor;
1769 let gend_ = bc_offset_end;
1770 let glen = gend_ - goff;
1771 gaps.push(PmmProjectByteRange {
1772 scope: "unknown",
1773 kind: "gap",
1774 owner_index: None,
1775 document_index: None,
1776 name: None,
1777 offset: goff,
1778 offset_end: gend_,
1779 byte_length: glen,
1780 });
1781 }
1782
1783 let coverage_ratio = if bc_byte_length == 0 {
1784 1.0
1785 } else {
1786 covered_byte_length as f32 / bc_byte_length as f32
1787 };
1788
1789 PmmProjectByteCoverage {
1790 offset: bc_offset,
1791 offset_end: bc_offset_end,
1792 byte_length: bc_byte_length,
1793 covered_byte_length,
1794 coverage_ratio,
1795 gap_count: gaps.len(),
1796 gaps,
1797 range_count: bc_ranges.len(),
1798 ranges: bc_ranges,
1799 }
1800}
1801
1802#[allow(clippy::too_many_arguments)]
1803fn make_asset_binding(
1804 scope: &'static str,
1805 asset_kind: &'static str,
1806 owner_index: Option<usize>,
1807 document_index: Option<u8>,
1808 owner_name: Option<String>,
1809 path: String,
1810 path_offset: Option<usize>,
1811 asset_reference_index: Option<usize>,
1812 asset_references: &[PmmAssetReference],
1813) -> PmmProjectAssetBinding {
1814 let (ar_offset, ar_end, ar_conf) = match asset_reference_index {
1815 Some(idx) => asset_references.get(idx).map_or((None, None, None), |ar| {
1816 (Some(ar.offset), Some(ar.offset_end), Some(ar.confidence))
1817 }),
1818 None => (None, None, None),
1819 };
1820 PmmProjectAssetBinding {
1821 scope,
1822 asset_kind,
1823 owner_index,
1824 document_index,
1825 owner_name,
1826 path,
1827 path_offset,
1828 asset_reference_index,
1829 asset_reference_offset: ar_offset,
1830 asset_reference_end_offset: ar_end,
1831 asset_reference_confidence: ar_conf,
1832 }
1833}
1834
1835fn build_project_asset_bindings(
1836 doc: &PmmDocumentSummary,
1837 glob: &PmmDocumentGlobalSummary,
1838 asset_references: &[PmmAssetReference],
1839) -> Vec<PmmProjectAssetBinding> {
1840 let mut asset_bindings: Vec<PmmProjectAssetBinding> = Vec::new();
1841
1842 for model in &doc.models {
1843 asset_bindings.push(make_asset_binding(
1844 "model",
1845 "model",
1846 Some(model.slot_index),
1847 Some(model.document_model_index),
1848 Some(model.name.clone()),
1849 model.path.clone(),
1850 Some(model.path_offset),
1851 model.asset_reference_index,
1852 asset_references,
1853 ));
1854 }
1855 for acc in &glob.accessories.accessories {
1856 asset_bindings.push(make_asset_binding(
1857 "accessory",
1858 "accessory",
1859 Some(acc.slot_index),
1860 Some(acc.document_accessory_index),
1861 Some(acc.name.clone()),
1862 acc.path.clone(),
1863 Some(acc.path_offset),
1864 acc.asset_reference_index,
1865 asset_references,
1866 ));
1867 }
1868
1869 let settings = &glob.settings;
1870 let audio_asset_reference_index = if settings.audio_path.is_empty() {
1871 None
1872 } else {
1873 asset_reference_index_for_path(asset_references, "audio", &settings.audio_path)
1874 };
1875 let background_video_asset_reference_index = if settings.background_video_path.is_empty() {
1876 None
1877 } else {
1878 asset_reference_index_for_path(asset_references, "video", &settings.background_video_path)
1879 };
1880 let background_image_asset_reference_index = if settings.background_image_path.is_empty() {
1881 None
1882 } else {
1883 asset_reference_index_for_path(asset_references, "image", &settings.background_image_path)
1884 };
1885
1886 if !settings.audio_path.is_empty() {
1887 asset_bindings.push(make_asset_binding(
1888 "sceneSettings",
1889 "audio",
1890 None,
1891 None,
1892 None,
1893 settings.audio_path.clone(),
1894 Some(settings.audio_path_offset),
1895 audio_asset_reference_index,
1896 asset_references,
1897 ));
1898 }
1899 if !settings.background_video_path.is_empty() {
1900 asset_bindings.push(make_asset_binding(
1901 "sceneSettings",
1902 "video",
1903 None,
1904 None,
1905 None,
1906 settings.background_video_path.clone(),
1907 Some(settings.background_video_path_offset),
1908 background_video_asset_reference_index,
1909 asset_references,
1910 ));
1911 }
1912 if !settings.background_image_path.is_empty() {
1913 asset_bindings.push(make_asset_binding(
1914 "sceneSettings",
1915 "image",
1916 None,
1917 None,
1918 None,
1919 settings.background_image_path.clone(),
1920 Some(settings.background_image_path_offset),
1921 background_image_asset_reference_index,
1922 asset_references,
1923 ));
1924 }
1925
1926 asset_bindings
1927}
1928
1929fn build_project_scene_settings(
1930 glob: &PmmDocumentGlobalSummary,
1931 asset_references: &[PmmAssetReference],
1932) -> PmmProjectSceneSettings {
1933 let settings = &glob.settings;
1934 let audio_asset_reference_index = if settings.audio_path.is_empty() {
1935 None
1936 } else {
1937 asset_reference_index_for_path(asset_references, "audio", &settings.audio_path)
1938 };
1939 let background_video_asset_reference_index = if settings.background_video_path.is_empty() {
1940 None
1941 } else {
1942 asset_reference_index_for_path(asset_references, "video", &settings.background_video_path)
1943 };
1944 let background_image_asset_reference_index = if settings.background_image_path.is_empty() {
1945 None
1946 } else {
1947 asset_reference_index_for_path(asset_references, "image", &settings.background_image_path)
1948 };
1949
1950 PmmProjectSceneSettings {
1951 offset: settings.offset,
1952 offset_end: settings.offset_end,
1953 current_frame_index: settings.current_frame_index,
1954 current_frame_index_in_text_field: settings.current_frame_index_in_text_field,
1955 begin_frame_index_enabled: settings.begin_frame_index_enabled,
1956 end_frame_index_enabled: settings.end_frame_index_enabled,
1957 begin_frame_index: settings.begin_frame_index,
1958 end_frame_index: settings.end_frame_index,
1959 preferred_fps: settings.preferred_fps,
1960 loop_enabled: settings.loop_enabled,
1961 audio_enabled: settings.audio_enabled,
1962 audio_path: settings.audio_path.clone(),
1963 audio_asset_reference_index,
1964 background_video_enabled: settings.background_video_enabled,
1965 background_video_path: settings.background_video_path.clone(),
1966 background_video_asset_reference_index,
1967 background_video_offset: settings.background_video_offset,
1968 background_video_scale_factor: settings.background_video_scale_factor,
1969 background_image_enabled: settings.background_image_enabled,
1970 background_image_path: settings.background_image_path.clone(),
1971 background_image_asset_reference_index,
1972 background_image_offset: settings.background_image_offset,
1973 background_image_scale_factor: settings.background_image_scale_factor,
1974 }
1975}
1976
1977fn build_project_export_readiness(
1978 byte_coverage: &PmmProjectByteCoverage,
1979 asset_bindings: &[PmmProjectAssetBinding],
1980) -> PmmProjectExportReadiness {
1981 let mut blockers: Vec<PmmProjectExportBlocker> = Vec::new();
1982 blockers.push(PmmProjectExportBlocker {
1983 code: "PMM_SEMANTIC_EXPORTER_UNFINISHED",
1984 severity: "blocker",
1985 message: "full semantic graph export/editing is not yet supported".to_string(),
1986 scope: None,
1987 kind: None,
1988 count: None,
1989 coverage_ratio: None,
1990 });
1991 if byte_coverage.gap_count > 0 {
1992 blockers.push(PmmProjectExportBlocker {
1993 code: "PMM_DECODED_BYTE_GAPS_REMAIN",
1994 severity: "warning",
1995 message: format!(
1996 "{} decoded byte gaps remain (coverage ratio {:.4})",
1997 byte_coverage.gap_count, byte_coverage.coverage_ratio
1998 ),
1999 scope: Some("projectGraph"),
2000 kind: Some("byteCoverage"),
2001 count: Some(byte_coverage.gap_count),
2002 coverage_ratio: Some(byte_coverage.coverage_ratio),
2003 });
2004 }
2005 let unresolved_count = asset_bindings
2006 .iter()
2007 .filter(|binding| binding.asset_reference_index.is_none())
2008 .count();
2009 if unresolved_count > 0 {
2010 blockers.push(PmmProjectExportBlocker {
2011 code: "PMM_UNRESOLVED_ASSET_BINDINGS",
2012 severity: "warning",
2013 message: format!(
2014 "{} asset bindings have unresolved asset references",
2015 unresolved_count
2016 ),
2017 scope: Some("projectGraph"),
2018 kind: Some("assetBindings"),
2019 count: Some(unresolved_count),
2020 coverage_ratio: None,
2021 });
2022 }
2023 let blocker_count = blockers.len();
2024 PmmProjectExportReadiness {
2025 lossless_parsed_byte_export_supported: true,
2026 semantic_graph_export_supported: false,
2027 source_byte_preservation_required: true,
2028 blocker_count,
2029 blockers,
2030 }
2031}
2032
2033const PMM_MANIFEST_PREFIX: &[u8] = b"Polygon Movie maker ";
2034
2035fn parse_pmm_manifest_version(data: &[u8]) -> Result<(String, Option<u32>), ImportError> {
2036 if !data.starts_with(PMM_MANIFEST_PREFIX) {
2037 return Err(ImportError::InvalidMagic { format: "PMM" });
2038 }
2039 let version_bytes = &data[PMM_MANIFEST_PREFIX.len()..data.len().min(32)];
2040 let version_end = version_bytes
2041 .iter()
2042 .position(|&byte| byte == 0)
2043 .unwrap_or(version_bytes.len());
2044 let version = String::from_utf8_lossy(&version_bytes[..version_end])
2045 .trim()
2046 .to_owned();
2047 let parsed_version = version.parse::<u32>().ok();
2048 Ok((version, parsed_version))
2049}
2050
2051pub fn parse_pmm_manifest(data: &[u8]) -> Result<PmmParsedManifest, ImportError> {
2052 let (version, parsed_version) = parse_pmm_manifest_version(data)?;
2053 let project_settings = parse_project_settings(data);
2054 let display_state = parse_display_state(data, parsed_version);
2055 let asset_references = extract_asset_references(data);
2056 let model_slot_scan = parse_model_slots_from_header(data, &asset_references, &display_state);
2057 let document_summary = parse_document_summary(data, parsed_version, &asset_references);
2058 let document_global_summary =
2059 parse_document_global_summary(data, parsed_version, &asset_references);
2060 let header_text_entries =
2061 parse_header_text_entries(data, parsed_version, asset_references.first());
2062 let model_assets = scene_assets_by_kind(&asset_references, "model");
2063 let accessory_assets = scene_assets_by_kind(&asset_references, "accessory");
2064 let motion_assets = scene_assets_by_kind(&asset_references, "motion");
2065 let audio_assets = scene_assets_by_kind(&asset_references, "audio");
2066 let image_assets = scene_assets_by_kind(&asset_references, "image");
2067 let video_assets = scene_assets_by_kind(&asset_references, "video");
2068 let timeline = timeline_from_project_settings(&project_settings);
2069 let asset_summary = asset_summary(&asset_references);
2070 let diagnostics = pmm_diagnostics(
2071 &asset_references,
2072 &model_slot_scan.slots,
2073 document_summary.as_ref(),
2074 document_global_summary.as_ref(),
2075 &display_state,
2076 model_slot_scan.stop.as_ref(),
2077 data,
2078 );
2079 let project_graph = match (&document_summary, &document_global_summary) {
2080 (Some(doc), Some(glob)) => {
2081 let track_references = build_project_track_references(doc, glob);
2082 let keyframe_references = build_project_keyframe_references(doc, glob);
2083 let byte_coverage = build_project_byte_coverage(doc, glob);
2084
2085 let asset_bindings = build_project_asset_bindings(doc, glob, &asset_references);
2089 let scene_settings = build_project_scene_settings(glob, &asset_references);
2090
2091 let export_readiness = build_project_export_readiness(&byte_coverage, &asset_bindings);
2094
2095 Some(PmmProjectGraph {
2096 source: "mmd-anim-format first PMMv2 document graph DTO slice",
2097 version: version.clone(),
2098 parsed_version,
2099 timeline: timeline.clone(),
2100 display_state: display_state.clone(),
2101 asset_summary: asset_summary.clone(),
2102 asset_references: asset_references.clone(),
2103 models: doc.models.clone(),
2104 global: glob.clone(),
2105 model_counts: doc.counts.clone(),
2106 accessory_count: glob.accessories.accessory_count,
2107 accessory_keyframes: glob.accessories.keyframes,
2108 track_references,
2109 keyframe_references,
2110 byte_coverage,
2111 scene_settings,
2112 asset_bindings,
2113 export_readiness,
2114 })
2115 }
2116 _ => None,
2117 };
2118 Ok(PmmParsedManifest {
2119 signature: "Polygon Movie maker",
2120 version,
2121 parsed_version,
2122 byte_length: data.len(),
2123 timeline,
2124 project_settings,
2125 display_state,
2126 header_text_entries,
2127 model_slots: model_slot_scan.slots,
2128 document_summary,
2129 document_global_summary,
2130 project_graph,
2131 asset_summary,
2132 model_assets,
2133 accessory_assets,
2134 motion_assets,
2135 audio_assets,
2136 image_assets,
2137 video_assets,
2138 model_paths: paths_by_kind(&asset_references, "model"),
2139 accessory_paths: paths_by_kind(&asset_references, "accessory"),
2140 motion_paths: paths_by_kind(&asset_references, "motion"),
2141 audio_paths: paths_by_kind(&asset_references, "audio"),
2142 image_paths: paths_by_kind(&asset_references, "image"),
2143 video_paths: paths_by_kind(&asset_references, "video"),
2144 diagnostics,
2145 asset_references,
2146 source_bytes: data.to_vec(),
2147 })
2148}
2149
2150pub fn export_pmm_manifest(manifest: &PmmParsedManifest) -> Vec<u8> {
2151 if !manifest.source_bytes.is_empty() {
2152 return manifest.source_bytes.clone();
2153 }
2154 let mut out = Vec::new();
2155 out.extend_from_slice(b"Polygon Movie maker 0002");
2156 out.resize(30, 0);
2157
2158 push_u32(
2159 &mut out,
2160 manifest.project_settings.screen_width.unwrap_or(640),
2161 );
2162 push_u32(
2163 &mut out,
2164 manifest.project_settings.screen_height.unwrap_or(480),
2165 );
2166 push_u32(
2167 &mut out,
2168 manifest
2169 .project_settings
2170 .timeline_frame_count
2171 .or(manifest.timeline.frame_count)
2172 .unwrap_or(0),
2173 );
2174 push_f32(
2175 &mut out,
2176 manifest.project_settings.frame_rate.unwrap_or(30.0),
2177 );
2178
2179 let mut flags = [0u8; 8];
2180 for (index, flag) in manifest
2181 .display_state
2182 .model_slot_flags
2183 .iter()
2184 .take(8)
2185 .enumerate()
2186 {
2187 flags[index] = *flag;
2188 }
2189 if flags.iter().all(|flag| *flag == 0) && !manifest.model_slots.is_empty() {
2190 for slot in &manifest.model_slots {
2191 if slot.slot_index < flags.len() {
2192 flags[slot.slot_index] = 1;
2193 }
2194 }
2195 }
2196 out.extend_from_slice(&flags);
2197
2198 let document_model_count = manifest
2199 .display_state
2200 .document_model_count
2201 .unwrap_or_else(|| manifest.model_slots.len().min(u8::MAX as usize) as u8);
2202 out.push(document_model_count);
2203 out.push(manifest.display_state.accessory_slot_count.unwrap_or(0));
2204
2205 let mut emitted_paths = Vec::<String>::new();
2206 for slot in &manifest.model_slots {
2207 push_pmm_len_prefixed_sjis(&mut out, &slot.name, &slot.name_bytes);
2208 push_pmm_len_prefixed_sjis(&mut out, &slot.english_name, &slot.english_name_bytes);
2209 push_pmm_sjis_string(&mut out, &slot.model_path, None);
2210 out.push(0);
2211 emitted_paths.push(slot.normalized_path.to_ascii_lowercase());
2212 }
2213
2214 for reference in &manifest.asset_references {
2215 let normalized = reference.normalized_path.to_ascii_lowercase();
2216 if emitted_paths.iter().any(|path| path == &normalized) {
2217 continue;
2218 }
2219 push_pmm_sjis_string(&mut out, &reference.path, None);
2220 out.push(0);
2221 emitted_paths.push(normalized);
2222 }
2223
2224 out
2225}
2226
2227pub fn export_pmm_manifest_with_document_model_path_overrides(
2233 manifest: &PmmParsedManifest,
2234 overrides: &[(u8, &str)],
2235) -> Result<Vec<u8>, String> {
2236 if manifest.source_bytes.is_empty() {
2237 return Err(
2238 "parsed source bytes are required for document model path override export".to_owned(),
2239 );
2240 }
2241 let document = manifest.document_summary.as_ref().ok_or_else(|| {
2242 "PMMv2 document summary is required for document model path override export".to_owned()
2243 })?;
2244
2245 let mut patched = manifest.source_bytes.clone();
2246
2247 for &(document_model_index, new_path) in overrides {
2248 let matching: Vec<_> = document
2249 .models
2250 .iter()
2251 .filter(|m| m.document_model_index == document_model_index)
2252 .collect();
2253 if matching.len() != 1 {
2254 return Err(format!(
2255 "exactly one PmmDocumentModelSummary with document_model_index {} is required (found {})",
2256 document_model_index,
2257 matching.len()
2258 ));
2259 }
2260 let model = matching[0];
2261 let start = model.path_offset;
2262 let end = start + 256;
2263 if end > patched.len() {
2264 return Err("document model path field exceeds source byte length".to_owned());
2265 }
2266 let field = &mut patched[start..end];
2267 write_pmm_fixed_sjis_to_slice(field, new_path)?;
2268 }
2269
2270 Ok(patched)
2271}
2272
2273#[derive(Debug, Clone, Default)]
2280pub struct PmmSceneFrameRangePatch {
2281 pub current_frame_index: Option<i32>,
2282 pub current_frame_index_in_text_field: Option<i32>,
2283 pub begin_frame_index_enabled: Option<bool>,
2284 pub end_frame_index_enabled: Option<bool>,
2285 pub begin_frame_index: Option<i32>,
2286 pub end_frame_index: Option<i32>,
2287}
2288
2289pub fn export_pmm_manifest_with_scene_frame_range_patch(
2290 manifest: &PmmParsedManifest,
2291 patch: &PmmSceneFrameRangePatch,
2292) -> Result<Vec<u8>, String> {
2293 if manifest.source_bytes.is_empty() {
2294 return Err(
2295 "parsed source bytes are required for scene frame range patch export".to_owned(),
2296 );
2297 }
2298 let global = manifest.document_global_summary.as_ref().ok_or_else(|| {
2299 "PMMv2 global/document settings summary is required for scene frame range patch export"
2300 .to_owned()
2301 })?;
2302 let settings = &global.settings;
2303
2304 let mut patched = manifest.source_bytes.clone();
2305
2306 if let Some(v) = patch.current_frame_index {
2307 let off = settings.current_frame_index_offset;
2308 if off + 4 > patched.len() {
2309 return Err("current_frame_index field offset exceeds source byte length".to_owned());
2310 }
2311 patched[off..off + 4].copy_from_slice(&v.to_le_bytes());
2312 }
2313 if let Some(v) = patch.current_frame_index_in_text_field {
2314 let off = settings.current_frame_index_in_text_field_offset;
2315 if off + 4 > patched.len() {
2316 return Err(
2317 "current_frame_index_in_text_field field offset exceeds source byte length"
2318 .to_owned(),
2319 );
2320 }
2321 patched[off..off + 4].copy_from_slice(&v.to_le_bytes());
2322 }
2323 if let Some(v) = patch.begin_frame_index_enabled {
2324 let off = settings.begin_frame_index_enabled_offset;
2325 if off >= patched.len() {
2326 return Err(
2327 "begin_frame_index_enabled field offset exceeds source byte length".to_owned(),
2328 );
2329 }
2330 patched[off] = if v { 1 } else { 0 };
2331 }
2332 if let Some(v) = patch.end_frame_index_enabled {
2333 let off = settings.end_frame_index_enabled_offset;
2334 if off >= patched.len() {
2335 return Err(
2336 "end_frame_index_enabled field offset exceeds source byte length".to_owned(),
2337 );
2338 }
2339 patched[off] = if v { 1 } else { 0 };
2340 }
2341 if let Some(v) = patch.begin_frame_index {
2342 let off = settings.begin_frame_index_offset;
2343 if off + 4 > patched.len() {
2344 return Err("begin_frame_index field offset exceeds source byte length".to_owned());
2345 }
2346 patched[off..off + 4].copy_from_slice(&v.to_le_bytes());
2347 }
2348 if let Some(v) = patch.end_frame_index {
2349 let off = settings.end_frame_index_offset;
2350 if off + 4 > patched.len() {
2351 return Err("end_frame_index field offset exceeds source byte length".to_owned());
2352 }
2353 patched[off..off + 4].copy_from_slice(&v.to_le_bytes());
2354 }
2355
2356 Ok(patched)
2357}
2358
2359#[derive(Debug, Clone, Default)]
2367pub struct PmmSceneMediaPathPatch {
2368 pub audio_path: Option<String>,
2369 pub background_video_path: Option<String>,
2370 pub background_image_path: Option<String>,
2371}
2372
2373pub fn export_pmm_manifest_with_scene_media_path_patch(
2374 manifest: &PmmParsedManifest,
2375 patch: &PmmSceneMediaPathPatch,
2376) -> Result<Vec<u8>, String> {
2377 if manifest.source_bytes.is_empty() {
2378 return Err(
2379 "parsed source bytes are required for scene media path patch export".to_owned(),
2380 );
2381 }
2382 let global = manifest.document_global_summary.as_ref().ok_or_else(|| {
2383 "PMMv2 global/document settings summary is required for scene media path patch export"
2384 .to_owned()
2385 })?;
2386 let settings = &global.settings;
2387
2388 let mut patched = manifest.source_bytes.clone();
2389
2390 if let Some(ref p) = patch.audio_path {
2391 let off = settings.audio_path_offset;
2392 if off + 256 > patched.len() {
2393 return Err("audio path field offset exceeds source byte length".to_owned());
2394 }
2395 let field = &mut patched[off..off + 256];
2396 write_pmm_fixed_sjis_to_slice(field, p)?;
2397 }
2398 if let Some(ref p) = patch.background_video_path {
2399 let off = settings.background_video_path_offset;
2400 if off + 256 > patched.len() {
2401 return Err("background video path field offset exceeds source byte length".to_owned());
2402 }
2403 let field = &mut patched[off..off + 256];
2404 write_pmm_fixed_sjis_to_slice(field, p)?;
2405 }
2406 if let Some(ref p) = patch.background_image_path {
2407 let off = settings.background_image_path_offset;
2408 if off + 256 > patched.len() {
2409 return Err("background image path field offset exceeds source byte length".to_owned());
2410 }
2411 let field = &mut patched[off..off + 256];
2412 write_pmm_fixed_sjis_to_slice(field, p)?;
2413 }
2414
2415 Ok(patched)
2416}
2417
2418#[derive(Debug, Clone)]
2419pub struct PmmSceneExportOptions {
2420 pub screen_width: u32,
2421 pub screen_height: u32,
2422 pub frame_rate: f32,
2423 pub camera_fov: f32,
2424}
2425
2426impl Default for PmmSceneExportOptions {
2427 fn default() -> Self {
2428 Self {
2429 screen_width: 1024,
2430 screen_height: 1024,
2431 frame_rate: 30.0,
2432 camera_fov: 30.0,
2433 }
2434 }
2435}
2436
2437#[derive(Debug, Clone)]
2438pub struct PmmSceneExportReport {
2439 pub bytes: Vec<u8>,
2440 pub bone_keyframes: usize,
2441 pub morph_keyframes: usize,
2442 pub frame_zero_bone_keyframes: usize,
2443 pub frame_zero_morph_keyframes: usize,
2444 pub skipped_bone_keyframes: usize,
2445 pub skipped_morph_keyframes: usize,
2446 pub max_frame: u32,
2447}
2448
2449#[derive(Debug, Clone)]
2450struct PmmExportBoneKeyframe {
2451 bone_index: usize,
2452 frame: u32,
2453 translation: [f32; 3],
2454 rotation: [f32; 4],
2455 interpolation: [u8; 16],
2456}
2457
2458#[derive(Debug, Clone)]
2459struct PmmExportMorphKeyframe {
2460 morph_index: usize,
2461 frame: u32,
2462 weight: f32,
2463}
2464
2465pub fn export_pmm_scene_from_pmx_vmd(
2466 model: &PmxParsedModel,
2467 motion: &VmdParsedAnimation,
2468 model_path: &str,
2469 options: &PmmSceneExportOptions,
2470) -> PmmSceneExportReport {
2471 let bone_names: Vec<&str> = model
2472 .skeleton
2473 .bones
2474 .iter()
2475 .map(|bone| bone.name.as_str())
2476 .collect();
2477 let morph_names: Vec<&str> = model
2478 .morphs
2479 .iter()
2480 .map(|morph| morph.name.as_str())
2481 .collect();
2482 let bone_indices: HashMap<&str, usize> = bone_names
2483 .iter()
2484 .enumerate()
2485 .map(|(index, name)| (*name, index))
2486 .collect();
2487 let morph_indices: HashMap<&str, usize> = morph_names
2488 .iter()
2489 .enumerate()
2490 .map(|(index, name)| (*name, index))
2491 .collect();
2492
2493 let mut deduped_bones = BTreeMap::<(usize, u32), &VmdParsedBoneFrame>::new();
2494 let mut skipped_bone_keyframes = 0usize;
2495 for frame in &motion.bone_frames {
2496 if let Some(&bone_index) = bone_indices.get(frame.bone_name.as_str()) {
2497 deduped_bones.insert((bone_index, frame.frame), frame);
2498 } else {
2499 skipped_bone_keyframes += 1;
2500 }
2501 }
2502
2503 let mut deduped_morphs = BTreeMap::<(usize, u32), &VmdParsedMorphFrame>::new();
2504 let mut skipped_morph_keyframes = 0usize;
2505 for frame in &motion.morph_frames {
2506 if let Some(&morph_index) = morph_indices.get(frame.morph_name.as_str()) {
2507 deduped_morphs.insert((morph_index, frame.frame), frame);
2508 } else {
2509 skipped_morph_keyframes += 1;
2510 }
2511 }
2512
2513 let mut initial_bones = vec![None::<PmmExportBoneKeyframe>; bone_names.len()];
2514 let mut additional_bones = Vec::<PmmExportBoneKeyframe>::new();
2515 let mut max_frame = 0u32;
2516 for ((bone_index, frame_index), frame) in deduped_bones {
2517 max_frame = max_frame.max(frame_index);
2518 let keyframe = pmm_export_bone_keyframe(bone_index, frame);
2519 if frame_index == 0 {
2520 initial_bones[bone_index] = Some(keyframe);
2521 } else {
2522 additional_bones.push(keyframe);
2523 }
2524 }
2525 additional_bones.sort_by_key(|frame| (frame.bone_index, frame.frame));
2526
2527 let mut initial_morphs = vec![None::<PmmExportMorphKeyframe>; morph_names.len()];
2528 let mut additional_morphs = Vec::<PmmExportMorphKeyframe>::new();
2529 for ((morph_index, frame_index), frame) in deduped_morphs {
2530 max_frame = max_frame.max(frame_index);
2531 let keyframe = PmmExportMorphKeyframe {
2532 morph_index,
2533 frame: frame.frame,
2534 weight: frame.weight,
2535 };
2536 if frame_index == 0 {
2537 initial_morphs[morph_index] = Some(keyframe);
2538 } else {
2539 additional_morphs.push(keyframe);
2540 }
2541 }
2542 additional_morphs.sort_by_key(|frame| (frame.morph_index, frame.frame));
2543
2544 let frame_zero_bone_keyframes = initial_bones.iter().filter(|frame| frame.is_some()).count();
2545 let frame_zero_morph_keyframes = initial_morphs
2546 .iter()
2547 .filter(|frame| frame.is_some())
2548 .count();
2549 let bytes = write_pmm_scene(
2550 model,
2551 model_path,
2552 options,
2553 &bone_names,
2554 &morph_names,
2555 &initial_bones,
2556 &additional_bones,
2557 &initial_morphs,
2558 &additional_morphs,
2559 max_frame,
2560 );
2561
2562 PmmSceneExportReport {
2563 bytes,
2564 bone_keyframes: additional_bones.len(),
2565 morph_keyframes: additional_morphs.len(),
2566 frame_zero_bone_keyframes,
2567 frame_zero_morph_keyframes,
2568 skipped_bone_keyframes,
2569 skipped_morph_keyframes,
2570 max_frame,
2571 }
2572}
2573
2574fn pmm_export_bone_keyframe(
2575 bone_index: usize,
2576 frame: &VmdParsedBoneFrame,
2577) -> PmmExportBoneKeyframe {
2578 PmmExportBoneKeyframe {
2579 bone_index,
2580 frame: frame.frame,
2581 translation: frame.translation,
2582 rotation: frame.rotation,
2583 interpolation: pmm_bone_interpolation_from_vmd(&frame.interpolation),
2584 }
2585}
2586
2587fn pmm_bone_interpolation_from_vmd(interpolation: &[u8]) -> [u8; 16] {
2588 if interpolation.len() < 16 {
2589 return [20; 16];
2590 }
2591 [
2592 interpolation[0],
2593 interpolation[4],
2594 interpolation[8],
2595 interpolation[12],
2596 interpolation[1],
2597 interpolation[5],
2598 interpolation[9],
2599 interpolation[13],
2600 interpolation[2],
2601 interpolation[6],
2602 interpolation[10],
2603 interpolation[14],
2604 interpolation[3],
2605 interpolation[7],
2606 interpolation[11],
2607 interpolation[15],
2608 ]
2609}
2610
2611const PMM_MODEL_NAME_MAX_BYTES: usize = 19;
2613
2614pub fn normalize_pmm_model_item_name(name: &str) -> String {
2620 decode_sjis(&encode_sjis_prefix_fit(name, PMM_MODEL_NAME_MAX_BYTES))
2621}
2622
2623struct PmmExportModelStructure {
2624 fixed_tracks: u8,
2625 constraint_bone_indices: Vec<i32>,
2626 outside_parent_subject_bone_indices: Vec<i32>,
2627}
2628
2629fn pmm_export_model_structure_from_pmx(model: &PmxParsedModel) -> PmmExportModelStructure {
2630 let constraint_bone_indices = model
2631 .skeleton
2632 .bones
2633 .iter()
2634 .enumerate()
2635 .filter(|(_, bone)| bone.flags.ik)
2636 .map(|(index, _)| index as i32)
2637 .collect();
2638 let outside_parent_subject_bone_indices = std::iter::once(-1)
2639 .chain(
2640 model
2641 .skeleton
2642 .bones
2643 .iter()
2644 .enumerate()
2645 .filter(|(_, bone)| pmm_is_outside_parent_subject_bone(bone))
2646 .map(|(index, _)| index as i32),
2647 )
2648 .collect();
2649 let fixed_tracks = if model.display_frames.is_empty() {
2650 0
2651 } else {
2652 (model.display_frames.len() + 1).min(255) as u8
2653 };
2654 PmmExportModelStructure {
2655 fixed_tracks,
2656 constraint_bone_indices,
2657 outside_parent_subject_bone_indices,
2658 }
2659}
2660
2661fn pmm_is_outside_parent_subject_bone(bone: &crate::pmx::PmxParsedBone) -> bool {
2662 bone.flags.ik || (bone.flags.visible && bone.flags.translatable)
2663}
2664
2665fn push_document_outside_parent_current_state(out: &mut Vec<u8>) {
2666 push_i32(out, 0);
2667 push_i32(out, 0);
2668 push_i32(out, -1);
2669 push_i32(out, 0);
2670}
2671
2672#[allow(clippy::too_many_arguments)]
2673fn write_pmm_scene(
2674 model: &PmxParsedModel,
2675 model_path: &str,
2676 options: &PmmSceneExportOptions,
2677 bone_names: &[&str],
2678 morph_names: &[&str],
2679 initial_bones: &[Option<PmmExportBoneKeyframe>],
2680 additional_bones: &[PmmExportBoneKeyframe],
2681 initial_morphs: &[Option<PmmExportMorphKeyframe>],
2682 additional_morphs: &[PmmExportMorphKeyframe],
2683 max_frame: u32,
2684) -> Vec<u8> {
2685 let model_structure = pmm_export_model_structure_from_pmx(model);
2686 let constraint_count = model_structure.constraint_bone_indices.len();
2687 let outside_parent_count = model_structure.outside_parent_subject_bone_indices.len();
2688 let constraint_states = vec![true; constraint_count];
2689 let outside_parent_keyframe_indices = vec![(-1, 0); outside_parent_count];
2690
2691 let mut out = b"Polygon Movie maker 0002".to_vec();
2692 out.resize(30, 0);
2693 push_u32(&mut out, options.screen_width);
2694 push_u32(&mut out, options.screen_height);
2695 push_u32(&mut out, max_frame);
2696 push_f32(&mut out, options.frame_rate);
2697 out.extend_from_slice(&[0, 1, 1, 1, 1, 1, 1]);
2698 out.push(0);
2699 out.push(1);
2700
2701 out.push(0);
2702 push_pmm_len_prefixed_sjis(&mut out, &model.metadata.name, &[]);
2703 push_pmm_len_prefixed_sjis(&mut out, &model.metadata.english_name, &[]);
2704 push_pmm_fixed_sjis(&mut out, model_path, 256);
2705 out.push(model_structure.fixed_tracks);
2706
2707 push_i32(&mut out, bone_names.len() as i32);
2711 for name in bone_names {
2712 let fitted = encode_sjis_prefix_fit(name, PMM_MODEL_NAME_MAX_BYTES);
2713 push_pmm_len_prefixed_sjis(&mut out, name, &fitted);
2714 }
2715 push_i32(&mut out, morph_names.len() as i32);
2716 for name in morph_names {
2717 let fitted = encode_sjis_prefix_fit(name, PMM_MODEL_NAME_MAX_BYTES);
2718 push_pmm_len_prefixed_sjis(&mut out, name, &fitted);
2719 }
2720
2721 push_i32(&mut out, constraint_count as i32);
2722 for index in &model_structure.constraint_bone_indices {
2723 push_i32(&mut out, *index);
2724 }
2725 push_i32(&mut out, outside_parent_count as i32);
2726 for index in &model_structure.outside_parent_subject_bone_indices {
2727 push_i32(&mut out, *index);
2728 }
2729 out.push(1); out.push(1);
2731 push_i32(&mut out, -1);
2732 for _ in 0..4 {
2733 push_i32(&mut out, -1);
2734 }
2735 out.push(0);
2736 push_i32(&mut out, 0);
2737 push_i32(&mut out, max_frame as i32);
2738
2739 let bone_next =
2740 next_keyframe_indices(bone_names.len(), bone_names.len(), additional_bones, |f| {
2741 f.bone_index
2742 });
2743 for bone_index in 0..bone_names.len() {
2744 let fallback = PmmExportBoneKeyframe {
2745 bone_index,
2746 frame: 0,
2747 translation: [0.0; 3],
2748 rotation: [0.0, 0.0, 0.0, 1.0],
2749 interpolation: [20; 16],
2750 };
2751 let frame = initial_bones[bone_index].as_ref().unwrap_or(&fallback);
2752 push_document_bone_keyframe(
2753 &mut out,
2754 None,
2755 0,
2756 0,
2757 bone_next[bone_index],
2758 frame.interpolation,
2759 frame.translation,
2760 frame.rotation,
2761 );
2762 }
2763 push_i32(&mut out, additional_bones.len() as i32);
2764 for (offset, frame) in additional_bones.iter().enumerate() {
2765 let index = bone_names.len() + offset;
2766 let previous = previous_keyframe_index(
2767 additional_bones,
2768 offset,
2769 frame.bone_index,
2770 bone_names.len(),
2771 |f| f.bone_index,
2772 );
2773 let next = next_keyframe_index(
2774 additional_bones,
2775 offset,
2776 frame.bone_index,
2777 bone_names.len(),
2778 |f| f.bone_index,
2779 );
2780 push_document_bone_keyframe(
2781 &mut out,
2782 Some(index as i32),
2783 frame.frame as i32,
2784 previous,
2785 next,
2786 frame.interpolation,
2787 frame.translation,
2788 frame.rotation,
2789 );
2790 }
2791
2792 let morph_next = next_keyframe_indices(
2793 morph_names.len(),
2794 morph_names.len(),
2795 additional_morphs,
2796 |f| f.morph_index,
2797 );
2798 for morph_index in 0..morph_names.len() {
2799 let weight = initial_morphs[morph_index]
2800 .as_ref()
2801 .map(|frame| frame.weight)
2802 .unwrap_or(0.0);
2803 push_document_morph_keyframe(&mut out, None, 0, 0, morph_next[morph_index], weight);
2804 }
2805 push_i32(&mut out, additional_morphs.len() as i32);
2806 for (offset, frame) in additional_morphs.iter().enumerate() {
2807 let index = morph_names.len() + offset;
2808 let previous = previous_keyframe_index(
2809 additional_morphs,
2810 offset,
2811 frame.morph_index,
2812 morph_names.len(),
2813 |f| f.morph_index,
2814 );
2815 let next = next_keyframe_index(
2816 additional_morphs,
2817 offset,
2818 frame.morph_index,
2819 morph_names.len(),
2820 |f| f.morph_index,
2821 );
2822 push_document_morph_keyframe(
2823 &mut out,
2824 Some(index as i32),
2825 frame.frame as i32,
2826 previous,
2827 next,
2828 frame.weight,
2829 );
2830 }
2831
2832 push_document_model_keyframe(
2833 &mut out,
2834 None,
2835 0,
2836 0,
2837 0,
2838 true,
2839 &constraint_states,
2840 &outside_parent_keyframe_indices,
2841 false,
2842 );
2843 push_i32(&mut out, 0);
2844
2845 for _ in bone_names {
2846 for value in [0.0f32, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0] {
2847 push_f32(&mut out, value);
2848 }
2849 out.push(0);
2850 out.push(0);
2851 out.push(0);
2852 }
2853 for _ in morph_names {
2854 push_f32(&mut out, 0.0);
2855 }
2856
2857 out.resize(out.len() + constraint_count, 1);
2859 for _ in 0..outside_parent_count {
2860 push_document_outside_parent_current_state(&mut out);
2861 }
2862
2863 out.push(0);
2864 push_f32(&mut out, 1.0);
2865 out.push(0);
2866 out.push(1); write_pmm_global_tail(&mut out, max_frame, options.camera_fov);
2869 push_pmm_sjis_string(&mut out, model_path, None);
2870 out.push(0);
2871 out
2872}
2873
2874fn next_keyframe_indices<T>(
2875 count: usize,
2876 initial_count: usize,
2877 frames: &[T],
2878 object_index: impl Fn(&T) -> usize,
2879) -> Vec<i32> {
2880 let mut next = vec![0; count];
2881 for (offset, frame) in frames.iter().enumerate() {
2882 let target = object_index(frame);
2883 if target < count && next[target] == 0 {
2884 next[target] = (initial_count + offset) as i32;
2885 }
2886 }
2887 next
2888}
2889
2890fn previous_keyframe_index<T>(
2891 frames: &[T],
2892 offset: usize,
2893 object_index: usize,
2894 initial_count: usize,
2895 frame_object_index: impl Fn(&T) -> usize,
2896) -> i32 {
2897 if offset > 0 && frame_object_index(&frames[offset - 1]) == object_index {
2898 (initial_count + offset - 1) as i32
2899 } else {
2900 object_index as i32
2901 }
2902}
2903
2904fn next_keyframe_index<T>(
2905 frames: &[T],
2906 offset: usize,
2907 object_index: usize,
2908 initial_count: usize,
2909 frame_object_index: impl Fn(&T) -> usize,
2910) -> i32 {
2911 if offset + 1 < frames.len() && frame_object_index(&frames[offset + 1]) == object_index {
2912 (initial_count + offset + 1) as i32
2913 } else {
2914 0
2915 }
2916}
2917
2918fn push_pmm_fixed_sjis(out: &mut Vec<u8>, text: &str, length: usize) {
2919 let encoded = encode_sjis(text);
2920 let mut bytes = vec![0u8; length];
2921 let copy_len = encoded.len().min(length);
2922 bytes[..copy_len].copy_from_slice(&encoded[..copy_len]);
2923 out.extend_from_slice(&bytes);
2924}
2925
2926fn write_pmm_fixed_sjis_to_slice(dest: &mut [u8], text: &str) -> Result<(), String> {
2930 let encoded = encode_sjis(text);
2931 if encoded.len() >= dest.len() {
2932 return Err("encoded Shift-JIS path must leave room for NUL terminator".to_owned());
2933 }
2934 dest.fill(0);
2935 let n = encoded.len();
2936 dest[..n].copy_from_slice(&encoded[..n]);
2937 Ok(())
2938}
2939
2940fn push_empty_pmm_path(out: &mut Vec<u8>) {
2941 out.extend_from_slice(&[0u8; 256]);
2942}
2943
2944#[allow(clippy::too_many_arguments)]
2945fn push_document_bone_keyframe(
2946 out: &mut Vec<u8>,
2947 index: Option<i32>,
2948 frame: i32,
2949 previous: i32,
2950 next: i32,
2951 interpolation: [u8; 16],
2952 translation: [f32; 3],
2953 rotation: [f32; 4],
2954) {
2955 if let Some(index) = index {
2956 push_i32(out, index);
2957 }
2958 push_i32(out, frame);
2959 push_i32(out, previous);
2960 push_i32(out, next);
2961 out.extend_from_slice(&interpolation);
2962 for value in translation {
2963 push_f32(out, value);
2964 }
2965 for value in rotation {
2966 push_f32(out, value);
2967 }
2968 out.push(0);
2969 out.push(0);
2970}
2971
2972fn push_document_morph_keyframe(
2973 out: &mut Vec<u8>,
2974 index: Option<i32>,
2975 frame: i32,
2976 previous: i32,
2977 next: i32,
2978 weight: f32,
2979) {
2980 if let Some(index) = index {
2981 push_i32(out, index);
2982 }
2983 push_i32(out, frame);
2984 push_i32(out, previous);
2985 push_i32(out, next);
2986 push_f32(out, weight);
2987 out.push(0);
2988}
2989
2990#[allow(clippy::too_many_arguments)]
2991fn push_document_model_keyframe(
2992 out: &mut Vec<u8>,
2993 index: Option<i32>,
2994 frame: i32,
2995 previous: i32,
2996 next: i32,
2997 visible: bool,
2998 constraint_states: &[bool],
2999 outside_parent_indices: &[(i32, i32)],
3000 self_shadow_enabled: bool,
3001) {
3002 if let Some(index) = index {
3003 push_i32(out, index);
3004 }
3005 push_i32(out, frame);
3006 push_i32(out, previous);
3007 push_i32(out, next);
3008 out.push(u8::from(visible));
3009 for &enabled in constraint_states {
3010 out.push(u8::from(enabled));
3011 }
3012 for &(parent_model_index, parent_model_bone_index) in outside_parent_indices {
3013 push_i32(out, parent_model_index);
3014 push_i32(out, parent_model_bone_index);
3015 }
3016 out.push(u8::from(self_shadow_enabled));
3017}
3018
3019fn push_document_base_keyframe(
3020 out: &mut Vec<u8>,
3021 index: Option<i32>,
3022 frame: i32,
3023 previous: i32,
3024 next: i32,
3025) {
3026 if let Some(index) = index {
3027 push_i32(out, index);
3028 }
3029 push_i32(out, frame);
3030 push_i32(out, previous);
3031 push_i32(out, next);
3032}
3033
3034fn push_document_camera_keyframe(out: &mut Vec<u8>, index: Option<i32>, selected: bool, fov: i32) {
3035 push_document_base_keyframe(out, index, 0, 0, 0);
3036 push_f32(out, 45.0);
3037 out.extend_from_slice(&[0u8; 12]);
3038 out.extend_from_slice(&[0u8; 12]);
3039 push_i32(out, -1);
3040 push_i32(out, -1);
3041 out.extend_from_slice(&[20u8; 24]);
3042 out.push(0);
3043 push_i32(out, fov);
3044 out.push(u8::from(selected));
3045}
3046
3047fn push_document_light_keyframe(out: &mut Vec<u8>, index: Option<i32>, selected: bool) {
3048 push_document_base_keyframe(out, index, 0, 0, 0);
3049 for value in [0.602f32, 0.602, 0.602, -0.5, -1.0, 0.5] {
3050 push_f32(out, value);
3051 }
3052 out.push(u8::from(selected));
3053}
3054
3055fn push_document_gravity_keyframe(out: &mut Vec<u8>, index: Option<i32>, selected: bool) {
3056 push_document_base_keyframe(out, index, 0, 0, 0);
3057 out.push(0);
3058 push_i32(out, 10);
3059 push_f32(out, 9.8);
3060 for value in [0.0f32, -1.0, 0.0] {
3061 push_f32(out, value);
3062 }
3063 out.push(u8::from(selected));
3064}
3065
3066fn push_document_self_shadow_keyframe(out: &mut Vec<u8>, index: Option<i32>, selected: bool) {
3067 push_document_base_keyframe(out, index, 0, 0, 0);
3068 out.push(1);
3069 push_f32(out, 0.01125);
3070 out.push(u8::from(selected));
3071}
3072
3073fn write_pmm_global_tail(out: &mut Vec<u8>, max_frame: u32, camera_fov: f32) {
3074 let camera_fov = camera_fov.round().clamp(1.0, i32::MAX as f32) as i32;
3075 push_document_camera_keyframe(out, None, true, camera_fov);
3076 push_i32(out, 0);
3077 out.extend_from_slice(&[0u8; 12 * 3]);
3078 out.push(0);
3079
3080 push_document_light_keyframe(out, None, true);
3081 push_i32(out, 0);
3082 for value in [0.602f32, 0.602, 0.602, -0.5, -1.0, 0.5] {
3083 push_f32(out, value);
3084 }
3085
3086 out.push(0);
3087 push_i32(out, 0);
3088 out.push(0);
3089
3090 push_i32(out, 0);
3091 push_i32(out, 0);
3092 push_i32(out, max_frame as i32);
3093 push_i32(out, 0);
3094 out.push(0);
3095 out.push(0);
3096 out.push(0);
3097 out.push(0);
3098 push_i32(out, 500);
3099 push_i32(out, 0);
3100 out.push(0);
3101 push_empty_pmm_path(out);
3102 push_i32(out, 0);
3103 push_i32(out, 0);
3104 push_f32(out, 0.0);
3105 push_empty_pmm_path(out);
3106 push_i32(out, 0);
3107 push_i32(out, 0);
3108 push_i32(out, 0);
3109 push_f32(out, 1.0);
3110 push_empty_pmm_path(out);
3111 out.push(0);
3112 out.push(0);
3113 out.push(1);
3114 out.push(1);
3115 push_f32(out, 60.0);
3116 push_i32(out, 0);
3117 push_i32(out, 1);
3118 push_f32(out, 1.0);
3119 out.push(1);
3120 out.push(2);
3121
3122 push_f32(out, 9.8);
3123 push_i32(out, 10);
3124 for value in [0.0f32, -1.0, 0.0] {
3125 push_f32(out, value);
3126 }
3127 out.push(0);
3128 push_document_gravity_keyframe(out, None, true);
3129 push_i32(out, 0);
3130
3131 out.push(1);
3132 push_f32(out, 0.01125);
3133 push_document_self_shadow_keyframe(out, None, true);
3134 push_i32(out, 0);
3135
3136 push_i32(out, 0);
3137 push_i32(out, 0);
3138 push_i32(out, 0);
3139 out.push(0);
3140 push_i32(out, -1);
3141 push_i32(out, -1);
3142 for value in [
3143 1.0f32, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
3144 ] {
3145 push_f32(out, value);
3146 }
3147 out.push(0);
3148 out.push(0);
3149 out.push(1);
3150 push_i32(out, 0);
3151 out.push(1);
3152 out.push(0);
3153 push_i32(out, 0);
3154}
3155
3156fn push_pmm_len_prefixed_sjis(out: &mut Vec<u8>, text: &str, original_bytes: &[u8]) {
3157 let bytes = if !original_bytes.is_empty() {
3158 original_bytes.to_vec()
3159 } else {
3160 encode_sjis(text)
3161 };
3162 let length = bytes.len().min(u8::MAX as usize);
3163 out.push(length as u8);
3164 out.extend_from_slice(&bytes[..length]);
3165}
3166
3167fn push_pmm_sjis_string(out: &mut Vec<u8>, text: &str, original_bytes: Option<&[u8]>) {
3168 if let Some(bytes) = original_bytes
3169 && !bytes.is_empty()
3170 {
3171 out.extend_from_slice(bytes);
3172 return;
3173 }
3174 out.extend_from_slice(&encode_sjis(text));
3175}
3176
3177fn timeline_from_project_settings(settings: &PmmProjectSettings) -> PmmTimeline {
3178 let duration_seconds = settings
3179 .timeline_frame_count
3180 .zip(settings.frame_rate)
3181 .and_then(|(frame_count, frame_rate)| {
3182 if frame_rate > 0.0 {
3183 Some(frame_count as f32 / frame_rate)
3184 } else {
3185 None
3186 }
3187 });
3188 PmmTimeline {
3189 start_frame: settings.timeline_frame_count.map(|_| 0),
3190 end_frame_exclusive: settings.timeline_frame_count,
3191 frame_count: settings.timeline_frame_count,
3192 frame_rate: settings.frame_rate,
3193 duration_seconds,
3194 }
3195}
3196
3197fn parse_project_settings(data: &[u8]) -> PmmProjectSettings {
3198 let screen_width = read_u32_at(data, 30).filter(|value| (1..=16_384).contains(value));
3199 let screen_height = read_u32_at(data, 34).filter(|value| (1..=16_384).contains(value));
3200 let timeline_frame_count = read_u32_at(data, 38).filter(|value| *value <= 10_000_000);
3201 let frame_rate =
3202 read_f32_at(data, 42).filter(|value| value.is_finite() && *value > 0.0 && *value <= 1000.0);
3203
3204 PmmProjectSettings {
3205 screen_width,
3206 screen_height,
3207 timeline_frame_count,
3208 frame_rate,
3209 }
3210}
3211
3212fn active_slot_indices(flags: &[u8]) -> Vec<usize> {
3213 flags
3214 .iter()
3215 .enumerate()
3216 .filter(|&(_, f)| *f != 0)
3217 .map(|(i, _)| i)
3218 .collect()
3219}
3220
3221fn empty_slot_indices(flags: &[u8]) -> Vec<usize> {
3222 flags
3223 .iter()
3224 .enumerate()
3225 .filter(|&(_, f)| *f == 0)
3226 .map(|(i, _)| i)
3227 .collect()
3228}
3229
3230fn slot_flag_counts(flags: &[u8]) -> BTreeMap<u8, usize> {
3231 let mut counts: BTreeMap<u8, usize> = BTreeMap::new();
3232 for &flag in flags {
3233 *counts.entry(flag).or_insert(0) += 1;
3234 }
3235 counts
3236}
3237
3238fn slot_flag_entries(flags: &[u8]) -> Vec<PmmModelSlotFlagEntry> {
3239 flags
3240 .iter()
3241 .enumerate()
3242 .map(|(slot_index, flag)| PmmModelSlotFlagEntry {
3243 slot_index,
3244 flag: *flag,
3245 active: *flag != 0,
3246 })
3247 .collect()
3248}
3249
3250fn document_expand_flags(flags: &[u8]) -> Option<PmmDocumentExpandFlags> {
3251 Some(PmmDocumentExpandFlags {
3252 editing_cla: *flags.first()? != 0,
3253 camera_panel: *flags.get(1)? != 0,
3254 light_panel: *flags.get(2)? != 0,
3255 accessory_panel: *flags.get(3)? != 0,
3256 bone_panel: *flags.get(4)? != 0,
3257 morph_panel: *flags.get(5)? != 0,
3258 self_shadow_panel: *flags.get(6)? != 0,
3259 })
3260}
3261
3262fn parse_display_state(data: &[u8], parsed_version: Option<u32>) -> PmmDisplayState {
3263 if parsed_version == Some(1) {
3264 if let (Some(model_count), Some(accessory_count)) =
3265 (data.get(52).copied(), data.get(53).copied())
3266 && model_count <= 64
3267 && accessory_count <= 64
3268 {
3269 return PmmDisplayState {
3270 layout: "pmm-v1-counts",
3271 model_slot_flags: Vec::new(),
3272 model_slot_flag_entries: Vec::new(),
3273 document_expand_flags: None,
3274 selected_model_index: None,
3275 document_model_count: None,
3276 declared_model_slot_count: Some(model_count),
3277 model_slot_count: model_count as usize,
3278 non_zero_model_slot_count: model_count as usize,
3279 accessory_slot_count: Some(accessory_count),
3280 active_model_slot_indices: Vec::new(),
3281 empty_model_slot_indices: Vec::new(),
3282 model_slot_flag_counts: BTreeMap::new(),
3283 };
3284 }
3285 return unknown_display_state();
3286 }
3287
3288 let model_slot_flags = data
3289 .get(46..54)
3290 .filter(|bytes| bytes.iter().all(|byte| *byte <= 2))
3291 .map(|bytes| bytes.to_vec());
3292 let accessory_slot_count = None;
3293 let document_expand_flags = model_slot_flags
3294 .as_ref()
3295 .and_then(|flags| document_expand_flags(flags));
3296 let selected_model_index = model_slot_flags
3297 .as_ref()
3298 .and_then(|_| data.get(53).copied());
3299 let document_model_count = model_slot_flags
3300 .as_ref()
3301 .and_then(|_| data.get(54).copied());
3302
3303 let model_slot_flags = model_slot_flags.unwrap_or_default();
3304 let model_slot_count = model_slot_flags.len();
3305 let non_zero_model_slot_count = model_slot_flags.iter().filter(|flag| **flag != 0).count();
3306 let active_model_slot_indices = active_slot_indices(&model_slot_flags);
3307 let empty_model_slot_indices = empty_slot_indices(&model_slot_flags);
3308 let model_slot_flag_counts = slot_flag_counts(&model_slot_flags);
3309 let model_slot_flag_entries = slot_flag_entries(&model_slot_flags);
3310 PmmDisplayState {
3311 layout: if model_slot_flags.is_empty() {
3312 "unknown"
3313 } else {
3314 "pmm-v2-flags"
3315 },
3316 declared_model_slot_count: if model_slot_flags.is_empty() {
3317 None
3318 } else {
3319 Some(model_slot_count as u8)
3320 },
3321 model_slot_count,
3322 non_zero_model_slot_count,
3323 model_slot_flags,
3324 model_slot_flag_entries,
3325 document_expand_flags,
3326 selected_model_index,
3327 document_model_count,
3328 accessory_slot_count,
3329 active_model_slot_indices,
3330 empty_model_slot_indices,
3331 model_slot_flag_counts,
3332 }
3333}
3334
3335fn unknown_display_state() -> PmmDisplayState {
3336 PmmDisplayState {
3337 layout: "unknown",
3338 model_slot_flags: Vec::new(),
3339 model_slot_flag_entries: Vec::new(),
3340 document_expand_flags: None,
3341 selected_model_index: None,
3342 document_model_count: None,
3343 declared_model_slot_count: None,
3344 model_slot_count: 0,
3345 non_zero_model_slot_count: 0,
3346 accessory_slot_count: None,
3347 active_model_slot_indices: Vec::new(),
3348 empty_model_slot_indices: Vec::new(),
3349 model_slot_flag_counts: BTreeMap::new(),
3350 }
3351}
3352
3353fn parse_header_text_entries(
3354 data: &[u8],
3355 parsed_version: Option<u32>,
3356 first_reference: Option<&PmmAssetReference>,
3357) -> Vec<PmmHeaderTextEntry> {
3358 if parsed_version != Some(1) {
3359 return Vec::new();
3360 }
3361 let start = 54usize;
3362 let Some(first_reference) = first_reference else {
3363 return Vec::new();
3364 };
3365 let end = first_reference.offset.min(data.len());
3366 if end <= start {
3367 return Vec::new();
3368 }
3369
3370 let mut entries = Vec::new();
3371 let mut chunk_start = start;
3372 for index in start..end {
3373 if data[index] != 0 {
3374 continue;
3375 }
3376 if index > chunk_start {
3377 let bytes = &data[chunk_start..index];
3378 let text = decode_sjis(bytes).trim().to_owned();
3379 if !text.is_empty() {
3380 entries.push(PmmHeaderTextEntry {
3381 index: entries.len(),
3382 offset: chunk_start,
3383 offset_end: index,
3384 text,
3385 text_bytes: bytes.to_vec(),
3386 });
3387 }
3388 }
3389 chunk_start = index + 1;
3390 }
3391 entries
3392}
3393
3394fn read_u32_at(data: &[u8], offset: usize) -> Option<u32> {
3395 data.get(offset..offset + 4)
3396 .and_then(|bytes| bytes.try_into().ok())
3397 .map(u32::from_le_bytes)
3398}
3399
3400fn read_f32_at(data: &[u8], offset: usize) -> Option<f32> {
3401 data.get(offset..offset + 4)
3402 .and_then(|bytes| bytes.try_into().ok())
3403 .map(f32::from_le_bytes)
3404}
3405
3406struct PmmDocumentCursor<'a> {
3407 inner: ByteReader<'a>,
3408}
3409
3410impl<'a> PmmDocumentCursor<'a> {
3411 fn new(data: &'a [u8]) -> Self {
3412 Self {
3413 inner: ByteReader::new(data),
3414 }
3415 }
3416
3417 fn offset(&self) -> usize {
3418 self.inner.pos
3419 }
3420
3421 fn data(&self) -> &'a [u8] {
3422 self.inner.data
3423 }
3424
3425 fn read_u8(&mut self) -> Option<u8> {
3426 self.inner.read_u8().ok()
3427 }
3428
3429 fn read_bool(&mut self) -> Option<bool> {
3430 Some(self.read_u8()? != 0)
3431 }
3432
3433 fn read_i32(&mut self) -> Option<i32> {
3434 self.inner.read_i32_le().ok()
3435 }
3436
3437 fn read_f32(&mut self) -> Option<f32> {
3438 self.inner.read_f32_le().ok()
3439 }
3440
3441 fn read_f32x3(&mut self) -> Option<[f32; 3]> {
3442 Some([self.read_f32()?, self.read_f32()?, self.read_f32()?])
3443 }
3444
3445 fn read_f32x4(&mut self) -> Option<[f32; 4]> {
3446 Some([
3447 self.read_f32()?,
3448 self.read_f32()?,
3449 self.read_f32()?,
3450 self.read_f32()?,
3451 ])
3452 }
3453
3454 fn read_bytes16(&mut self) -> Option<[u8; 16]> {
3455 self.inner.read_bytes(16).ok()?.try_into().ok()
3456 }
3457
3458 fn read_bytes24(&mut self) -> Option<[u8; 24]> {
3459 self.inner.read_bytes(24).ok()?.try_into().ok()
3460 }
3461
3462 fn read_variable_string(&mut self) -> Option<String> {
3463 let length = self.read_u8()? as usize;
3464 let bytes = self.inner.read_bytes(length).ok()?;
3465 Some(decode_shift_jis(bytes))
3466 }
3467
3468 fn read_fixed_string(&mut self, length: usize) -> Option<String> {
3469 let bytes = self.inner.read_bytes(length).ok()?;
3470 let end = bytes
3471 .iter()
3472 .position(|byte| *byte == 0)
3473 .unwrap_or(bytes.len());
3474 Some(decode_shift_jis(&bytes[..end]))
3475 }
3476
3477 fn skip(&mut self, length: usize) -> Option<()> {
3478 self.inner.skip(length).ok()
3479 }
3480}
3481
3482fn parse_document_summary(
3483 data: &[u8],
3484 parsed_version: Option<u32>,
3485 references: &[PmmAssetReference],
3486) -> Option<PmmDocumentSummary> {
3487 if parsed_version != Some(2) {
3488 return None;
3489 }
3490 let mut cursor = PmmDocumentCursor::new(data);
3491 cursor.skip(30)?;
3492 cursor.read_i32()?;
3493 cursor.read_i32()?;
3494 cursor.read_i32()?;
3495 cursor.read_f32()?;
3496 cursor.skip(7)?;
3497 let selected_model_index = cursor.read_u8()?;
3498 let model_count = cursor.read_u8()? as usize;
3499 if model_count == 0 || selected_model_index as usize >= model_count {
3500 return None;
3501 }
3502
3503 let mut models = Vec::with_capacity(model_count);
3504 for slot_index in 0..model_count {
3505 models.push(read_document_model_summary(
3506 &mut cursor,
3507 slot_index,
3508 references,
3509 )?);
3510 }
3511 let counts = summarize_document_counts(&models);
3512
3513 Some(PmmDocumentSummary {
3514 source: "nanoem/ext/document.c PMMv2 layout",
3515 selected_model_index,
3516 model_count,
3517 counts,
3518 models,
3519 })
3520}
3521
3522fn parse_document_global_summary(
3523 data: &[u8],
3524 parsed_version: Option<u32>,
3525 references: &[PmmAssetReference],
3526) -> Option<PmmDocumentGlobalSummary> {
3527 if parsed_version != Some(2) {
3528 return None;
3529 }
3530 let mut cursor = PmmDocumentCursor::new(data);
3531 cursor.skip(30)?;
3532 cursor.read_i32()?;
3533 cursor.read_i32()?;
3534 cursor.read_i32()?;
3535 cursor.read_f32()?;
3536 cursor.skip(7)?;
3537 let selected_model_index = cursor.read_u8()?;
3538 let model_count = cursor.read_u8()? as usize;
3539 if model_count == 0 || selected_model_index as usize >= model_count {
3540 return None;
3541 }
3542 for slot_index in 0..model_count {
3543 read_document_model_summary(&mut cursor, slot_index, references)?;
3544 }
3545
3546 let offset = cursor.offset();
3547 let camera = read_document_camera_summary(&mut cursor)?;
3548 let light = read_document_light_summary(&mut cursor)?;
3549 let accessories = read_document_accessory_block_summary(&mut cursor, references)?;
3550 let settings_before_gravity = read_document_settings_before_gravity(&mut cursor)?;
3551 let gravity = read_document_gravity_summary(&mut cursor)?;
3552 let self_shadow = read_document_self_shadow_summary(&mut cursor)?;
3553 let settings =
3554 finish_document_settings_summary(settings_before_gravity, &mut cursor, model_count)?;
3555 let offset_end = cursor.offset();
3556
3557 Some(PmmDocumentGlobalSummary {
3558 source: "nanoem/ext/document.c PMMv2 global layout",
3559 offset,
3560 offset_end,
3561 camera,
3562 light,
3563 accessories,
3564 settings,
3565 gravity,
3566 self_shadow,
3567 })
3568}
3569
3570fn read_document_model_summary(
3571 cursor: &mut PmmDocumentCursor<'_>,
3572 slot_index: usize,
3573 references: &[PmmAssetReference],
3574) -> Option<PmmDocumentModelSummary> {
3575 const PMM_PATH_BYTE_LENGTH: usize = 256;
3576 let offset = cursor.offset();
3577 let document_model_index = cursor.read_u8()?;
3578 let name = cursor.read_variable_string()?;
3579 let english_name = cursor.read_variable_string()?;
3580 let path_offset = cursor.offset();
3581 let path = cursor.read_fixed_string(PMM_PATH_BYTE_LENGTH)?;
3582 let asset_reference_index = asset_reference_index_for_path(references, "model", &path);
3583 cursor.read_u8()?;
3584 let bone_count = usize_from_i32(cursor.read_i32()?)?;
3585 let mut bone_names = Vec::new();
3586 for _ in 0..bone_count {
3587 bone_names.push(cursor.read_variable_string()?);
3588 }
3589 let morph_count = usize_from_i32(cursor.read_i32()?)?;
3590 let mut morph_names = Vec::new();
3591 for _ in 0..morph_count {
3592 morph_names.push(cursor.read_variable_string()?);
3593 }
3594 let constraint_bone_count = usize_from_i32(cursor.read_i32()?)?;
3595 cursor.skip(constraint_bone_count.checked_mul(4)?)?;
3596 let outside_parent_subject_bone_count = usize_from_i32(cursor.read_i32()?)?;
3597 cursor.skip(outside_parent_subject_bone_count.checked_mul(4)?)?;
3598 let draw_order_index = cursor.read_u8()?;
3599 let visible = cursor.read_bool()?;
3600 let selected_bone_index = cursor.read_i32()?;
3601 let selected_morph_indices = [
3602 cursor.read_i32()?,
3603 cursor.read_i32()?,
3604 cursor.read_i32()?,
3605 cursor.read_i32()?,
3606 ];
3607 let expansion_state_count = cursor.read_u8()? as usize;
3608 cursor.skip(expansion_state_count)?;
3609 let vertical_scroll = cursor.read_i32()?;
3610 let last_frame_index = cursor.read_i32()?;
3611
3612 let initial_bone_keyframes_offset = cursor.offset();
3613 let initial_bone_keyframes = bone_count;
3614 let mut initial_bone_keyframe_summaries = Vec::with_capacity(initial_bone_keyframes);
3615 for _ in 0..initial_bone_keyframes {
3616 initial_bone_keyframe_summaries.push(read_document_bone_keyframe(cursor, false)?);
3617 }
3618 let bone_keyframe_count_offset = cursor.offset();
3619 let bone_keyframes = usize_from_i32(cursor.read_i32()?)?;
3620 let bone_keyframes_offset = cursor.offset();
3621 let mut bone_keyframe_summaries = Vec::with_capacity(bone_keyframes);
3622 for _ in 0..bone_keyframes {
3623 bone_keyframe_summaries.push(read_document_bone_keyframe(cursor, true)?);
3624 }
3625 let bone_keyframes_end_offset = cursor.offset();
3626
3627 let initial_morph_keyframes_offset = cursor.offset();
3628 let initial_morph_keyframes = morph_count;
3629 let mut initial_morph_keyframe_summaries = Vec::with_capacity(initial_morph_keyframes);
3630 for _ in 0..initial_morph_keyframes {
3631 initial_morph_keyframe_summaries.push(read_document_morph_keyframe(cursor, false)?);
3632 }
3633 let morph_keyframe_count_offset = cursor.offset();
3634 let morph_keyframes = usize_from_i32(cursor.read_i32()?)?;
3635 let morph_keyframes_offset = cursor.offset();
3636 let mut morph_keyframe_summaries = Vec::with_capacity(morph_keyframes);
3637 for _ in 0..morph_keyframes {
3638 morph_keyframe_summaries.push(read_document_morph_keyframe(cursor, true)?);
3639 }
3640 let morph_keyframes_end_offset = cursor.offset();
3641
3642 let initial_model_keyframes = 1;
3643 let initial_model_keyframe_offset = cursor.offset();
3644 let initial_model_keyframe = read_document_model_keyframe(
3645 cursor,
3646 false,
3647 constraint_bone_count,
3648 outside_parent_subject_bone_count,
3649 )?;
3650 let model_keyframe_count_offset = cursor.offset();
3651 let model_keyframes = usize_from_i32(cursor.read_i32()?)?;
3652 let model_keyframes_offset = cursor.offset();
3653 let mut model_keyframe_summaries = Vec::with_capacity(model_keyframes);
3654 for _ in 0..model_keyframes {
3655 model_keyframe_summaries.push(read_document_model_keyframe(
3656 cursor,
3657 true,
3658 constraint_bone_count,
3659 outside_parent_subject_bone_count,
3660 )?);
3661 }
3662 let model_keyframes_end_offset = cursor.offset();
3663
3664 let bone_states_offset = cursor.offset();
3665 let mut bone_state_summaries = Vec::with_capacity(bone_count);
3666 for _ in 0..bone_count {
3667 bone_state_summaries.push(read_document_bone_state(cursor)?);
3668 }
3669 let morph_states_offset = cursor.offset();
3670 let mut morph_state_summaries = Vec::with_capacity(morph_count);
3671 for _ in 0..morph_count {
3672 morph_state_summaries.push(read_document_morph_state(cursor)?);
3673 }
3674 let constraint_states_offset = cursor.offset();
3675 let mut constraint_state_summaries = Vec::with_capacity(constraint_bone_count);
3676 for _ in 0..constraint_bone_count {
3677 constraint_state_summaries.push(read_document_constraint_state(cursor)?);
3678 }
3679 let outside_parent_states_offset = cursor.offset();
3680 let mut outside_parent_state_summaries = Vec::with_capacity(outside_parent_subject_bone_count);
3681 for _ in 0..outside_parent_subject_bone_count {
3682 outside_parent_state_summaries.push(read_document_outside_parent_state(cursor)?);
3683 }
3684
3685 let blend_enabled = cursor.read_bool()?;
3686 let edge_width = cursor.read_f32()?;
3687 let self_shadow_enabled = cursor.read_bool()?;
3688 let transform_order_index = cursor.read_u8()?;
3689 let offset_end = cursor.offset();
3690
3691 Some(PmmDocumentModelSummary {
3692 slot_index,
3693 document_model_index,
3694 offset,
3695 offset_end,
3696 path_offset,
3697 name,
3698 english_name,
3699 path,
3700 asset_reference_index,
3701 bone_count,
3702 bone_names,
3703 morph_count,
3704 morph_names,
3705 constraint_bone_count,
3706 outside_parent_subject_bone_count,
3707 draw_order_index,
3708 transform_order_index,
3709 selected_bone_index,
3710 selected_morph_indices,
3711 vertical_scroll,
3712 sections: PmmDocumentModelSections {
3713 initial_bone_keyframes_offset,
3714 bone_keyframe_count_offset,
3715 bone_keyframes_offset,
3716 bone_keyframes_end_offset,
3717 initial_morph_keyframes_offset,
3718 morph_keyframe_count_offset,
3719 morph_keyframes_offset,
3720 morph_keyframes_end_offset,
3721 initial_model_keyframe_offset,
3722 model_keyframe_count_offset,
3723 model_keyframes_offset,
3724 model_keyframes_end_offset,
3725 bone_states_offset,
3726 morph_states_offset,
3727 constraint_states_offset,
3728 outside_parent_states_offset,
3729 },
3730 initial_bone_keyframes,
3731 initial_bone_keyframe_summaries,
3732 bone_keyframes,
3733 bone_keyframe_summaries,
3734 initial_morph_keyframes,
3735 initial_morph_keyframe_summaries,
3736 morph_keyframes,
3737 morph_keyframe_summaries,
3738 initial_model_keyframes,
3739 model_keyframes,
3740 initial_model_keyframe,
3741 model_keyframe_summaries,
3742 last_frame_index,
3743 visible,
3744 blend_enabled,
3745 edge_width,
3746 self_shadow_enabled,
3747 bone_state_summaries,
3748 morph_state_summaries,
3749 constraint_state_summaries,
3750 outside_parent_state_summaries,
3751 })
3752}
3753
3754fn read_document_bone_keyframe(
3755 cursor: &mut PmmDocumentCursor<'_>,
3756 include_index: bool,
3757) -> Option<PmmDocumentBoneKeyframeSummary> {
3758 let offset = cursor.offset();
3759 let base = read_document_base_keyframe(cursor, include_index)?;
3760 let payload_offset = cursor.offset();
3761 let interpolation = cursor.read_bytes16()?;
3762 let translation = cursor.read_f32x3()?;
3763 let orientation = cursor.read_f32x4()?;
3764 let physics_disabled = cursor.read_bool()?;
3765 let selected = cursor.read_bool()?;
3766 let byte_length = cursor.offset() - offset;
3767 let payload_byte_length = cursor.offset() - payload_offset;
3768 let payload_bytes = cursor.data().get(payload_offset..cursor.offset())?.to_vec();
3769 Some(PmmDocumentBoneKeyframeSummary {
3770 index: base.index,
3771 frame_index: base.frame_index,
3772 previous_keyframe_index: base.previous_keyframe_index,
3773 next_keyframe_index: base.next_keyframe_index,
3774 interpolation,
3775 translation,
3776 orientation,
3777 physics_disabled,
3778 selected,
3779 offset,
3780 byte_length,
3781 payload_offset,
3782 payload_byte_length,
3783 payload_bytes,
3784 })
3785}
3786
3787fn read_document_morph_keyframe(
3788 cursor: &mut PmmDocumentCursor<'_>,
3789 include_index: bool,
3790) -> Option<PmmDocumentMorphKeyframeSummary> {
3791 let offset = cursor.offset();
3792 let base = read_document_base_keyframe(cursor, include_index)?;
3793 let payload_offset = cursor.offset();
3794 let weight = cursor.read_f32()?;
3795 let selected = cursor.read_bool()?;
3796 let byte_length = cursor.offset() - offset;
3797 let payload_byte_length = cursor.offset() - payload_offset;
3798 let payload_bytes = cursor.data().get(payload_offset..cursor.offset())?.to_vec();
3799 Some(PmmDocumentMorphKeyframeSummary {
3800 index: base.index,
3801 frame_index: base.frame_index,
3802 previous_keyframe_index: base.previous_keyframe_index,
3803 next_keyframe_index: base.next_keyframe_index,
3804 weight,
3805 selected,
3806 offset,
3807 byte_length,
3808 payload_offset,
3809 payload_byte_length,
3810 payload_bytes,
3811 })
3812}
3813
3814fn read_document_model_keyframe(
3815 cursor: &mut PmmDocumentCursor<'_>,
3816 include_index: bool,
3817 constraint_bone_count: usize,
3818 outside_parent_subject_bone_count: usize,
3819) -> Option<PmmDocumentModelKeyframeSummary> {
3820 let offset = cursor.offset();
3821 let base = read_document_base_keyframe(cursor, include_index)?;
3822 let payload_offset = cursor.offset();
3823 let visible_offset = cursor.offset();
3824 let visible = cursor.read_bool()?;
3825 let constraint_states_offset = cursor.offset();
3826 let mut constraint_states = Vec::with_capacity(constraint_bone_count);
3827 for _ in 0..constraint_bone_count {
3828 constraint_states.push(cursor.read_bool()?);
3829 }
3830 let constraint_states_byte_length = cursor.offset() - constraint_states_offset;
3831 let outside_parent_indices_offset = cursor.offset();
3832 let mut outside_parent_indices = Vec::with_capacity(outside_parent_subject_bone_count);
3833 for _ in 0..outside_parent_subject_bone_count {
3834 outside_parent_indices.push(PmmDocumentOutsideParentIndexSummary {
3835 parent_model_index: cursor.read_i32()?,
3836 parent_model_bone_index: cursor.read_i32()?,
3837 });
3838 }
3839 let outside_parent_indices_byte_length = cursor.offset() - outside_parent_indices_offset;
3840 let self_shadow_enabled_offset = cursor.offset();
3841 let self_shadow_enabled = cursor.read_bool()?;
3842 let byte_length = cursor.offset() - offset;
3843 let payload_byte_length = cursor.offset() - payload_offset;
3844 let payload_bytes = cursor.data().get(payload_offset..cursor.offset())?.to_vec();
3845 let constraint_state_count = constraint_bone_count;
3846 let outside_parent_index_count = outside_parent_subject_bone_count;
3847 Some(PmmDocumentModelKeyframeSummary {
3848 index: base.index,
3849 frame_index: base.frame_index,
3850 previous_keyframe_index: base.previous_keyframe_index,
3851 next_keyframe_index: base.next_keyframe_index,
3852 visible,
3853 constraint_states,
3854 outside_parent_indices,
3855 self_shadow_enabled,
3856 visible_offset,
3857 constraint_state_count,
3858 constraint_states_offset,
3859 constraint_states_byte_length,
3860 outside_parent_index_count,
3861 outside_parent_indices_offset,
3862 outside_parent_indices_byte_length,
3863 self_shadow_enabled_offset,
3864 offset,
3865 byte_length,
3866 payload_offset,
3867 payload_byte_length,
3868 payload_bytes,
3869 })
3870}
3871
3872fn read_document_camera_summary(
3873 cursor: &mut PmmDocumentCursor<'_>,
3874) -> Option<PmmDocumentTrackSummary> {
3875 let offset = cursor.offset();
3876 let initial_keyframe = read_document_camera_keyframe(cursor, false)?;
3877 let keyframe_count_offset = cursor.offset();
3878 let keyframes = usize_from_i32(cursor.read_i32()?)?;
3879 let keyframes_offset = cursor.offset();
3880 let mut keyframe_summaries = Vec::with_capacity(keyframes);
3881 for _ in 0..keyframes {
3882 keyframe_summaries.push(read_document_camera_keyframe(cursor, true)?);
3883 }
3884 let keyframes_end_offset = cursor.offset();
3885 let state_offset = cursor.offset();
3886 cursor.skip(12 * 3)?;
3887 cursor.read_bool()?;
3888 let state_end_offset = cursor.offset();
3889 Some(PmmDocumentTrackSummary {
3890 offset,
3891 offset_end: state_end_offset,
3892 initial_keyframes: 1,
3893 keyframes,
3894 initial_keyframe: Some(initial_keyframe),
3895 keyframe_summaries,
3896 keyframe_count_offset,
3897 keyframes_offset,
3898 keyframes_end_offset,
3899 state_offset: Some(state_offset),
3900 state_end_offset: Some(state_end_offset),
3901 })
3902}
3903
3904fn read_document_camera_keyframe(
3905 cursor: &mut PmmDocumentCursor<'_>,
3906 include_index: bool,
3907) -> Option<PmmDocumentKeyframeSummary> {
3908 let offset = cursor.offset();
3909 let base = read_document_base_keyframe(cursor, include_index)?;
3910 let payload_offset = cursor.offset();
3911 let distance_offset = cursor.offset();
3912 let distance = cursor.read_f32()?;
3913 let look_at_offset = cursor.offset();
3914 let look_at = cursor.read_f32x3()?;
3915 let look_at_byte_length = cursor.offset() - look_at_offset;
3916 let angle_offset = cursor.offset();
3917 let angle = cursor.read_f32x3()?;
3918 let angle_byte_length = cursor.offset() - angle_offset;
3919 let parent_model_index_offset = cursor.offset();
3920 let parent_model_index = cursor.read_i32()?;
3921 let parent_model_bone_index_offset = cursor.offset();
3922 let parent_model_bone_index = cursor.read_i32()?;
3923 let interpolation_offset = cursor.offset();
3924 let interpolation = cursor.read_bytes24()?;
3925 let interpolation_byte_length = cursor.offset() - interpolation_offset;
3926 let perspective_view_offset = cursor.offset();
3927 let perspective_view = !cursor.read_bool()?;
3928 let fov_offset = cursor.offset();
3929 let fov = cursor.read_i32()?;
3930 let selected_offset = cursor.offset();
3931 let selected = cursor.read_bool()?;
3932 let byte_length = cursor.offset() - offset;
3933 let payload_byte_length = cursor.offset() - payload_offset;
3934 let payload_bytes = cursor.data().get(payload_offset..cursor.offset())?.to_vec();
3935 Some(PmmDocumentKeyframeSummary::Camera {
3936 index: base.index,
3937 frame_index: base.frame_index,
3938 previous_keyframe_index: base.previous_keyframe_index,
3939 next_keyframe_index: base.next_keyframe_index,
3940 distance,
3941 look_at,
3942 angle,
3943 parent_model_index,
3944 parent_model_bone_index,
3945 interpolation,
3946 perspective_view,
3947 fov,
3948 selected,
3949 distance_offset,
3950 look_at_offset,
3951 look_at_byte_length,
3952 angle_offset,
3953 angle_byte_length,
3954 parent_model_index_offset,
3955 parent_model_bone_index_offset,
3956 interpolation_offset,
3957 interpolation_byte_length,
3958 perspective_view_offset,
3959 fov_offset,
3960 selected_offset,
3961 offset,
3962 byte_length,
3963 payload_offset,
3964 payload_byte_length,
3965 payload_bytes,
3966 })
3967}
3968
3969fn read_document_light_summary(
3970 cursor: &mut PmmDocumentCursor<'_>,
3971) -> Option<PmmDocumentTrackSummary> {
3972 let offset = cursor.offset();
3973 let initial_keyframe = read_document_light_keyframe(cursor, false)?;
3974 let keyframe_count_offset = cursor.offset();
3975 let keyframes = usize_from_i32(cursor.read_i32()?)?;
3976 let keyframes_offset = cursor.offset();
3977 let mut keyframe_summaries = Vec::with_capacity(keyframes);
3978 for _ in 0..keyframes {
3979 keyframe_summaries.push(read_document_light_keyframe(cursor, true)?);
3980 }
3981 let keyframes_end_offset = cursor.offset();
3982 let state_offset = cursor.offset();
3983 cursor.skip(12)?;
3984 cursor.skip(12)?;
3985 let state_end_offset = cursor.offset();
3986 Some(PmmDocumentTrackSummary {
3987 offset,
3988 offset_end: state_end_offset,
3989 initial_keyframes: 1,
3990 keyframes,
3991 initial_keyframe: Some(initial_keyframe),
3992 keyframe_summaries,
3993 keyframe_count_offset,
3994 keyframes_offset,
3995 keyframes_end_offset,
3996 state_offset: Some(state_offset),
3997 state_end_offset: Some(state_end_offset),
3998 })
3999}
4000
4001fn read_document_light_keyframe(
4002 cursor: &mut PmmDocumentCursor<'_>,
4003 include_index: bool,
4004) -> Option<PmmDocumentKeyframeSummary> {
4005 let offset = cursor.offset();
4006 let base = read_document_base_keyframe(cursor, include_index)?;
4007 let payload_offset = cursor.offset();
4008 let color_offset = cursor.offset();
4009 let color = cursor.read_f32x3()?;
4010 let color_byte_length = cursor.offset() - color_offset;
4011 let direction_offset = cursor.offset();
4012 let direction = cursor.read_f32x3()?;
4013 let direction_byte_length = cursor.offset() - direction_offset;
4014 let selected_offset = cursor.offset();
4015 let selected = cursor.read_bool()?;
4016 let byte_length = cursor.offset() - offset;
4017 let payload_byte_length = cursor.offset() - payload_offset;
4018 let payload_bytes = cursor.data().get(payload_offset..cursor.offset())?.to_vec();
4019 Some(PmmDocumentKeyframeSummary::Light {
4020 index: base.index,
4021 frame_index: base.frame_index,
4022 previous_keyframe_index: base.previous_keyframe_index,
4023 next_keyframe_index: base.next_keyframe_index,
4024 color,
4025 direction,
4026 selected,
4027 color_offset,
4028 color_byte_length,
4029 direction_offset,
4030 direction_byte_length,
4031 selected_offset,
4032 offset,
4033 byte_length,
4034 payload_offset,
4035 payload_byte_length,
4036 payload_bytes,
4037 })
4038}
4039
4040fn read_document_accessory_block_summary(
4041 cursor: &mut PmmDocumentCursor<'_>,
4042 references: &[PmmAssetReference],
4043) -> Option<PmmDocumentAccessoryBlockSummary> {
4044 let offset = cursor.offset();
4045 let selected_accessory_index = cursor.read_u8()?;
4046 let horizontal_scroll = cursor.read_i32()?;
4047 let accessory_count = cursor.read_u8()? as usize;
4048 let mut accessories = Vec::with_capacity(accessory_count);
4049 for slot_index in 0..accessory_count {
4050 accessories.push(read_document_accessory_summary(
4051 cursor, slot_index, references,
4052 )?);
4053 }
4054 let offset_end = cursor.offset();
4055 let keyframes = accessories
4056 .iter()
4057 .map(|accessory| accessory.keyframes)
4058 .sum();
4059 Some(PmmDocumentAccessoryBlockSummary {
4060 offset,
4061 offset_end,
4062 selected_accessory_index,
4063 horizontal_scroll,
4064 accessory_count,
4065 keyframes,
4066 accessories,
4067 })
4068}
4069
4070fn read_document_accessory_summary(
4071 cursor: &mut PmmDocumentCursor<'_>,
4072 slot_index: usize,
4073 references: &[PmmAssetReference],
4074) -> Option<PmmDocumentAccessorySummary> {
4075 const PMM_ACCESSORY_NAME_BYTE_LENGTH: usize = 100;
4076 const PMM_PATH_BYTE_LENGTH: usize = 256;
4077 let offset = cursor.offset();
4078 let document_accessory_index = cursor.read_u8()?;
4079 let name = cursor.read_fixed_string(PMM_ACCESSORY_NAME_BYTE_LENGTH)?;
4080 let path_offset = cursor.offset();
4081 let path = cursor.read_fixed_string(PMM_PATH_BYTE_LENGTH)?;
4082 let asset_reference_index = asset_reference_index_for_path(references, "accessory", &path);
4083 let draw_order_index = cursor.read_u8()?;
4084 let initial_keyframe = read_document_accessory_keyframe(cursor, false)?;
4085 let keyframe_count_offset = cursor.offset();
4086 let keyframes = usize_from_i32(cursor.read_i32()?)?;
4087 let keyframes_offset = cursor.offset();
4088 let mut keyframe_summaries = Vec::with_capacity(keyframes);
4089 for _ in 0..keyframes {
4090 keyframe_summaries.push(read_document_accessory_keyframe(cursor, true)?);
4091 }
4092 let keyframes_end_offset = cursor.offset();
4093
4094 let state_offset = cursor.offset();
4095 let (opacity, visible) = unpack_document_accessory_opacity_and_visible(cursor.read_u8()?);
4096 let parent_model_index = cursor.read_i32()?;
4097 let parent_model_bone_index = cursor.read_i32()?;
4098 cursor.skip(12)?;
4099 let scale_factor = cursor.read_f32()?;
4100 cursor.skip(12)?;
4101 let shadow_enabled = cursor.read_bool()?;
4102 let add_blend_enabled = cursor.read_bool()?;
4103 let state_end_offset = cursor.offset();
4104 let offset_end = cursor.offset();
4105
4106 Some(PmmDocumentAccessorySummary {
4107 slot_index,
4108 document_accessory_index,
4109 offset,
4110 offset_end,
4111 path_offset,
4112 name,
4113 path,
4114 asset_reference_index,
4115 draw_order_index,
4116 keyframes,
4117 initial_keyframe,
4118 keyframe_summaries,
4119 keyframe_count_offset,
4120 keyframes_offset,
4121 keyframes_end_offset,
4122 state_offset,
4123 state_end_offset,
4124 visible,
4125 opacity,
4126 parent_model_index,
4127 parent_model_bone_index,
4128 scale_factor,
4129 shadow_enabled,
4130 add_blend_enabled,
4131 })
4132}
4133
4134fn read_document_accessory_keyframe(
4135 cursor: &mut PmmDocumentCursor<'_>,
4136 include_index: bool,
4137) -> Option<PmmDocumentAccessoryKeyframeSummary> {
4138 let offset = cursor.offset();
4139 let base = read_document_base_keyframe(cursor, include_index)?;
4140 let payload_offset = cursor.offset();
4141 let packed_opacity_visible_offset = cursor.offset();
4142 let (opacity, visible) = unpack_document_accessory_opacity_and_visible(cursor.read_u8()?);
4143 let parent_model_index_offset = cursor.offset();
4144 let parent_model_index = cursor.read_i32()?;
4145 let parent_model_bone_index_offset = cursor.offset();
4146 let parent_model_bone_index = cursor.read_i32()?;
4147 let translation_offset = cursor.offset();
4148 let translation = cursor.read_f32x3()?;
4149 let translation_byte_length = cursor.offset() - translation_offset;
4150 let orientation_offset = cursor.offset();
4151 let orientation = cursor.read_f32x3()?;
4152 let orientation_byte_length = cursor.offset() - orientation_offset;
4153 let scale_factor_offset = cursor.offset();
4154 let scale_factor = cursor.read_f32()?;
4155 let shadow_enabled_offset = cursor.offset();
4156 let shadow_enabled = cursor.read_bool()?;
4157 let selected_offset = cursor.offset();
4158 let selected = cursor.read_bool()?;
4159 let byte_length = cursor.offset() - offset;
4160 let payload_byte_length = cursor.offset() - payload_offset;
4161 let payload_bytes = cursor.data().get(payload_offset..cursor.offset())?.to_vec();
4162 Some(PmmDocumentAccessoryKeyframeSummary {
4163 index: base.index,
4164 frame_index: base.frame_index,
4165 previous_keyframe_index: base.previous_keyframe_index,
4166 next_keyframe_index: base.next_keyframe_index,
4167 visible,
4168 opacity,
4169 parent_model_index,
4170 parent_model_bone_index,
4171 translation,
4172 orientation,
4173 scale_factor,
4174 shadow_enabled,
4175 selected,
4176 packed_opacity_visible_offset,
4177 parent_model_index_offset,
4178 parent_model_bone_index_offset,
4179 translation_offset,
4180 translation_byte_length,
4181 orientation_offset,
4182 orientation_byte_length,
4183 scale_factor_offset,
4184 shadow_enabled_offset,
4185 selected_offset,
4186 offset,
4187 byte_length,
4188 payload_offset,
4189 payload_byte_length,
4190 payload_bytes,
4191 })
4192}
4193
4194struct PmmDocumentSettingsBeforeGravity {
4195 offset: usize,
4196 current_frame_index: i32,
4197 current_frame_index_offset: usize,
4198 horizontal_scroll: i32,
4199 horizontal_scroll_thumb: i32,
4200 editing_mode: i32,
4201 camera_look_mode: u8,
4202 loop_enabled: bool,
4203 begin_frame_index_enabled: bool,
4204 begin_frame_index_enabled_offset: usize,
4205 end_frame_index_enabled: bool,
4206 end_frame_index_enabled_offset: usize,
4207 begin_frame_index: i32,
4208 begin_frame_index_offset: usize,
4209 end_frame_index: i32,
4210 end_frame_index_offset: usize,
4211 audio_enabled: bool,
4212 audio_path: String,
4213 audio_path_offset: usize,
4214 background_video_offset: [i32; 2],
4215 background_video_scale_factor: f32,
4216 background_video_path: String,
4217 background_video_path_offset: usize,
4218 background_video_enabled: bool,
4219 background_image_offset: [i32; 2],
4220 background_image_scale_factor: f32,
4221 background_image_path: String,
4222 background_image_path_offset: usize,
4223 background_image_enabled: bool,
4224 information_shown: bool,
4225 grid_and_axis_shown: bool,
4226 ground_shadow_shown: bool,
4227 preferred_fps: f32,
4228 screen_capture_mode: i32,
4229 accessory_index_after_models: i32,
4230 ground_shadow_brightness: f32,
4231 translucent_ground_shadow_enabled: bool,
4232 physics_simulation_mode: u8,
4233}
4234
4235fn read_document_settings_before_gravity(
4236 cursor: &mut PmmDocumentCursor<'_>,
4237) -> Option<PmmDocumentSettingsBeforeGravity> {
4238 const PMM_PATH_BYTE_LENGTH: usize = 256;
4239 let offset = cursor.offset();
4240 let current_frame_index_offset = cursor.offset();
4241 let current_frame_index = cursor.read_i32()?;
4242 let horizontal_scroll = cursor.read_i32()?;
4243 let horizontal_scroll_thumb = cursor.read_i32()?;
4244 let editing_mode = cursor.read_i32()?;
4245 let camera_look_mode = cursor.read_u8()?;
4246 let loop_enabled = cursor.read_bool()?;
4247 let begin_frame_index_enabled_offset = cursor.offset();
4248 let begin_frame_index_enabled = cursor.read_bool()?;
4249 let end_frame_index_enabled_offset = cursor.offset();
4250 let end_frame_index_enabled = cursor.read_bool()?;
4251 let begin_frame_index_offset = cursor.offset();
4252 let begin_frame_index = cursor.read_i32()?;
4253 let end_frame_index_offset = cursor.offset();
4254 let end_frame_index = cursor.read_i32()?;
4255 let audio_enabled = cursor.read_bool()?;
4256 let audio_path_offset = cursor.offset();
4257 let audio_path = cursor.read_fixed_string(PMM_PATH_BYTE_LENGTH)?;
4258 let background_video_offset = [cursor.read_i32()?, cursor.read_i32()?];
4259 let background_video_scale_factor = cursor.read_f32()?;
4260 let background_video_path_offset = cursor.offset();
4261 let background_video_path = cursor.read_fixed_string(PMM_PATH_BYTE_LENGTH)?;
4262 let background_video_enabled = cursor.read_i32()? != 0;
4263 let background_image_offset = [cursor.read_i32()?, cursor.read_i32()?];
4264 let background_image_scale_factor = cursor.read_f32()?;
4265 let background_image_path_offset = cursor.offset();
4266 let background_image_path = cursor.read_fixed_string(PMM_PATH_BYTE_LENGTH)?;
4267 let background_image_enabled = cursor.read_bool()?;
4268 let information_shown = cursor.read_bool()?;
4269 let grid_and_axis_shown = cursor.read_bool()?;
4270 let ground_shadow_shown = cursor.read_bool()?;
4271 let preferred_fps = cursor.read_f32()?;
4272 let screen_capture_mode = cursor.read_i32()?;
4273 let accessory_index_after_models = cursor.read_i32()?;
4274 let ground_shadow_brightness = cursor.read_f32()?;
4275 let translucent_ground_shadow_enabled = cursor.read_bool()?;
4276 let physics_simulation_mode = cursor.read_u8()?;
4277
4278 Some(PmmDocumentSettingsBeforeGravity {
4279 offset,
4280 current_frame_index,
4281 current_frame_index_offset,
4282 horizontal_scroll,
4283 horizontal_scroll_thumb,
4284 editing_mode,
4285 camera_look_mode,
4286 loop_enabled,
4287 begin_frame_index_enabled,
4288 begin_frame_index_enabled_offset,
4289 end_frame_index_enabled,
4290 end_frame_index_enabled_offset,
4291 begin_frame_index,
4292 begin_frame_index_offset,
4293 end_frame_index,
4294 end_frame_index_offset,
4295 audio_enabled,
4296 audio_path,
4297 audio_path_offset,
4298 background_video_offset,
4299 background_video_scale_factor,
4300 background_video_path,
4301 background_video_path_offset,
4302 background_video_enabled,
4303 background_image_offset,
4304 background_image_scale_factor,
4305 background_image_path,
4306 background_image_path_offset,
4307 background_image_enabled,
4308 information_shown,
4309 grid_and_axis_shown,
4310 ground_shadow_shown,
4311 preferred_fps,
4312 screen_capture_mode,
4313 accessory_index_after_models,
4314 ground_shadow_brightness,
4315 translucent_ground_shadow_enabled,
4316 physics_simulation_mode,
4317 })
4318}
4319
4320fn read_document_gravity_summary(
4321 cursor: &mut PmmDocumentCursor<'_>,
4322) -> Option<PmmDocumentTrackSummary> {
4323 let offset = cursor.offset();
4324 let state_offset = cursor.offset();
4325 cursor.read_f32()?;
4326 cursor.read_i32()?;
4327 cursor.skip(12)?;
4328 cursor.read_bool()?;
4329 let state_end_offset = cursor.offset();
4330 let initial_keyframe = read_document_gravity_keyframe(cursor, false)?;
4331 let keyframe_count_offset = cursor.offset();
4332 let keyframes = usize_from_i32(cursor.read_i32()?)?;
4333 let keyframes_offset = cursor.offset();
4334 let mut keyframe_summaries = Vec::with_capacity(keyframes);
4335 for _ in 0..keyframes {
4336 keyframe_summaries.push(read_document_gravity_keyframe(cursor, true)?);
4337 }
4338 let keyframes_end_offset = cursor.offset();
4339 Some(PmmDocumentTrackSummary {
4340 offset,
4341 offset_end: keyframes_end_offset,
4342 initial_keyframes: 1,
4343 keyframes,
4344 initial_keyframe: Some(initial_keyframe),
4345 keyframe_summaries,
4346 keyframe_count_offset,
4347 keyframes_offset,
4348 keyframes_end_offset,
4349 state_offset: Some(state_offset),
4350 state_end_offset: Some(state_end_offset),
4351 })
4352}
4353
4354fn read_document_gravity_keyframe(
4355 cursor: &mut PmmDocumentCursor<'_>,
4356 include_index: bool,
4357) -> Option<PmmDocumentKeyframeSummary> {
4358 let offset = cursor.offset();
4359 let base = read_document_base_keyframe(cursor, include_index)?;
4360 let payload_offset = cursor.offset();
4361 let noise_enabled_offset = cursor.offset();
4362 let noise_enabled = cursor.read_bool()?;
4363 let noise_offset = cursor.offset();
4364 let noise = cursor.read_i32()?;
4365 let acceleration_offset = cursor.offset();
4366 let acceleration = cursor.read_f32()?;
4367 let direction_offset = cursor.offset();
4368 let direction = cursor.read_f32x3()?;
4369 let direction_byte_length = cursor.offset() - direction_offset;
4370 let selected_offset = cursor.offset();
4371 let selected = cursor.read_bool()?;
4372 let byte_length = cursor.offset() - offset;
4373 let payload_byte_length = cursor.offset() - payload_offset;
4374 let payload_bytes = cursor.data().get(payload_offset..cursor.offset())?.to_vec();
4375 Some(PmmDocumentKeyframeSummary::Gravity {
4376 index: base.index,
4377 frame_index: base.frame_index,
4378 previous_keyframe_index: base.previous_keyframe_index,
4379 next_keyframe_index: base.next_keyframe_index,
4380 noise_enabled,
4381 noise,
4382 acceleration,
4383 direction,
4384 selected,
4385 noise_enabled_offset,
4386 noise_offset,
4387 acceleration_offset,
4388 direction_offset,
4389 direction_byte_length,
4390 selected_offset,
4391 offset,
4392 byte_length,
4393 payload_offset,
4394 payload_byte_length,
4395 payload_bytes,
4396 })
4397}
4398
4399fn read_document_self_shadow_summary(
4400 cursor: &mut PmmDocumentCursor<'_>,
4401) -> Option<PmmDocumentTrackSummary> {
4402 let offset = cursor.offset();
4403 let state_offset = cursor.offset();
4404 cursor.read_bool()?;
4405 cursor.read_f32()?;
4406 let state_end_offset = cursor.offset();
4407 let initial_keyframe = read_document_self_shadow_keyframe(cursor, false)?;
4408 let keyframe_count_offset = cursor.offset();
4409 let keyframes = usize_from_i32(cursor.read_i32()?)?;
4410 let keyframes_offset = cursor.offset();
4411 let mut keyframe_summaries = Vec::with_capacity(keyframes);
4412 for _ in 0..keyframes {
4413 keyframe_summaries.push(read_document_self_shadow_keyframe(cursor, true)?);
4414 }
4415 let keyframes_end_offset = cursor.offset();
4416 Some(PmmDocumentTrackSummary {
4417 offset,
4418 offset_end: keyframes_end_offset,
4419 initial_keyframes: 1,
4420 keyframes,
4421 initial_keyframe: Some(initial_keyframe),
4422 keyframe_summaries,
4423 keyframe_count_offset,
4424 keyframes_offset,
4425 keyframes_end_offset,
4426 state_offset: Some(state_offset),
4427 state_end_offset: Some(state_end_offset),
4428 })
4429}
4430
4431fn read_document_self_shadow_keyframe(
4432 cursor: &mut PmmDocumentCursor<'_>,
4433 include_index: bool,
4434) -> Option<PmmDocumentKeyframeSummary> {
4435 let offset = cursor.offset();
4436 let base = read_document_base_keyframe(cursor, include_index)?;
4437 let payload_offset = cursor.offset();
4438 let mode_offset = cursor.offset();
4439 let mode = cursor.read_u8()?;
4440 let distance_offset = cursor.offset();
4441 let distance = cursor.read_f32()?;
4442 let selected_offset = cursor.offset();
4443 let selected = cursor.read_bool()?;
4444 let byte_length = cursor.offset() - offset;
4445 let payload_byte_length = cursor.offset() - payload_offset;
4446 let payload_bytes = cursor.data().get(payload_offset..cursor.offset())?.to_vec();
4447 Some(PmmDocumentKeyframeSummary::SelfShadow {
4448 index: base.index,
4449 frame_index: base.frame_index,
4450 previous_keyframe_index: base.previous_keyframe_index,
4451 next_keyframe_index: base.next_keyframe_index,
4452 mode,
4453 distance,
4454 selected,
4455 mode_offset,
4456 distance_offset,
4457 selected_offset,
4458 offset,
4459 byte_length,
4460 payload_offset,
4461 payload_byte_length,
4462 payload_bytes,
4463 })
4464}
4465
4466fn finish_document_settings_summary(
4467 before_gravity: PmmDocumentSettingsBeforeGravity,
4468 cursor: &mut PmmDocumentCursor<'_>,
4469 model_count: usize,
4470) -> Option<PmmDocumentSettingsSummary> {
4471 let edge_color = [
4472 cursor.read_i32()? as f32 / 255.0,
4473 cursor.read_i32()? as f32 / 255.0,
4474 cursor.read_i32()? as f32 / 255.0,
4475 ];
4476 let black_background_enabled = cursor.read_bool()?;
4477 let camera_look_at_model_index = cursor.read_i32()?;
4478 let camera_look_at_model_bone_index = cursor.read_i32()?;
4479 let unknown_matrix_offset = cursor.offset();
4480 cursor.skip(16 * 4)?;
4481 let unknown_matrix_end_offset = cursor.offset();
4482 let following_look_at_enabled = cursor.read_bool()?;
4483 cursor.read_bool()?;
4484 let physics_ground_enabled = cursor.read_bool()?;
4485 let current_frame_index_in_text_field_offset = cursor.offset();
4486 let current_frame_index_in_text_field = cursor.read_i32()?;
4487
4488 let mut model_selection_footer_present = false;
4489 let mut model_selection_footer_offset = None;
4490 let mut model_selection_footer_end_offset = None;
4491 if cursor.offset() < cursor.data().len() {
4492 let footer_offset = cursor.offset();
4493 if cursor.read_u8()? != 0 {
4494 model_selection_footer_present = true;
4495 model_selection_footer_offset = Some(footer_offset);
4496 for _ in 0..model_count {
4497 cursor.read_u8()?;
4498 cursor.read_i32()?;
4499 }
4500 model_selection_footer_end_offset = Some(cursor.offset());
4501 }
4502 }
4503 let offset_end = cursor.offset();
4504
4505 Some(PmmDocumentSettingsSummary {
4506 offset: before_gravity.offset,
4507 offset_end,
4508 current_frame_index: before_gravity.current_frame_index,
4509 current_frame_index_offset: before_gravity.current_frame_index_offset,
4510 horizontal_scroll: before_gravity.horizontal_scroll,
4511 horizontal_scroll_thumb: before_gravity.horizontal_scroll_thumb,
4512 editing_mode: before_gravity.editing_mode,
4513 camera_look_mode: before_gravity.camera_look_mode,
4514 loop_enabled: before_gravity.loop_enabled,
4515 begin_frame_index_enabled: before_gravity.begin_frame_index_enabled,
4516 begin_frame_index_enabled_offset: before_gravity.begin_frame_index_enabled_offset,
4517 end_frame_index_enabled: before_gravity.end_frame_index_enabled,
4518 end_frame_index_enabled_offset: before_gravity.end_frame_index_enabled_offset,
4519 begin_frame_index: before_gravity.begin_frame_index,
4520 begin_frame_index_offset: before_gravity.begin_frame_index_offset,
4521 end_frame_index: before_gravity.end_frame_index,
4522 end_frame_index_offset: before_gravity.end_frame_index_offset,
4523 audio_enabled: before_gravity.audio_enabled,
4524 audio_path: before_gravity.audio_path,
4525 audio_path_offset: before_gravity.audio_path_offset,
4526 background_video_offset: before_gravity.background_video_offset,
4527 background_video_scale_factor: before_gravity.background_video_scale_factor,
4528 background_video_path: before_gravity.background_video_path,
4529 background_video_path_offset: before_gravity.background_video_path_offset,
4530 background_video_enabled: before_gravity.background_video_enabled,
4531 background_image_offset: before_gravity.background_image_offset,
4532 background_image_scale_factor: before_gravity.background_image_scale_factor,
4533 background_image_path: before_gravity.background_image_path,
4534 background_image_path_offset: before_gravity.background_image_path_offset,
4535 background_image_enabled: before_gravity.background_image_enabled,
4536 information_shown: before_gravity.information_shown,
4537 grid_and_axis_shown: before_gravity.grid_and_axis_shown,
4538 ground_shadow_shown: before_gravity.ground_shadow_shown,
4539 preferred_fps: before_gravity.preferred_fps,
4540 screen_capture_mode: before_gravity.screen_capture_mode,
4541 accessory_index_after_models: before_gravity.accessory_index_after_models,
4542 ground_shadow_brightness: before_gravity.ground_shadow_brightness,
4543 translucent_ground_shadow_enabled: before_gravity.translucent_ground_shadow_enabled,
4544 physics_simulation_mode: before_gravity.physics_simulation_mode,
4545 edge_color,
4546 black_background_enabled,
4547 camera_look_at_model_index,
4548 camera_look_at_model_bone_index,
4549 unknown_matrix_offset,
4550 unknown_matrix_end_offset,
4551 following_look_at_enabled,
4552 physics_ground_enabled,
4553 current_frame_index_in_text_field,
4554 current_frame_index_in_text_field_offset,
4555 model_selection_footer_present,
4556 model_selection_footer_offset,
4557 model_selection_footer_end_offset,
4558 })
4559}
4560
4561fn read_document_bone_state(
4562 cursor: &mut PmmDocumentCursor<'_>,
4563) -> Option<PmmDocumentBoneStateSummary> {
4564 let translation = cursor.read_f32x3()?;
4565 let orientation = cursor.read_f32x4()?;
4566 let dirty = cursor.read_bool()?;
4567 let physics_disabled = cursor.read_bool()?;
4568 let selected = cursor.read_bool()?;
4569 Some(PmmDocumentBoneStateSummary {
4570 translation,
4571 orientation,
4572 dirty,
4573 physics_disabled,
4574 selected,
4575 })
4576}
4577
4578fn read_document_morph_state(
4579 cursor: &mut PmmDocumentCursor<'_>,
4580) -> Option<PmmDocumentMorphStateSummary> {
4581 let weight = cursor.read_f32()?;
4582 Some(PmmDocumentMorphStateSummary { weight })
4583}
4584
4585fn read_document_constraint_state(
4586 cursor: &mut PmmDocumentCursor<'_>,
4587) -> Option<PmmDocumentConstraintStateSummary> {
4588 let enabled = cursor.read_bool()?;
4589 Some(PmmDocumentConstraintStateSummary { enabled })
4590}
4591
4592fn read_document_outside_parent_state(
4593 cursor: &mut PmmDocumentCursor<'_>,
4594) -> Option<PmmDocumentOutsideParentStateSummary> {
4595 let parent_model_index = cursor.read_i32()?;
4596 let parent_model_bone_index = cursor.read_i32()?;
4597 let subject_bone_index = cursor.read_i32()?;
4598 let target_model_index = cursor.read_i32()?;
4599 Some(PmmDocumentOutsideParentStateSummary {
4600 parent_model_index,
4601 parent_model_bone_index,
4602 subject_bone_index,
4603 target_model_index,
4604 })
4605}
4606
4607fn read_document_base_keyframe(
4608 cursor: &mut PmmDocumentCursor<'_>,
4609 include_index: bool,
4610) -> Option<PmmDocumentBaseKeyframeSummary> {
4611 let index = if include_index {
4612 Some(cursor.read_i32()?)
4613 } else {
4614 None
4615 };
4616 let frame_index = cursor.read_i32()?;
4617 let previous_keyframe_index = cursor.read_i32()?;
4618 let next_keyframe_index = cursor.read_i32()?;
4619 Some(PmmDocumentBaseKeyframeSummary {
4620 index,
4621 frame_index,
4622 previous_keyframe_index,
4623 next_keyframe_index,
4624 })
4625}
4626
4627fn unpack_document_accessory_opacity_and_visible(value: u8) -> (f32, bool) {
4628 let visible = value & 1 != 0;
4629 let opacity = 1.0 - ((value >> 1) as f32 / 100.0);
4630 (opacity, visible)
4631}
4632
4633fn summarize_document_counts(models: &[PmmDocumentModelSummary]) -> PmmDocumentCounts {
4634 PmmDocumentCounts {
4635 models: models.len(),
4636 bones: models.iter().map(|model| model.bone_count).sum(),
4637 morphs: models.iter().map(|model| model.morph_count).sum(),
4638 initial_bone_keyframes: models
4639 .iter()
4640 .map(|model| model.initial_bone_keyframes)
4641 .sum(),
4642 bone_keyframes: models.iter().map(|model| model.bone_keyframes).sum(),
4643 initial_morph_keyframes: models
4644 .iter()
4645 .map(|model| model.initial_morph_keyframes)
4646 .sum(),
4647 morph_keyframes: models.iter().map(|model| model.morph_keyframes).sum(),
4648 initial_model_keyframes: models
4649 .iter()
4650 .map(|model| model.initial_model_keyframes)
4651 .sum(),
4652 model_keyframes: models.iter().map(|model| model.model_keyframes).sum(),
4653 }
4654}
4655
4656fn usize_from_i32(value: i32) -> Option<usize> {
4657 usize::try_from(value).ok()
4658}
4659
4660fn decode_shift_jis(bytes: &[u8]) -> String {
4661 decode_sjis_trim_nul(bytes)
4662}
4663
4664#[derive(Debug, Clone)]
4665struct PmmModelSlotScan {
4666 slots: Vec<PmmModelSlot>,
4667 stop: Option<PmmModelSlotScanStop>,
4668}
4669
4670#[derive(Debug, Clone)]
4671struct PmmModelSlotScanStop {
4672 offset: usize,
4673 reason: &'static str,
4674}
4675
4676fn parse_model_slots_from_header(
4677 data: &[u8],
4678 references: &[PmmAssetReference],
4679 display_state: &PmmDisplayState,
4680) -> PmmModelSlotScan {
4681 const INITIAL_MODEL_SLOT_OFFSET: usize = 56;
4682 if display_state.layout != "pmm-v2-flags" {
4683 return PmmModelSlotScan {
4684 slots: Vec::new(),
4685 stop: None,
4686 };
4687 }
4688 let declared_count = display_state.non_zero_model_slot_count.max(1);
4689 let mut slots = Vec::new();
4690 let mut offset = INITIAL_MODEL_SLOT_OFFSET;
4691 let mut stop = None;
4692
4693 for slot_index in 0..declared_count {
4694 let require_reference_at_offset = slot_index > 0;
4695 match parse_model_slot_at(
4696 data,
4697 references,
4698 offset,
4699 slot_index,
4700 display_state
4701 .active_model_slot_indices
4702 .get(slot_index)
4703 .copied(),
4704 require_reference_at_offset,
4705 ) {
4706 Ok((slot, next_offset)) => {
4707 slots.push(slot);
4708 offset = next_offset;
4709 }
4710 Err(reason) => {
4711 if !slots.is_empty() && slot_index < declared_count {
4712 stop = Some(PmmModelSlotScanStop { offset, reason });
4713 }
4714 break;
4715 }
4716 }
4717 }
4718
4719 PmmModelSlotScan { slots, stop }
4720}
4721
4722fn parse_model_slot_at(
4723 data: &[u8],
4724 references: &[PmmAssetReference],
4725 offset: usize,
4726 slot_index: usize,
4727 display_slot_index: Option<usize>,
4728 require_reference_at_offset: bool,
4729) -> Result<(PmmModelSlot, usize), &'static str> {
4730 let (name, name_bytes, after_name) =
4731 read_pmm_len_prefixed_sjis(data, offset).ok_or("missing_name")?;
4732 let (english_name, english_name_bytes, path_offset) =
4733 read_pmm_len_prefixed_sjis(data, after_name).ok_or("missing_english_name")?;
4734 let path_end = data
4735 .get(path_offset..)
4736 .ok_or("missing_model_path")?
4737 .iter()
4738 .position(|byte| *byte == 0)
4739 .map(|relative| path_offset + relative)
4740 .ok_or("missing_path_terminator")?;
4741 if path_end == path_offset {
4742 return Err("empty_path");
4743 }
4744 let decoded_path = decode_sjis(&data[path_offset..path_end]);
4745 let model_path = find_asset_candidates(&decoded_path)
4746 .into_iter()
4747 .find(|candidate| {
4748 candidate
4749 .rsplit_once('.')
4750 .map(|(_, ext)| matches!(ext.to_ascii_lowercase().as_str(), "pmx" | "pmd"))
4751 .unwrap_or(false)
4752 })
4753 .ok_or("missing_model_path")?;
4754 let normalized_path = normalize_path(&model_path);
4755 let asset_reference_index = references.iter().position(|reference| {
4756 reference.kind == "model"
4757 && reference
4758 .normalized_path
4759 .eq_ignore_ascii_case(&normalized_path)
4760 && (!require_reference_at_offset || reference.offset == offset)
4761 });
4762 if require_reference_at_offset && asset_reference_index.is_none() {
4763 return Err("asset_reference_not_matched");
4764 }
4765 let confidence = asset_reference_index
4766 .and_then(|index| references.get(index).map(|reference| reference.confidence))
4767 .unwrap_or_else(|| asset_reference_confidence(&model_path, &normalized_path));
4768 let padding_start = path_end + 1;
4769 let trailing_zero_padding_bytes = data
4770 .get(padding_start..)
4771 .map(|bytes| bytes.iter().take_while(|byte| **byte == 0).count())
4772 .unwrap_or(0);
4773 let next_non_zero_offset = data
4774 .get(padding_start..)
4775 .and_then(|bytes| bytes.iter().position(|byte| *byte != 0))
4776 .map(|relative| padding_start + relative);
4777
4778 Ok((
4779 PmmModelSlot {
4780 slot_index,
4781 display_slot_index,
4782 offset,
4783 offset_end: path_end,
4784 model_path_offset: path_offset,
4785 trailing_zero_padding_bytes,
4786 next_non_zero_offset,
4787 name,
4788 name_bytes,
4789 english_name,
4790 english_name_bytes,
4791 model_path,
4792 normalized_path,
4793 asset_reference_index,
4794 confidence,
4795 },
4796 path_end + 1,
4797 ))
4798}
4799
4800fn read_pmm_len_prefixed_sjis(data: &[u8], offset: usize) -> Option<(String, Vec<u8>, usize)> {
4801 let length = *data.get(offset)? as usize;
4802 if length == 0 || length > 128 {
4803 return None;
4804 }
4805 let start = offset + 1;
4806 let end = start.checked_add(length)?;
4807 let bytes = data.get(start..end)?;
4808 if bytes.contains(&0) {
4809 return None;
4810 }
4811 Some((decode_sjis_trim_nul(bytes), bytes.to_vec(), end))
4812}
4813
4814fn extract_asset_references(data: &[u8]) -> Vec<PmmAssetReference> {
4815 let mut refs = Vec::new();
4816 let mut chunk_start = 0usize;
4817 for index in 0..=data.len() {
4818 if index < data.len() && data[index] != 0 {
4819 continue;
4820 }
4821 if index > chunk_start {
4822 let decoded = decode_sjis(&data[chunk_start..index]);
4823 for candidate in find_asset_candidates(&decoded) {
4824 let normalized = normalize_path(&candidate);
4825 if refs.iter().any(|existing: &PmmAssetReference| {
4826 existing.normalized_path.eq_ignore_ascii_case(&normalized)
4827 }) {
4828 continue;
4829 }
4830 let file_name = normalized
4831 .rsplit('/')
4832 .next()
4833 .unwrap_or(&normalized)
4834 .to_owned();
4835 let extension = file_name
4836 .rsplit_once('.')
4837 .map(|(_, ext)| ext.to_ascii_lowercase())
4838 .unwrap_or_default();
4839 refs.push(PmmAssetReference {
4840 confidence: asset_reference_confidence(&candidate, &normalized),
4841 path: candidate,
4842 normalized_path: normalized,
4843 file_name,
4844 kind: classify_extension(&extension),
4845 extension,
4846 offset: chunk_start,
4847 offset_end: index,
4848 });
4849 }
4850 }
4851 chunk_start = index + 1;
4852 }
4853 refs.sort_by_key(|reference| reference.offset);
4854 refs
4855}
4856
4857fn asset_reference_confidence(path: &str, normalized_path: &str) -> &'static str {
4858 if path.contains('\u{FFFD}')
4859 || normalized_path.starts_with('/')
4860 || normalized_path.starts_with(":/")
4861 {
4862 return "low";
4863 }
4864 if normalized_path.contains('/') || last_windows_drive_path_start(path).is_some() {
4865 return "high";
4866 }
4867 "medium"
4868}
4869
4870fn find_asset_candidates(text: &str) -> Vec<String> {
4871 let mut candidates = Vec::new();
4872 for segment in text.split(|c: char| c.is_control() || c == '"' || c == '\'') {
4873 let lower = segment.to_ascii_lowercase();
4874 let mut search_from = 0usize;
4875 while let Some((relative_end, extension)) = next_asset_extension(&lower[search_from..]) {
4876 let end = search_from + relative_end + extension.len();
4877 let start = asset_candidate_start(segment, end);
4878 let candidate = segment[start..end].trim();
4879 if has_asset_file_stem(candidate)
4880 && !candidates
4881 .iter()
4882 .any(|existing: &String| existing.eq_ignore_ascii_case(candidate))
4883 {
4884 candidates.push(candidate.to_owned());
4885 }
4886 search_from = end;
4887 }
4888 }
4889 candidates
4890}
4891
4892fn next_asset_extension(text: &str) -> Option<(usize, &'static str)> {
4893 [
4894 ".pmd", ".pmx", ".vmd", ".vac", ".x", ".wav", ".mp3", ".ogg", ".bmp", ".tga", ".png",
4895 ".jpg", ".jpeg", ".dds", ".avi", ".avs", ".mp4", ".wmv",
4896 ]
4897 .iter()
4898 .filter_map(|extension| {
4899 find_extension_with_boundary(text, extension).map(|index| (index, *extension))
4900 })
4901 .min_by_key(|(index, _)| *index)
4902}
4903
4904fn find_extension_with_boundary(text: &str, extension: &str) -> Option<usize> {
4905 let mut search_from = 0usize;
4906 while let Some(relative_index) = text[search_from..].find(extension) {
4907 let index = search_from + relative_index;
4908 let after = index + extension.len();
4909 if text[after..]
4910 .chars()
4911 .next()
4912 .is_none_or(|ch| ch.is_whitespace())
4913 {
4914 return Some(index);
4915 }
4916 search_from = after;
4917 }
4918 None
4919}
4920
4921fn asset_candidate_start(segment: &str, end: usize) -> usize {
4922 let prefix = &segment[..end];
4923 let lower = prefix.to_ascii_lowercase();
4924 if let Some(index) = last_userfile_path_start(&lower) {
4925 return index;
4926 }
4927 if let Some(index) = last_windows_drive_path_start(prefix) {
4928 return index;
4929 }
4930 prefix
4931 .rfind(char::is_whitespace)
4932 .map(|index| index + 1)
4933 .unwrap_or(0)
4934}
4935
4936fn last_userfile_path_start(value: &str) -> Option<usize> {
4937 ["userfile\\", "userfile/"]
4938 .iter()
4939 .flat_map(|needle| value.match_indices(needle).map(|(index, _)| index))
4940 .filter(|index| {
4941 *index == 0
4942 || value.as_bytes()[index - 1] == b'\\'
4943 || value.as_bytes()[index - 1] == b'/'
4944 })
4945 .last()
4946}
4947
4948fn last_windows_drive_path_start(value: &str) -> Option<usize> {
4949 let bytes = value.as_bytes();
4950 if bytes.len() < 3 {
4951 return None;
4952 }
4953 let mut last = None;
4954 for index in 0..=bytes.len() - 3 {
4955 if bytes[index].is_ascii_alphabetic()
4956 && bytes[index + 1] == b':'
4957 && (bytes[index + 2] == b'\\' || bytes[index + 2] == b'/')
4958 {
4959 last = Some(index);
4960 }
4961 }
4962 last
4963}
4964
4965fn has_asset_file_stem(value: &str) -> bool {
4966 let Some((stem, _)) = value
4967 .rsplit(['\\', '/'])
4968 .next()
4969 .and_then(|file_name| file_name.rsplit_once('.'))
4970 else {
4971 return false;
4972 };
4973 if stem.is_empty() {
4974 return false;
4975 }
4976 if value.contains(['\\', '/']) || last_windows_drive_path_start(value).is_some() {
4977 return true;
4978 }
4979 !stem
4980 .chars()
4981 .any(|c| c.is_control() || matches!(c, '<' | '>' | ':' | '"' | '|' | '?' | '*' | '='))
4982}
4983
4984fn normalize_path(value: &str) -> String {
4985 let normalized = value.replace('\\', "/").replace("//", "/");
4986 let lower = normalized.to_ascii_lowercase();
4987 match last_userfile_path_start(&lower) {
4988 Some(index) => normalized[index..].to_owned(),
4989 None => normalized,
4990 }
4991}
4992
4993fn classify_extension(extension: &str) -> &'static str {
4994 match extension {
4995 "pmd" | "pmx" => "model",
4996 "x" | "vac" => "accessory",
4997 "vmd" => "motion",
4998 "wav" | "mp3" | "ogg" => "audio",
4999 "bmp" | "tga" | "png" | "jpg" | "jpeg" | "dds" => "image",
5000 "avi" | "avs" | "mp4" | "wmv" => "video",
5001 _ => "unknown",
5002 }
5003}
5004
5005fn paths_by_kind(references: &[PmmAssetReference], kind: &str) -> Vec<String> {
5006 references
5007 .iter()
5008 .filter(|reference| reference.kind == kind)
5009 .map(|reference| reference.normalized_path.clone())
5010 .collect()
5011}
5012
5013fn asset_reference_index_for_path(
5014 references: &[PmmAssetReference],
5015 kind: &str,
5016 path: &str,
5017) -> Option<usize> {
5018 let normalized = normalize_path(path);
5019 references.iter().position(|reference| {
5020 reference.kind == kind && reference.normalized_path.eq_ignore_ascii_case(&normalized)
5021 })
5022}
5023
5024fn scene_assets_by_kind(references: &[PmmAssetReference], kind: &str) -> Vec<PmmSceneAsset> {
5025 references
5026 .iter()
5027 .enumerate()
5028 .filter(|(_, reference)| reference.kind == kind)
5029 .enumerate()
5030 .map(|(kind_index, (reference_index, reference))| PmmSceneAsset {
5031 reference_index,
5032 kind_index,
5033 path: reference.path.clone(),
5034 normalized_path: reference.normalized_path.clone(),
5035 file_name: reference.file_name.clone(),
5036 extension: reference.extension.clone(),
5037 offset: reference.offset,
5038 offset_end: reference.offset_end,
5039 confidence: reference.confidence,
5040 })
5041 .collect()
5042}
5043
5044fn asset_summary(references: &[PmmAssetReference]) -> PmmAssetSummary {
5045 let mut kind_counts = BTreeMap::new();
5046 let mut extension_counts = BTreeMap::new();
5047 let mut confidence_counts = BTreeMap::new();
5048 for reference in references {
5049 *kind_counts.entry(reference.kind.to_owned()).or_insert(0) += 1;
5050 *extension_counts
5051 .entry(reference.extension.to_owned())
5052 .or_insert(0) += 1;
5053 *confidence_counts
5054 .entry(reference.confidence.to_owned())
5055 .or_insert(0) += 1;
5056 }
5057
5058 PmmAssetSummary {
5059 reference_count: references.len(),
5060 high_confidence_count: *confidence_counts.get("high").unwrap_or(&0),
5061 medium_confidence_count: *confidence_counts.get("medium").unwrap_or(&0),
5062 low_confidence_count: *confidence_counts.get("low").unwrap_or(&0),
5063 kind_counts,
5064 extension_counts,
5065 confidence_counts,
5066 }
5067}
5068
5069fn pmm_diagnostics(
5070 references: &[PmmAssetReference],
5071 model_slots: &[PmmModelSlot],
5072 document_summary: Option<&PmmDocumentSummary>,
5073 document_global_summary: Option<&PmmDocumentGlobalSummary>,
5074 display_state: &PmmDisplayState,
5075 model_slot_scan_stop: Option<&PmmModelSlotScanStop>,
5076 data: &[u8],
5077) -> Vec<PmmParserDiagnostic> {
5078 let mut diagnostics = vec![
5079 PmmParserDiagnostic {
5080 level: "warning",
5081 code: "PMM_PROJECT_GRAPH_PARTIAL",
5082 message: "PMM parser currently exposes header-derived project settings, timeline, display state, and manifest-derived asset references; binary project graph tracks are not fully decoded yet.".to_owned(),
5083 },
5084 PmmParserDiagnostic {
5085 level: "info",
5086 code: "PMM_ASSET_REFERENCES_SCAN",
5087 message: format!(
5088 "PMM asset references are extracted from decoded null-terminated text chunks and grouped by extension; references={}.",
5089 references.len()
5090 ),
5091 },
5092 ];
5093 if !model_slots.is_empty() {
5094 diagnostics.push(PmmParserDiagnostic {
5095 level: "info",
5096 code: "PMM_MODEL_SLOT_INITIAL_SLICE",
5097 message: format!(
5098 "PMM parser decoded model slot records from the initial fixed-header slice; modelSlots={}.",
5099 model_slots.len()
5100 ),
5101 });
5102 }
5103 if let Some(stop) = model_slot_scan_stop {
5104 diagnostics.push(PmmParserDiagnostic {
5105 level: "info",
5106 code: "PMM_MODEL_SLOT_SCAN_STOPPED",
5107 message: format!(
5108 "PMM model slot header scan stopped at offset {}: {}; decodedSlots={} declaredModelSlots={}.",
5109 stop.offset,
5110 stop.reason,
5111 model_slots.len(),
5112 display_state.non_zero_model_slot_count
5113 ),
5114 });
5115 }
5116 if let Some(summary) = document_summary {
5117 let total_bone_keyframes =
5118 summary.counts.initial_bone_keyframes + summary.counts.bone_keyframes;
5119 let total_morph_keyframes =
5120 summary.counts.initial_morph_keyframes + summary.counts.morph_keyframes;
5121 diagnostics.push(PmmParserDiagnostic {
5122 level: "info",
5123 code: "PMM_DOCUMENT_SUMMARY_PARTIAL",
5124 message: format!(
5125 "PMM parser decoded a PMMv2 document inventory slice with {} model(s), {} total bone keyframe(s) ({} additional), and {} total morph keyframe(s) ({} additional); full keyframe payloads and camera/light/self-shadow/property tracks are not exposed yet.",
5126 summary.model_count,
5127 total_bone_keyframes,
5128 summary.counts.bone_keyframes,
5129 total_morph_keyframes,
5130 summary.counts.morph_keyframes
5131 ),
5132 });
5133 }
5134 if let Some(summary) = document_global_summary {
5135 diagnostics.push(PmmParserDiagnostic {
5136 level: "info",
5137 code: "PMM_DOCUMENT_GLOBAL_SUMMARY_PARTIAL",
5138 message: format!(
5139 "PMM parser decoded a PMMv2 global document inventory slice with {} camera keyframe(s), {} light keyframe(s), {} accessory object(s), {} gravity keyframe(s), and {} self-shadow keyframe(s); full payloads are not exposed yet.",
5140 summary.camera.initial_keyframes + summary.camera.keyframes,
5141 summary.light.initial_keyframes + summary.light.keyframes,
5142 summary.accessories.accessory_count,
5143 summary.gravity.initial_keyframes + summary.gravity.keyframes,
5144 summary.self_shadow.initial_keyframes + summary.self_shadow.keyframes
5145 ),
5146 });
5147 }
5148 if display_state.layout == "pmm-v2-flags"
5149 && display_state.non_zero_model_slot_count > model_slots.len()
5150 {
5151 diagnostics.push(PmmParserDiagnostic {
5152 level: "warning",
5153 code: "PMM_MODEL_SLOT_COUNT_PARTIAL_DECODE",
5154 message: format!(
5155 "PMM displayState reports {} non-zero model slot flags, but only {} initial model slot(s) are decoded.",
5156 display_state.non_zero_model_slot_count,
5157 model_slots.len()
5158 ),
5159 });
5160 }
5161 diagnostics.extend(asset_count_mismatch_diagnostics(references, display_state));
5162 if display_state.layout == "unknown"
5163 && let Some(bytes) = data.get(46..54)
5164 && bytes.iter().any(|byte| *byte > 2)
5165 {
5166 diagnostics.push(PmmParserDiagnostic {
5167 level: "warning",
5168 code: "PMM_DISPLAY_STATE_UNPLAUSIBLE",
5169 message: format!(
5170 "PMM displayState.modelSlotFlags header bytes were outside the observed 0..=2 range and were omitted: {:?}.",
5171 bytes
5172 ),
5173 });
5174 }
5175 diagnostics.extend(duplicate_asset_reference_diagnostics(references));
5176 if references.iter().any(|reference| reference.kind == "model")
5177 && !references
5178 .iter()
5179 .any(|reference| reference.kind == "motion")
5180 {
5181 diagnostics.push(PmmParserDiagnostic {
5182 level: "warning",
5183 code: "PMM_MOTION_REFERENCES_NOT_FOUND_IN_SCAN",
5184 message: "PMM manifest-derived asset scan found model references but no motion references; motion paths may live in unparsed binary project graph sections.".to_owned(),
5185 });
5186 }
5187 diagnostics
5188}
5189
5190fn asset_count_mismatch_diagnostics(
5191 references: &[PmmAssetReference],
5192 display_state: &PmmDisplayState,
5193) -> Vec<PmmParserDiagnostic> {
5194 let mut diagnostics = Vec::new();
5195 let scanned_models = references
5196 .iter()
5197 .filter(|reference| reference.kind == "model")
5198 .count();
5199 if display_state.model_slot_count > 0 && scanned_models != display_state.model_slot_count {
5200 diagnostics.push(PmmParserDiagnostic {
5201 level: "info",
5202 code: "PMM_ASSET_COUNT_MISMATCH",
5203 message: format!(
5204 "PMM displayState declares {} model slot(s), but manifest-derived asset scan found {} model reference(s).",
5205 display_state.model_slot_count,
5206 scanned_models
5207 ),
5208 });
5209 }
5210
5211 if let Some(accessory_count) = display_state.accessory_slot_count {
5212 let scanned_accessories = references
5213 .iter()
5214 .filter(|reference| reference.kind == "accessory")
5215 .count();
5216 if scanned_accessories != accessory_count as usize {
5217 diagnostics.push(PmmParserDiagnostic {
5218 level: "info",
5219 code: "PMM_ASSET_COUNT_MISMATCH",
5220 message: format!(
5221 "PMM displayState declares {} accessory slot(s), but manifest-derived asset scan found {} accessory reference(s).",
5222 accessory_count,
5223 scanned_accessories
5224 ),
5225 });
5226 }
5227 }
5228 diagnostics
5229}
5230
5231fn duplicate_asset_reference_diagnostics(
5232 references: &[PmmAssetReference],
5233) -> Vec<PmmParserDiagnostic> {
5234 let mut by_file = HashMap::<(String, String), Vec<(usize, &PmmAssetReference)>>::new();
5235 for (index, reference) in references.iter().enumerate() {
5236 by_file
5237 .entry((reference.file_name.clone(), reference.extension.clone()))
5238 .or_default()
5239 .push((index, reference));
5240 }
5241
5242 let mut diagnostics = Vec::new();
5243 for ((file_name, extension), entries) in by_file {
5244 if entries.len() < 2 {
5245 continue;
5246 }
5247 let mut unique_paths = entries
5248 .iter()
5249 .map(|(_, reference)| reference.normalized_path.to_ascii_lowercase())
5250 .collect::<Vec<_>>();
5251 unique_paths.sort();
5252 unique_paths.dedup();
5253 if unique_paths.len() < 2 {
5254 continue;
5255 }
5256 let indices = entries
5257 .iter()
5258 .map(|(index, _)| index.to_string())
5259 .collect::<Vec<_>>()
5260 .join(",");
5261 diagnostics.push(PmmParserDiagnostic {
5262 level: "warning",
5263 code: "PMM_ASSET_REFERENCE_DUPLICATE",
5264 message: format!(
5265 "PMM manifest-derived asset scan found duplicate file names with different paths: fileName={file_name:?} extension={extension:?} referenceIndices={indices}."
5266 ),
5267 });
5268 }
5269 diagnostics.sort_by(|a, b| a.message.cmp(&b.message));
5270 diagnostics
5271}
5272
5273#[cfg(test)]
5274mod tests {
5275 use super::*;
5276
5277 #[test]
5278 fn normalize_pmm_model_item_name_fits_sjis_without_splitting_character() {
5279 assert_eq!(
5280 normalize_pmm_model_item_name("123456789あいうえおか"),
5281 "123456789あいうえお"
5282 );
5283 }
5284
5285 fn pmm_with_project_settings() -> Vec<u8> {
5286 let mut data = b"Polygon Movie maker 0002".to_vec();
5287 data.resize(30, 0);
5288 data.extend_from_slice(&1920u32.to_le_bytes());
5289 data.extend_from_slice(&1080u32.to_le_bytes());
5290 data.extend_from_slice(&424u32.to_le_bytes());
5291 data.extend_from_slice(&30.0f32.to_le_bytes());
5292 data.extend_from_slice(&[1, 1, 2, 0, 0, 0, 0, 0]);
5293 data.push(3);
5294 data.push(0);
5295 data.extend_from_slice(
5296 b"UserFile\\Model\\miku.pmx\0UserFile\\Motion\\walk.vmd\0UserFile\\Audio\\song.wav\0",
5297 );
5298 data
5299 }
5300
5301 fn pmm_with_initial_model_slot() -> Vec<u8> {
5302 let mut data = b"Polygon Movie maker 0002".to_vec();
5303 data.resize(30, 0);
5304 data.extend_from_slice(&640u32.to_le_bytes());
5305 data.extend_from_slice(&360u32.to_le_bytes());
5306 data.extend_from_slice(&250u32.to_le_bytes());
5307 data.extend_from_slice(&30.0f32.to_le_bytes());
5308 data.extend_from_slice(&[0, 1, 1, 1, 1, 1, 1, 0]);
5309 data.push(1);
5310 data.push(0);
5311 data.push(9);
5312 data.extend_from_slice(b"TestModel");
5313 data.push(4);
5314 data.extend_from_slice(b"Base");
5315 data.extend_from_slice(b"C:\\MMDDev\\data\\unittest\\test_1bone_cube.pmx");
5316 data.push(0);
5317 data.extend_from_slice(b"UserFile\\Motion\\walk.vmd\0");
5318 data
5319 }
5320
5321 #[test]
5322 fn exports_pmm_project_settings_and_asset_references() {
5323 let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
5324 let exported = export_pmm_manifest(&parsed);
5325 let reparsed = parse_pmm_manifest(&exported).unwrap();
5326
5327 assert_eq!(reparsed.version, "0002");
5328 assert_eq!(reparsed.project_settings.screen_width, Some(1920));
5329 assert_eq!(reparsed.project_settings.screen_height, Some(1080));
5330 assert_eq!(reparsed.project_settings.timeline_frame_count, Some(424));
5331 assert_eq!(reparsed.project_settings.frame_rate, Some(30.0));
5332 assert_eq!(reparsed.model_paths, vec!["UserFile/Model/miku.pmx"]);
5333 assert_eq!(reparsed.motion_paths, vec!["UserFile/Motion/walk.vmd"]);
5334 assert_eq!(reparsed.audio_paths, vec!["UserFile/Audio/song.wav"]);
5335 }
5336
5337 #[test]
5338 fn exports_pmm_manifest_preserves_v1_input_bytes() {
5339 let mut data = pmm_with_project_settings();
5340 data[20..24].copy_from_slice(b"0001");
5341 let parsed = parse_pmm_manifest(&data).unwrap();
5342 assert_eq!(parsed.version, "0001");
5343
5344 let exported = export_pmm_manifest(&parsed);
5345 assert_eq!(
5346 exported, data,
5347 "PMMv1 input must be preserved byte-for-byte"
5348 );
5349 let reparsed = parse_pmm_manifest(&exported).unwrap();
5350
5351 assert_eq!(reparsed.version, "0001");
5352 assert_eq!(reparsed.project_settings.timeline_frame_count, Some(424));
5353 assert_eq!(reparsed.model_paths, vec!["UserFile/Model/miku.pmx"]);
5354 }
5355
5356 #[test]
5357 fn exports_pmm_initial_model_slot() {
5358 let parsed = parse_pmm_manifest(&pmm_with_initial_model_slot()).unwrap();
5359 let exported = export_pmm_manifest(&parsed);
5360 let reparsed = parse_pmm_manifest(&exported).unwrap();
5361
5362 assert_eq!(reparsed.project_settings.screen_width, Some(640));
5363 assert_eq!(reparsed.project_settings.screen_height, Some(360));
5364 assert_eq!(reparsed.project_settings.timeline_frame_count, Some(250));
5365 assert_eq!(reparsed.model_slots.len(), 1);
5366 let slot = &reparsed.model_slots[0];
5367 assert_eq!(slot.name, "TestModel");
5368 assert_eq!(slot.english_name, "Base");
5369 assert_eq!(
5370 slot.normalized_path,
5371 "C:/MMDDev/data/unittest/test_1bone_cube.pmx"
5372 );
5373 assert_eq!(reparsed.motion_paths, vec!["UserFile/Motion/walk.vmd"]);
5374 }
5375
5376 #[test]
5377 fn pmm_export_roundtrip_bytes_v2_document_global() {
5378 let input = pmm_with_document_global_summary();
5379 let parsed = parse_pmm_manifest(&input).unwrap();
5380 let exported = export_pmm_manifest(&parsed);
5381 assert_eq!(
5382 exported, input,
5383 "parse->export must be byte-for-byte for PMMv2 document/global synthetic"
5384 );
5385 assert!(parsed.document_summary.is_some());
5386 assert!(parsed.document_global_summary.is_some());
5387 }
5388
5389 #[test]
5390 fn pmm_export_roundtrip_bytes_v2_document_summary() {
5391 let input = pmm_with_document_summary();
5392 let parsed = parse_pmm_manifest(&input).unwrap();
5393 let exported = export_pmm_manifest(&parsed);
5394 assert_eq!(
5395 exported, input,
5396 "parse->export must be byte-for-byte for PMMv2 document synthetic"
5397 );
5398 }
5399
5400 #[test]
5401 fn pmm_document_model_path_override_basic() {
5402 let input = pmm_with_document_summary();
5404 let parsed = parse_pmm_manifest(&input).unwrap();
5405 let document = parsed
5406 .document_summary
5407 .as_ref()
5408 .expect("document_summary required");
5409 let model0 = &document.models[0];
5410 assert_eq!(model0.document_model_index, 0);
5411 let original_path = model0.path.clone();
5412 let path_offset = model0.path_offset;
5413
5414 let new_path = "UserFile\\Model\\override_for_gui_smoke.pmx";
5415 let patched =
5416 export_pmm_manifest_with_document_model_path_overrides(&parsed, &[(0u8, new_path)])
5417 .expect("override patch must succeed for valid short path");
5418
5419 assert_eq!(
5420 patched.len(),
5421 input.len(),
5422 "patched bytes must keep identical length"
5423 );
5424
5425 let mut changed_positions = Vec::new();
5427 for (i, (a, b)) in input.iter().zip(patched.iter()).enumerate() {
5428 if a != b {
5429 changed_positions.push(i);
5430 }
5431 }
5432 assert!(
5433 !changed_positions.is_empty(),
5434 "expected at least the path bytes to change"
5435 );
5436 let field_start = path_offset;
5437 let field_end = path_offset + 256;
5438 for &pos in &changed_positions {
5439 assert!(
5440 pos >= field_start && pos < field_end,
5441 "changed byte at {} must be inside the document model path field [{}, {})",
5442 pos,
5443 field_start,
5444 field_end
5445 );
5446 }
5447
5448 let reparsed = parse_pmm_manifest(&patched).unwrap();
5449 let reparsed_doc = reparsed
5450 .document_summary
5451 .as_ref()
5452 .expect("reparsed must have document_summary");
5453 assert_eq!(
5454 reparsed_doc.models[0].path, new_path,
5455 "reparsed document model path must reflect the override"
5456 );
5457 assert_ne!(
5458 reparsed_doc.models[0].path, original_path,
5459 "reparsed path must differ from original"
5460 );
5461 }
5462
5463 #[test]
5464 fn pmm_document_model_path_override_too_long_returns_err() {
5465 let input = pmm_with_document_summary();
5466 let parsed = parse_pmm_manifest(&input).unwrap();
5467 let too_long = "X".repeat(256);
5469 let result =
5470 export_pmm_manifest_with_document_model_path_overrides(&parsed, &[(0u8, &too_long)]);
5471 assert!(result.is_err(), "too-long path must error");
5472 let msg = result.unwrap_err();
5473 assert!(
5474 msg.contains("NUL") || msg.contains("too long") || msg.contains("leave room"),
5475 "error message should indicate length/NUL room issue: {}",
5476 msg
5477 );
5478 }
5479
5480 #[test]
5481 fn pmm_document_model_path_override_missing_index_returns_err() {
5482 let input = pmm_with_document_summary();
5483 let parsed = parse_pmm_manifest(&input).unwrap();
5484 let result = export_pmm_manifest_with_document_model_path_overrides(
5485 &parsed,
5486 &[(99u8, "some/path.pmx")],
5487 );
5488 assert!(result.is_err(), "missing document_model_index must error");
5489 let msg = result.unwrap_err();
5490 assert!(
5491 msg.contains("exactly one") && msg.contains("99"),
5492 "error should report missing index: {}",
5493 msg
5494 );
5495 }
5496
5497 #[test]
5498 fn pmm_document_model_path_override_requires_source_bytes() {
5499 let input = pmm_with_document_summary();
5500 let mut parsed = parse_pmm_manifest(&input).unwrap();
5501 parsed.source_bytes.clear();
5502
5503 let result = export_pmm_manifest_with_document_model_path_overrides(
5504 &parsed,
5505 &[(0u8, "UserFile\\Model\\override_for_gui_smoke.pmx")],
5506 );
5507 assert!(result.is_err(), "missing source bytes must error");
5508 let msg = result.unwrap_err();
5509 assert!(
5510 msg.contains("source bytes"),
5511 "error should explain source bytes are required: {}",
5512 msg
5513 );
5514 }
5515
5516 #[test]
5517 fn pmm_scene_frame_range_patch_updates_only_expected_bytes() {
5518 let input = pmm_with_document_global_summary();
5519 let parsed = parse_pmm_manifest(&input).unwrap();
5520 let settings = parsed
5521 .document_global_summary
5522 .as_ref()
5523 .expect("fixture must have global summary")
5524 .settings
5525 .clone();
5526
5527 let patch = PmmSceneFrameRangePatch {
5528 current_frame_index: Some(99),
5529 current_frame_index_in_text_field: Some(77),
5530 begin_frame_index_enabled: Some(true),
5531 end_frame_index_enabled: Some(false),
5532 begin_frame_index: Some(10),
5533 end_frame_index: Some(240),
5534 };
5535 let patched = export_pmm_manifest_with_scene_frame_range_patch(&parsed, &patch)
5536 .expect("scene frame range patch must succeed");
5537
5538 assert_eq!(
5539 patched.len(),
5540 input.len(),
5541 "patch must preserve byte length"
5542 );
5543
5544 let reparsed = parse_pmm_manifest(&patched).unwrap();
5545 let updated = &reparsed
5546 .document_global_summary
5547 .as_ref()
5548 .expect("patched PMM must keep global summary")
5549 .settings;
5550 assert_eq!(updated.current_frame_index, 99);
5551 assert_eq!(updated.current_frame_index_in_text_field, 77);
5552 assert!(updated.begin_frame_index_enabled);
5553 assert!(!updated.end_frame_index_enabled);
5554 assert_eq!(updated.begin_frame_index, 10);
5555 assert_eq!(updated.end_frame_index, 240);
5556
5557 let mut allowed_changed = Vec::new();
5558 allowed_changed
5559 .extend(settings.current_frame_index_offset..settings.current_frame_index_offset + 4);
5560 allowed_changed.push(settings.begin_frame_index_enabled_offset);
5561 allowed_changed.push(settings.end_frame_index_enabled_offset);
5562 allowed_changed
5563 .extend(settings.begin_frame_index_offset..settings.begin_frame_index_offset + 4);
5564 allowed_changed
5565 .extend(settings.end_frame_index_offset..settings.end_frame_index_offset + 4);
5566 allowed_changed.extend(
5567 settings.current_frame_index_in_text_field_offset
5568 ..settings.current_frame_index_in_text_field_offset + 4,
5569 );
5570
5571 let changed: Vec<usize> = input
5572 .iter()
5573 .zip(patched.iter())
5574 .enumerate()
5575 .filter_map(|(index, (before, after))| (before != after).then_some(index))
5576 .collect();
5577
5578 assert!(
5579 changed.iter().all(|index| allowed_changed.contains(index)),
5580 "scene frame range patch changed bytes outside decoded frame range fields: {:?}",
5581 changed
5582 .iter()
5583 .filter(|index| !allowed_changed.contains(index))
5584 .collect::<Vec<_>>()
5585 );
5586 assert_eq!(
5587 &patched[settings.current_frame_index_offset..settings.current_frame_index_offset + 4],
5588 &99i32.to_le_bytes()
5589 );
5590 assert_eq!(patched[settings.begin_frame_index_enabled_offset], 1);
5591 assert_eq!(patched[settings.end_frame_index_enabled_offset], 0);
5592 assert_eq!(
5593 &patched[settings.begin_frame_index_offset..settings.begin_frame_index_offset + 4],
5594 &10i32.to_le_bytes()
5595 );
5596 assert_eq!(
5597 &patched[settings.end_frame_index_offset..settings.end_frame_index_offset + 4],
5598 &240i32.to_le_bytes()
5599 );
5600 assert_eq!(
5601 &patched[settings.current_frame_index_in_text_field_offset
5602 ..settings.current_frame_index_in_text_field_offset + 4],
5603 &77i32.to_le_bytes()
5604 );
5605 }
5606
5607 #[test]
5608 fn pmm_scene_frame_range_patch_noop_preserves_source_bytes() {
5609 let input = pmm_with_document_global_summary();
5610 let parsed = parse_pmm_manifest(&input).unwrap();
5611 let patched = export_pmm_manifest_with_scene_frame_range_patch(
5612 &parsed,
5613 &PmmSceneFrameRangePatch::default(),
5614 )
5615 .expect("no-op scene frame range patch must succeed");
5616
5617 assert_eq!(patched, input, "no-op patch must be byte-identical");
5618 }
5619
5620 #[test]
5621 fn pmm_scene_frame_range_patch_requires_source_bytes() {
5622 let input = pmm_with_document_global_summary();
5623 let mut parsed = parse_pmm_manifest(&input).unwrap();
5624 parsed.source_bytes.clear();
5625
5626 let result = export_pmm_manifest_with_scene_frame_range_patch(
5627 &parsed,
5628 &PmmSceneFrameRangePatch {
5629 begin_frame_index: Some(1),
5630 ..PmmSceneFrameRangePatch::default()
5631 },
5632 );
5633 assert!(result.is_err(), "missing source bytes must error");
5634 let msg = result.unwrap_err();
5635 assert!(
5636 msg.contains("source bytes"),
5637 "error should explain source bytes are required: {}",
5638 msg
5639 );
5640 }
5641
5642 #[test]
5643 fn pmm_scene_media_path_patch_updates_only_expected_bytes() {
5644 let input = pmm_with_document_global_summary();
5645 let parsed = parse_pmm_manifest(&input).unwrap();
5646 let settings = parsed
5647 .document_global_summary
5648 .as_ref()
5649 .expect("fixture must have global summary")
5650 .settings
5651 .clone();
5652
5653 let audio_path = "UserFile\\Audio\\song.wav";
5654 let background_video_path = "UserFile\\Movie\\bg.avi";
5655 let background_image_path = "UserFile\\Image\\bg.png";
5656 let patch = PmmSceneMediaPathPatch {
5657 audio_path: Some(audio_path.to_owned()),
5658 background_video_path: Some(background_video_path.to_owned()),
5659 background_image_path: Some(background_image_path.to_owned()),
5660 };
5661 let patched = export_pmm_manifest_with_scene_media_path_patch(&parsed, &patch)
5662 .expect("scene media path patch must succeed");
5663
5664 assert_eq!(
5665 patched.len(),
5666 input.len(),
5667 "patch must preserve byte length"
5668 );
5669
5670 let reparsed = parse_pmm_manifest(&patched).unwrap();
5671 let updated = &reparsed
5672 .document_global_summary
5673 .as_ref()
5674 .expect("patched PMM must keep global summary")
5675 .settings;
5676 assert_eq!(updated.audio_path, audio_path);
5677 assert_eq!(updated.background_video_path, background_video_path);
5678 assert_eq!(updated.background_image_path, background_image_path);
5679
5680 let bindings = &reparsed
5681 .project_graph
5682 .as_ref()
5683 .expect("patched PMM must keep project graph")
5684 .asset_bindings;
5685 let scene_binding_offset = |kind: &str| {
5686 bindings
5687 .iter()
5688 .find(|binding| binding.scope == "sceneSettings" && binding.asset_kind == kind)
5689 .and_then(|binding| binding.path_offset)
5690 };
5691 assert_eq!(
5692 scene_binding_offset("audio"),
5693 Some(settings.audio_path_offset)
5694 );
5695 assert_eq!(
5696 scene_binding_offset("video"),
5697 Some(settings.background_video_path_offset)
5698 );
5699 assert_eq!(
5700 scene_binding_offset("image"),
5701 Some(settings.background_image_path_offset)
5702 );
5703
5704 let allowed_ranges = [
5705 settings.audio_path_offset..settings.audio_path_offset + 256,
5706 settings.background_video_path_offset..settings.background_video_path_offset + 256,
5707 settings.background_image_path_offset..settings.background_image_path_offset + 256,
5708 ];
5709 let changed: Vec<usize> = input
5710 .iter()
5711 .zip(patched.iter())
5712 .enumerate()
5713 .filter_map(|(index, (before, after))| (before != after).then_some(index))
5714 .collect();
5715
5716 assert!(
5717 changed
5718 .iter()
5719 .all(|index| allowed_ranges.iter().any(|range| range.contains(index))),
5720 "scene media path patch changed bytes outside decoded media path fields: {:?}",
5721 changed
5722 .iter()
5723 .filter(|index| !allowed_ranges.iter().any(|range| range.contains(index)))
5724 .collect::<Vec<_>>()
5725 );
5726
5727 let assert_path_field = |offset: usize, path: &str| {
5728 let field = &patched[offset..offset + 256];
5729 assert_eq!(&field[..path.len()], path.as_bytes());
5730 assert_eq!(field[path.len()], 0);
5731 };
5732 assert_path_field(settings.audio_path_offset, audio_path);
5733 assert_path_field(settings.background_video_path_offset, background_video_path);
5734 assert_path_field(settings.background_image_path_offset, background_image_path);
5735 }
5736
5737 #[test]
5738 fn pmm_scene_media_path_patch_noop_preserves_source_bytes() {
5739 let input = pmm_with_document_global_summary();
5740 let parsed = parse_pmm_manifest(&input).unwrap();
5741 let patched = export_pmm_manifest_with_scene_media_path_patch(
5742 &parsed,
5743 &PmmSceneMediaPathPatch::default(),
5744 )
5745 .expect("no-op scene media path patch must succeed");
5746
5747 assert_eq!(patched, input, "no-op patch must be byte-identical");
5748 }
5749
5750 #[test]
5751 fn pmm_scene_media_path_patch_requires_source_bytes() {
5752 let input = pmm_with_document_global_summary();
5753 let mut parsed = parse_pmm_manifest(&input).unwrap();
5754 parsed.source_bytes.clear();
5755
5756 let result = export_pmm_manifest_with_scene_media_path_patch(
5757 &parsed,
5758 &PmmSceneMediaPathPatch {
5759 audio_path: Some("UserFile\\Audio\\song.wav".to_owned()),
5760 ..PmmSceneMediaPathPatch::default()
5761 },
5762 );
5763 assert!(result.is_err(), "missing source bytes must error");
5764 let msg = result.unwrap_err();
5765 assert!(
5766 msg.contains("source bytes"),
5767 "error should explain source bytes are required: {}",
5768 msg
5769 );
5770 }
5771
5772 #[test]
5773 fn pmm_scene_media_path_patch_too_long_returns_err() {
5774 let input = pmm_with_document_global_summary();
5775 let parsed = parse_pmm_manifest(&input).unwrap();
5776
5777 let result = export_pmm_manifest_with_scene_media_path_patch(
5778 &parsed,
5779 &PmmSceneMediaPathPatch {
5780 audio_path: Some("X".repeat(256)),
5781 ..PmmSceneMediaPathPatch::default()
5782 },
5783 );
5784 assert!(result.is_err(), "too-long media path must error");
5785 let msg = result.unwrap_err();
5786 assert!(
5787 msg.contains("NUL") || msg.contains("leave room"),
5788 "error should explain fixed string length/NUL room: {}",
5789 msg
5790 );
5791 }
5792
5793 #[test]
5794 fn pmm_export_json_top_level_does_not_include_source_bytes() {
5795 let data = pmm_with_document_global_summary();
5797 let parsed = parse_pmm_manifest(&data).unwrap();
5798 let value = serde_json::to_value(&parsed).unwrap();
5799 let keys = json_keys(&value);
5800 assert!(
5801 !keys.contains(&"sourceBytes".to_string())
5802 && !keys.contains(&"source_bytes".to_string()),
5803 "top-level JSON keys must not include source bytes field: {:?}",
5804 keys
5805 );
5806 }
5807
5808 #[test]
5809 fn exports_pmm_scene_header_timeline_fps_and_camera_fov() {
5810 let model =
5811 crate::pmx::parse_pmx_model(include_bytes!("../fixtures/pmx/ik_multi_axis_limit.pmx"))
5812 .unwrap();
5813 let motion = VmdParsedAnimation {
5814 kind: "vmd",
5815 metadata: crate::vmd::VmdParsedMetadata {
5816 format: "vmd",
5817 model_name: "ik_multi_axis_limit_fixture".to_owned(),
5818 model_name_bytes: Vec::new(),
5819 counts: crate::vmd::VmdParsedCounts {
5820 bones: 1,
5821 morphs: 0,
5822 cameras: 0,
5823 lights: 0,
5824 self_shadows: 0,
5825 properties: 0,
5826 },
5827 max_frame: 600,
5828 },
5829 bone_frames: vec![VmdParsedBoneFrame {
5830 bone_name: "link_root".to_owned(),
5831 bone_name_bytes: Vec::new(),
5832 frame: 600,
5833 translation: [0.0, 1.0, 0.0],
5834 rotation: [0.0, 0.0, 0.0, 1.0],
5835 interpolation: vec![20; 64],
5836 }],
5837 morph_frames: Vec::new(),
5838 camera_frames: Vec::new(),
5839 light_frames: Vec::new(),
5840 self_shadow_frames: Vec::new(),
5841 property_frames: Vec::new(),
5842 };
5843
5844 let report = export_pmm_scene_from_pmx_vmd(
5845 &model,
5846 &motion,
5847 "UserFile/Model/ik_multi_axis_limit.pmx",
5848 &PmmSceneExportOptions {
5849 screen_width: 800,
5850 screen_height: 600,
5851 frame_rate: 60.0,
5852 camera_fov: 42.0,
5853 },
5854 );
5855 let reparsed = parse_pmm_manifest(&report.bytes).unwrap();
5856
5857 assert_eq!(report.max_frame, 600);
5858 assert_eq!(reparsed.project_settings.screen_width, Some(800));
5859 assert_eq!(reparsed.project_settings.screen_height, Some(600));
5860 assert_eq!(reparsed.project_settings.timeline_frame_count, Some(600));
5861 assert_eq!(reparsed.project_settings.frame_rate, Some(60.0));
5862 let global = reparsed.document_global_summary.as_ref().unwrap();
5863 match global.camera.initial_keyframe.as_ref().unwrap() {
5864 PmmDocumentKeyframeSummary::Camera { fov, .. } => assert_eq!(*fov, 42),
5865 other => panic!("unexpected camera keyframe summary: {other:?}"),
5866 }
5867 }
5868
5869 fn append_pmm_model_slot(data: &mut Vec<u8>, name: &[u8], english_name: &[u8], path: &[u8]) {
5870 data.push(name.len() as u8);
5871 data.extend_from_slice(name);
5872 data.push(english_name.len() as u8);
5873 data.extend_from_slice(english_name);
5874 data.extend_from_slice(path);
5875 data.push(0);
5876 }
5877
5878 fn push_i32(data: &mut Vec<u8>, value: i32) {
5879 data.extend_from_slice(&value.to_le_bytes());
5880 }
5881
5882 fn push_f32(data: &mut Vec<u8>, value: f32) {
5883 data.extend_from_slice(&value.to_le_bytes());
5884 }
5885
5886 fn push_pmm_variable_string(data: &mut Vec<u8>, value: &[u8]) {
5887 data.push(value.len() as u8);
5888 data.extend_from_slice(value);
5889 }
5890
5891 fn push_pmm_fixed_path(data: &mut Vec<u8>, value: &[u8]) {
5892 let mut path = [0u8; 256];
5893 path[..value.len()].copy_from_slice(value);
5894 data.extend_from_slice(&path);
5895 }
5896
5897 fn push_pmm_fixed_bytes<const N: usize>(data: &mut Vec<u8>, value: &[u8]) {
5898 let mut bytes = [0u8; N];
5899 bytes[..value.len()].copy_from_slice(value);
5900 data.extend_from_slice(&bytes);
5901 }
5902
5903 fn push_document_bone_keyframe(data: &mut Vec<u8>, include_index: bool, frame: i32) {
5904 if include_index {
5905 push_i32(data, 0);
5906 }
5907 push_i32(data, frame);
5908 push_i32(data, 0);
5909 push_i32(data, 0);
5910 data.extend_from_slice(&[20u8; 16]);
5911 data.extend_from_slice(&[0.0f32.to_le_bytes(); 3].concat());
5912 push_f32(data, 0.0);
5913 push_f32(data, 0.0);
5914 push_f32(data, 0.0);
5915 push_f32(data, 1.0);
5916 data.push(0);
5917 data.push(0);
5918 }
5919
5920 fn push_document_morph_keyframe(data: &mut Vec<u8>, include_index: bool, frame: i32) {
5921 if include_index {
5922 push_i32(data, 0);
5923 }
5924 push_i32(data, frame);
5925 push_i32(data, 0);
5926 push_i32(data, 0);
5927 push_f32(data, 0.5);
5928 data.push(0);
5929 }
5930
5931 fn push_document_model_keyframe(data: &mut Vec<u8>, include_index: bool, frame: i32) {
5932 if include_index {
5933 push_i32(data, frame);
5934 }
5935 push_i32(data, frame);
5936 push_i32(data, 0);
5937 push_i32(data, 0);
5938 data.push(1);
5939 data.push(1);
5940 push_i32(data, 2);
5941 push_i32(data, 3);
5942 data.push(1);
5943 }
5944
5945 fn assert_model_keyframe_payload_layout(kf: &PmmDocumentModelKeyframeSummary) {
5946 assert_eq!(kf.visible_offset, kf.payload_offset);
5947 assert_eq!(kf.constraint_state_count, kf.constraint_states.len());
5948 assert_eq!(kf.constraint_state_count, 1);
5949 assert_eq!(kf.constraint_states_offset, kf.payload_offset + 1);
5950 assert_eq!(kf.constraint_states_byte_length, kf.constraint_state_count);
5951 assert_eq!(
5952 kf.outside_parent_index_count,
5953 kf.outside_parent_indices.len()
5954 );
5955 assert_eq!(kf.outside_parent_index_count, 1);
5956 assert_eq!(kf.outside_parent_indices_offset, kf.payload_offset + 2);
5957 assert_eq!(
5958 kf.outside_parent_indices_byte_length,
5959 kf.outside_parent_index_count * 8
5960 );
5961 assert_eq!(
5962 kf.self_shadow_enabled_offset,
5963 kf.payload_offset + kf.payload_byte_length - 1
5964 );
5965 assert_eq!(
5966 kf.payload_byte_length,
5967 1 + kf.constraint_states_byte_length + kf.outside_parent_indices_byte_length + 1
5968 );
5969 assert_eq!(kf.payload_bytes[0], kf.visible as u8);
5970 assert_eq!(kf.payload_bytes[1], kf.constraint_states[0] as u8);
5971 assert_eq!(
5972 i32::from_le_bytes(kf.payload_bytes[2..6].try_into().unwrap()),
5973 kf.outside_parent_indices[0].parent_model_index
5974 );
5975 assert_eq!(
5976 i32::from_le_bytes(kf.payload_bytes[6..10].try_into().unwrap()),
5977 kf.outside_parent_indices[0].parent_model_bone_index
5978 );
5979 assert_eq!(
5980 kf.payload_bytes[kf.payload_bytes.len() - 1],
5981 kf.self_shadow_enabled as u8
5982 );
5983 }
5984
5985 fn push_document_base_keyframe(data: &mut Vec<u8>, include_index: bool, frame: i32) {
5986 if include_index {
5987 push_i32(data, frame);
5988 }
5989 push_i32(data, frame);
5990 push_i32(data, 0);
5991 push_i32(data, 0);
5992 }
5993
5994 fn push_document_camera_keyframe(data: &mut Vec<u8>, include_index: bool, frame: i32) {
5995 push_document_base_keyframe(data, include_index, frame);
5996 push_f32(data, 45.0);
5997 data.extend_from_slice(&[0u8; 12]);
5998 data.extend_from_slice(&[0u8; 12]);
5999 push_i32(data, -1);
6000 push_i32(data, -1);
6001 data.extend_from_slice(&[20u8; 24]);
6002 data.push(0);
6003 push_i32(data, 30);
6004 data.push(0);
6005 }
6006
6007 fn push_document_light_keyframe(data: &mut Vec<u8>, include_index: bool, frame: i32) {
6008 push_document_base_keyframe(data, include_index, frame);
6009 for value in [0.6f32, 0.6, 0.6, -0.5, -1.0, 0.5] {
6010 push_f32(data, value);
6011 }
6012 data.push(0);
6013 }
6014
6015 fn push_document_gravity_keyframe(data: &mut Vec<u8>, include_index: bool, frame: i32) {
6016 push_document_base_keyframe(data, include_index, frame);
6017 data.push(0);
6018 push_i32(data, 10);
6019 push_f32(data, 9.8);
6020 for value in [0.0f32, -1.0, 0.0] {
6021 push_f32(data, value);
6022 }
6023 data.push(0);
6024 }
6025
6026 fn push_document_self_shadow_keyframe(data: &mut Vec<u8>, include_index: bool, frame: i32) {
6027 push_document_base_keyframe(data, include_index, frame);
6028 data.push(1);
6029 push_f32(data, 8875.0);
6030 data.push(0);
6031 }
6032
6033 fn push_document_accessory_keyframe(data: &mut Vec<u8>, include_index: bool, frame: i32) {
6034 push_document_base_keyframe(data, include_index, frame);
6035 data.push(1);
6036 push_i32(data, -1);
6037 push_i32(data, -1);
6038 for value in [1.0f32, 2.0, 3.0, 0.1, 0.2, 0.3] {
6039 push_f32(data, value);
6040 }
6041 push_f32(data, 10.0);
6042 data.push(1);
6043 data.push(0);
6044 }
6045
6046 fn push_empty_pmm_path(data: &mut Vec<u8>) {
6047 data.extend_from_slice(&[0u8; 256]);
6048 }
6049
6050 fn pmm_with_document_summary() -> Vec<u8> {
6051 let mut data = b"Polygon Movie maker 0002".to_vec();
6052 data.resize(30, 0);
6053 push_i32(&mut data, 640);
6054 push_i32(&mut data, 360);
6055 push_i32(&mut data, 120);
6056 push_f32(&mut data, 30.0);
6057 data.extend_from_slice(&[0, 0, 0, 0, 0, 0, 0]);
6058 data.push(0);
6059 data.push(1);
6060 data.push(0);
6061 push_pmm_variable_string(&mut data, b"Miku");
6062 push_pmm_variable_string(&mut data, b"MikuEn");
6063 push_pmm_fixed_path(&mut data, b"UserFile\\Model\\miku.pmx");
6064 data.push(0);
6065 push_i32(&mut data, 1);
6066 push_pmm_variable_string(&mut data, b"center");
6067 push_i32(&mut data, 1);
6068 push_pmm_variable_string(&mut data, b"smile");
6069 push_i32(&mut data, 1);
6070 push_i32(&mut data, 7);
6071 push_i32(&mut data, 1);
6072 push_i32(&mut data, 8);
6073 data.push(0);
6074 data.push(1);
6075 push_i32(&mut data, 0);
6076 for _ in 0..4 {
6077 push_i32(&mut data, -1);
6078 }
6079 data.push(0);
6080 push_i32(&mut data, 0);
6081 push_i32(&mut data, 30);
6082 push_document_bone_keyframe(&mut data, false, 0);
6083 push_i32(&mut data, 1);
6084 push_document_bone_keyframe(&mut data, true, 30);
6085 push_document_morph_keyframe(&mut data, false, 0);
6086 push_i32(&mut data, 1);
6087 push_document_morph_keyframe(&mut data, true, 15);
6088 push_document_model_keyframe(&mut data, false, 0);
6089 push_i32(&mut data, 1);
6090 push_document_model_keyframe(&mut data, true, 30);
6091 push_f32(&mut data, 1.0);
6093 push_f32(&mut data, 2.0);
6094 push_f32(&mut data, 3.0);
6095 push_f32(&mut data, 0.0);
6096 push_f32(&mut data, 0.0);
6097 push_f32(&mut data, 0.0);
6098 push_f32(&mut data, 1.0);
6099 data.push(1); data.push(0); data.push(1); push_f32(&mut data, 0.75);
6104 data.push(1);
6106 push_i32(&mut data, 0);
6108 push_i32(&mut data, 5);
6109 push_i32(&mut data, 7);
6110 push_i32(&mut data, 1);
6111 data.push(1);
6112 push_f32(&mut data, 1.0);
6113 data.push(0);
6114 data.push(0);
6115 data.extend_from_slice(b"UserFile\\Model\\miku.pmx\0");
6116 data
6117 }
6118
6119 fn pmm_with_document_global_summary() -> Vec<u8> {
6120 let mut data = pmm_with_document_summary();
6121 let trailing_reference = b"UserFile\\Model\\miku.pmx\0";
6122 data.truncate(data.len() - trailing_reference.len());
6123
6124 push_document_camera_keyframe(&mut data, false, 0);
6125 push_i32(&mut data, 1);
6126 push_document_camera_keyframe(&mut data, true, 42);
6127 data.extend_from_slice(&[0u8; 12 * 3]);
6128 data.push(0);
6129
6130 push_document_light_keyframe(&mut data, false, 0);
6131 push_i32(&mut data, 1);
6132 push_document_light_keyframe(&mut data, true, 43);
6133 for value in [0.6f32, 0.6, 0.6, -0.5, -1.0, 0.5] {
6134 push_f32(&mut data, value);
6135 }
6136
6137 data.push(0);
6138 push_i32(&mut data, 0);
6139 data.push(1);
6140 data.push(0);
6141 push_pmm_fixed_bytes::<100>(&mut data, b"Stage");
6142 push_pmm_fixed_path(&mut data, b"UserFile\\Accessory\\stage.x");
6143 data.push(0);
6144 push_document_accessory_keyframe(&mut data, false, 0);
6145 push_i32(&mut data, 1);
6146 push_document_accessory_keyframe(&mut data, true, 46);
6147 data.push(1);
6148 push_i32(&mut data, -1);
6149 push_i32(&mut data, -1);
6150 for value in [1.0f32, 2.0, 3.0] {
6151 push_f32(&mut data, value);
6152 }
6153 push_f32(&mut data, 10.0);
6154 for value in [0.1f32, 0.2, 0.3] {
6155 push_f32(&mut data, value);
6156 }
6157 data.push(1);
6158 data.push(0);
6159
6160 push_i32(&mut data, 12);
6161 push_i32(&mut data, 34);
6162 push_i32(&mut data, 56);
6163 push_i32(&mut data, 2);
6164 data.push(0);
6165 data.push(1);
6166 data.push(0);
6167 data.push(1);
6168 push_i32(&mut data, 3);
6169 push_i32(&mut data, 120);
6170 data.push(1);
6171 push_empty_pmm_path(&mut data);
6172 push_i32(&mut data, 0);
6173 push_i32(&mut data, 0);
6174 push_f32(&mut data, 1.0);
6175 push_empty_pmm_path(&mut data);
6176 push_i32(&mut data, 0);
6177 push_i32(&mut data, 0);
6178 push_i32(&mut data, 0);
6179 push_f32(&mut data, 1.0);
6180 push_empty_pmm_path(&mut data);
6181 data.push(0);
6182 data.push(1);
6183 data.push(1);
6184 data.push(1);
6185 push_f32(&mut data, 60.0);
6186 push_i32(&mut data, 2);
6187 push_i32(&mut data, -1);
6188 push_f32(&mut data, 1.0);
6189 data.push(1);
6190 data.push(2);
6191
6192 push_f32(&mut data, 9.8);
6193 push_i32(&mut data, 10);
6194 for value in [0.0f32, -1.0, 0.0] {
6195 push_f32(&mut data, value);
6196 }
6197 data.push(0);
6198 push_document_gravity_keyframe(&mut data, false, 0);
6199 push_i32(&mut data, 1);
6200 push_document_gravity_keyframe(&mut data, true, 44);
6201
6202 data.push(1);
6203 push_f32(&mut data, 8875.0);
6204 push_document_self_shadow_keyframe(&mut data, false, 0);
6205 push_i32(&mut data, 1);
6206 push_document_self_shadow_keyframe(&mut data, true, 45);
6207
6208 push_i32(&mut data, 0);
6209 push_i32(&mut data, 0);
6210 push_i32(&mut data, 0);
6211 data.push(0);
6212 push_i32(&mut data, -1);
6213 push_i32(&mut data, -1);
6214 data.extend_from_slice(&[0u8; 16 * 4]);
6215 data.push(0);
6216 data.push(0);
6217 data.push(1);
6218 push_i32(&mut data, 12);
6219 data.push(1);
6220 data.push(0);
6221 push_i32(&mut data, 0);
6222
6223 data
6224 }
6225
6226 fn pmm_with_two_initial_model_slots() -> Vec<u8> {
6227 let mut data = b"Polygon Movie maker 0002".to_vec();
6228 data.resize(30, 0);
6229 data.extend_from_slice(&640u32.to_le_bytes());
6230 data.extend_from_slice(&360u32.to_le_bytes());
6231 data.extend_from_slice(&250u32.to_le_bytes());
6232 data.extend_from_slice(&30.0f32.to_le_bytes());
6233 data.extend_from_slice(&[1, 1, 0, 0, 0, 0, 0, 0]);
6234 data.push(1);
6235 data.push(0);
6236 append_pmm_model_slot(
6237 &mut data,
6238 b"TestModel",
6239 b"Base",
6240 b"C:\\MMDDev\\data\\unittest\\test_1bone_cube.pmx",
6241 );
6242 append_pmm_model_slot(
6243 &mut data,
6244 b"Sour",
6245 b"Sour",
6246 b"G:\\MikuMikuDance\\MMD Models\\Sour\\sour.pmx",
6247 );
6248 data.extend_from_slice(b"UserFile\\Motion\\walk.vmd\0");
6249 data
6250 }
6251
6252 fn pmm_v1_with_display_counts() -> Vec<u8> {
6253 let mut data = b"Polygon Movie maker 0001".to_vec();
6254 data.resize(30, 0);
6255 data.extend_from_slice(&512u32.to_le_bytes());
6256 data.extend_from_slice(&288u32.to_le_bytes());
6257 data.extend_from_slice(&435u32.to_le_bytes());
6258 data.extend_from_slice(&35.0f32.to_le_bytes());
6259 data.extend_from_slice(&[1, 1, 1, 1, 1, 1]);
6260 data.push(8);
6261 data.push(9);
6262 data.extend_from_slice(b"Dummy\0 maker Dummy\0");
6263 data.extend_from_slice(b"UserFile\\Model\\miku.pmd\0UserFile\\Accessory\\stage01.x\0");
6264 data
6265 }
6266
6267 fn json_keys(value: &serde_json::Value) -> Vec<String> {
6268 let mut keys = value
6269 .as_object()
6270 .unwrap()
6271 .keys()
6272 .map(ToOwned::to_owned)
6273 .collect::<Vec<_>>();
6274 keys.sort();
6275 keys
6276 }
6277
6278 #[test]
6279 fn pmm_manifest_json_top_level_schema_is_stable() {
6280 let data =
6281 b"Polygon Movie maker 0002\0UserFile\\Model\\miku.pmx\0UserFile\\Motion\\walk.vmd\0";
6282 let parsed = parse_pmm_manifest(data).unwrap();
6283 let keys = json_keys(&serde_json::to_value(&parsed).unwrap());
6284
6285 assert_eq!(
6286 keys,
6287 vec![
6288 "accessoryAssets",
6289 "accessoryPaths",
6290 "assetReferences",
6291 "assetSummary",
6292 "audioAssets",
6293 "audioPaths",
6294 "byteLength",
6295 "diagnostics",
6296 "displayState",
6297 "documentGlobalSummary",
6298 "documentSummary",
6299 "headerTextEntries",
6300 "imageAssets",
6301 "imagePaths",
6302 "modelAssets",
6303 "modelPaths",
6304 "modelSlots",
6305 "motionAssets",
6306 "motionPaths",
6307 "parsedVersion",
6308 "projectGraph",
6309 "projectSettings",
6310 "signature",
6311 "timeline",
6312 "version",
6313 "videoAssets",
6314 "videoPaths",
6315 ]
6316 );
6317 }
6318
6319 #[test]
6320 fn pmm_project_graph_json_schema_is_stable() {
6321 let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
6322 let graph = parsed.project_graph.unwrap();
6323 let keys = json_keys(&serde_json::to_value(&graph).unwrap());
6324
6325 assert_eq!(
6326 keys,
6327 vec![
6328 "accessoryCount",
6329 "accessoryKeyframes",
6330 "assetBindings",
6331 "assetReferences",
6332 "assetSummary",
6333 "byteCoverage",
6334 "displayState",
6335 "exportReadiness",
6336 "global",
6337 "keyframeReferences",
6338 "modelCounts",
6339 "models",
6340 "parsedVersion",
6341 "sceneSettings",
6342 "source",
6343 "timeline",
6344 "trackReferences",
6345 "version",
6346 ]
6347 );
6348 }
6349
6350 #[test]
6351 fn pmm_project_export_readiness_json_schema_is_stable() {
6352 let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
6353 let graph = parsed.project_graph.unwrap();
6354 let readiness = &graph.export_readiness;
6355 let keys = json_keys(&serde_json::to_value(readiness).unwrap());
6356
6357 assert_eq!(
6358 keys,
6359 vec![
6360 "blockerCount",
6361 "blockers",
6362 "losslessParsedByteExportSupported",
6363 "semanticGraphExportSupported",
6364 "sourceBytePreservationRequired",
6365 ]
6366 );
6367 }
6368
6369 #[test]
6370 fn pmm_project_export_blocker_json_schema_is_stable() {
6371 let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
6372 let graph = parsed.project_graph.unwrap();
6373 let blocker = graph
6375 .export_readiness
6376 .blockers
6377 .iter()
6378 .find(|b| b.code == "PMM_SEMANTIC_EXPORTER_UNFINISHED")
6379 .expect("PMM_SEMANTIC_EXPORTER_UNFINISHED blocker must be present for schema test");
6380 let keys = json_keys(&serde_json::to_value(blocker).unwrap());
6381
6382 assert_eq!(
6383 keys,
6384 vec![
6385 "code",
6386 "count",
6387 "coverageRatio",
6388 "kind",
6389 "message",
6390 "scope",
6391 "severity",
6392 ]
6393 );
6394 }
6395
6396 #[test]
6397 fn pmm_project_asset_binding_json_schema_is_stable() {
6398 let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
6399 let graph = parsed.project_graph.unwrap();
6400 let binding = &graph.asset_bindings[0];
6402 let keys = json_keys(&serde_json::to_value(binding).unwrap());
6403
6404 assert_eq!(
6405 keys,
6406 vec![
6407 "assetKind",
6408 "assetReferenceConfidence",
6409 "assetReferenceEndOffset",
6410 "assetReferenceIndex",
6411 "assetReferenceOffset",
6412 "documentIndex",
6413 "ownerIndex",
6414 "ownerName",
6415 "path",
6416 "pathOffset",
6417 "scope",
6418 ]
6419 );
6420 }
6421
6422 #[test]
6423 fn pmm_project_scene_settings_json_schema_is_stable() {
6424 let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
6425 let graph = parsed.project_graph.unwrap();
6426 let keys = json_keys(&serde_json::to_value(&graph.scene_settings).unwrap());
6427
6428 assert_eq!(
6429 keys,
6430 vec![
6431 "audioAssetReferenceIndex",
6432 "audioEnabled",
6433 "audioPath",
6434 "backgroundImageAssetReferenceIndex",
6435 "backgroundImageEnabled",
6436 "backgroundImageOffset",
6437 "backgroundImagePath",
6438 "backgroundImageScaleFactor",
6439 "backgroundVideoAssetReferenceIndex",
6440 "backgroundVideoEnabled",
6441 "backgroundVideoOffset",
6442 "backgroundVideoPath",
6443 "backgroundVideoScaleFactor",
6444 "beginFrameIndex",
6445 "beginFrameIndexEnabled",
6446 "currentFrameIndex",
6447 "currentFrameIndexInTextField",
6448 "endFrameIndex",
6449 "endFrameIndexEnabled",
6450 "loopEnabled",
6451 "offset",
6452 "offsetEnd",
6453 "preferredFps",
6454 ]
6455 );
6456 }
6457
6458 #[test]
6459 fn pmm_project_track_reference_json_schema_is_stable() {
6460 let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
6461 let graph = parsed.project_graph.unwrap();
6462 let reference = &graph.track_references[0];
6463 let keys = json_keys(&serde_json::to_value(reference).unwrap());
6464
6465 assert_eq!(
6466 keys,
6467 vec![
6468 "documentIndex",
6469 "initialKeyframes",
6470 "initialKeyframesOffset",
6471 "keyframeCountOffset",
6472 "keyframes",
6473 "keyframesEndOffset",
6474 "keyframesOffset",
6475 "ownerIndex",
6476 "ownerName",
6477 "scope",
6478 "stateEndOffset",
6479 "stateOffset",
6480 "trackKind",
6481 ]
6482 );
6483 }
6484
6485 #[test]
6486 fn pmm_project_keyframe_reference_json_schema_is_stable() {
6487 let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
6488 let graph = parsed.project_graph.unwrap();
6489 let reference = &graph.keyframe_references[0];
6490 let keys = json_keys(&serde_json::to_value(reference).unwrap());
6491
6492 assert_eq!(
6493 keys,
6494 vec![
6495 "byteLength",
6496 "documentIndex",
6497 "frameIndex",
6498 "initial",
6499 "keyframeIndex",
6500 "nextKeyframeIndex",
6501 "offset",
6502 "ownerIndex",
6503 "ownerName",
6504 "payloadByteLength",
6505 "payloadOffset",
6506 "previousKeyframeIndex",
6507 "scope",
6508 "trackKind",
6509 ]
6510 );
6511 }
6512
6513 #[test]
6514 fn pmm_project_byte_coverage_json_schema_is_stable() {
6515 let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
6516 let graph = parsed.project_graph.unwrap();
6517 let coverage = &graph.byte_coverage;
6518 let keys = json_keys(&serde_json::to_value(coverage).unwrap());
6519
6520 assert_eq!(
6521 keys,
6522 vec![
6523 "byteLength",
6524 "coverageRatio",
6525 "coveredByteLength",
6526 "gapCount",
6527 "gaps",
6528 "offset",
6529 "offsetEnd",
6530 "rangeCount",
6531 "ranges",
6532 ]
6533 );
6534 }
6535
6536 #[test]
6537 fn pmm_project_byte_range_json_schema_is_stable() {
6538 let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
6539 let graph = parsed.project_graph.unwrap();
6540 let range0 = &graph.byte_coverage.ranges[0];
6541 let keys = json_keys(&serde_json::to_value(range0).unwrap());
6542
6543 assert_eq!(
6544 keys,
6545 vec![
6546 "byteLength",
6547 "documentIndex",
6548 "kind",
6549 "name",
6550 "offset",
6551 "offsetEnd",
6552 "ownerIndex",
6553 "scope",
6554 ]
6555 );
6556 }
6557
6558 #[test]
6559 fn pmm_header_text_entry_json_schema_is_stable() {
6560 let parsed = parse_pmm_manifest(&pmm_v1_with_display_counts()).unwrap();
6561 let keys = json_keys(&serde_json::to_value(&parsed.header_text_entries[0]).unwrap());
6562
6563 assert_eq!(
6564 keys,
6565 vec!["index", "offset", "offsetEnd", "text", "textBytes"]
6566 );
6567 }
6568
6569 #[test]
6570 fn pmm_model_slot_json_schema_is_stable() {
6571 let parsed = parse_pmm_manifest(&pmm_with_initial_model_slot()).unwrap();
6572 let keys = json_keys(&serde_json::to_value(&parsed.model_slots[0]).unwrap());
6573
6574 assert_eq!(
6575 keys,
6576 vec![
6577 "assetReferenceIndex",
6578 "confidence",
6579 "displaySlotIndex",
6580 "englishName",
6581 "englishNameBytes",
6582 "modelPath",
6583 "modelPathOffset",
6584 "name",
6585 "nameBytes",
6586 "nextNonZeroOffset",
6587 "normalizedPath",
6588 "offset",
6589 "offsetEnd",
6590 "slotIndex",
6591 "trailingZeroPaddingBytes"
6592 ]
6593 );
6594 }
6595
6596 #[test]
6597 fn pmm_project_settings_json_schema_is_stable() {
6598 let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
6599 let keys = json_keys(&serde_json::to_value(&parsed.project_settings).unwrap());
6600
6601 assert_eq!(
6602 keys,
6603 vec![
6604 "frameRate",
6605 "screenHeight",
6606 "screenWidth",
6607 "timelineFrameCount",
6608 ]
6609 );
6610 }
6611
6612 #[test]
6613 fn pmm_display_state_json_schema_is_stable() {
6614 let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
6615 let keys = json_keys(&serde_json::to_value(&parsed.display_state).unwrap());
6616
6617 assert_eq!(
6618 keys,
6619 vec![
6620 "accessorySlotCount",
6621 "activeModelSlotIndices",
6622 "declaredModelSlotCount",
6623 "documentExpandFlags",
6624 "documentModelCount",
6625 "emptyModelSlotIndices",
6626 "layout",
6627 "modelSlotCount",
6628 "modelSlotFlagCounts",
6629 "modelSlotFlagEntries",
6630 "modelSlotFlags",
6631 "nonZeroModelSlotCount",
6632 "selectedModelIndex"
6633 ]
6634 );
6635 }
6636
6637 #[test]
6638 fn pmm_model_slot_flag_entry_json_schema_is_stable() {
6639 let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
6640 let keys = json_keys(
6641 &serde_json::to_value(&parsed.display_state.model_slot_flag_entries[0]).unwrap(),
6642 );
6643
6644 assert_eq!(keys, vec!["active", "flag", "slotIndex"]);
6645 }
6646
6647 #[test]
6648 fn pmm_document_expand_flags_json_schema_is_stable() {
6649 let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
6650 let keys = json_keys(
6651 &serde_json::to_value(parsed.display_state.document_expand_flags.unwrap()).unwrap(),
6652 );
6653
6654 assert_eq!(
6655 keys,
6656 vec![
6657 "accessoryPanel",
6658 "bonePanel",
6659 "cameraPanel",
6660 "editingCla",
6661 "lightPanel",
6662 "morphPanel",
6663 "selfShadowPanel"
6664 ]
6665 );
6666 }
6667
6668 #[test]
6669 fn pmm_timeline_json_schema_is_stable() {
6670 let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
6671 let keys = json_keys(&serde_json::to_value(&parsed.timeline).unwrap());
6672
6673 assert_eq!(
6674 keys,
6675 vec![
6676 "durationSeconds",
6677 "endFrameExclusive",
6678 "frameCount",
6679 "frameRate",
6680 "startFrame"
6681 ]
6682 );
6683 }
6684
6685 #[test]
6686 fn pmm_scene_asset_json_schema_is_stable() {
6687 let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
6688 let keys = json_keys(&serde_json::to_value(&parsed.model_assets[0]).unwrap());
6689
6690 assert_eq!(
6691 keys,
6692 vec![
6693 "confidence",
6694 "extension",
6695 "fileName",
6696 "kindIndex",
6697 "normalizedPath",
6698 "offset",
6699 "offsetEnd",
6700 "path",
6701 "referenceIndex"
6702 ]
6703 );
6704 }
6705
6706 #[test]
6707 fn pmm_asset_reference_json_schema_is_stable() {
6708 let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
6709 let keys = json_keys(&serde_json::to_value(&parsed.asset_references[0]).unwrap());
6710
6711 assert_eq!(
6712 keys,
6713 vec![
6714 "confidence",
6715 "extension",
6716 "fileName",
6717 "kind",
6718 "normalizedPath",
6719 "offset",
6720 "offsetEnd",
6721 "path"
6722 ]
6723 );
6724 }
6725
6726 #[test]
6727 fn pmm_asset_summary_json_schema_is_stable() {
6728 let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
6729 let keys = json_keys(&serde_json::to_value(&parsed.asset_summary).unwrap());
6730
6731 assert_eq!(
6732 keys,
6733 vec![
6734 "confidenceCounts",
6735 "extensionCounts",
6736 "highConfidenceCount",
6737 "kindCounts",
6738 "lowConfidenceCount",
6739 "mediumConfidenceCount",
6740 "referenceCount"
6741 ]
6742 );
6743 }
6744
6745 #[test]
6746 fn pmm_document_summary_json_schema_is_stable() {
6747 let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
6748 let keys = json_keys(&serde_json::to_value(parsed.document_summary.unwrap()).unwrap());
6749
6750 assert_eq!(
6751 keys,
6752 vec![
6753 "counts",
6754 "modelCount",
6755 "models",
6756 "selectedModelIndex",
6757 "source"
6758 ]
6759 );
6760 }
6761
6762 #[test]
6763 fn pmm_document_counts_json_schema_is_stable() {
6764 let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
6765 let document = parsed.document_summary.unwrap();
6766 let keys = json_keys(&serde_json::to_value(document.counts).unwrap());
6767
6768 assert_eq!(
6769 keys,
6770 vec![
6771 "boneKeyframes",
6772 "bones",
6773 "initialBoneKeyframes",
6774 "initialModelKeyframes",
6775 "initialMorphKeyframes",
6776 "modelKeyframes",
6777 "models",
6778 "morphKeyframes",
6779 "morphs"
6780 ]
6781 );
6782 }
6783
6784 #[test]
6785 fn pmm_document_global_summary_json_schema_is_stable() {
6786 let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
6787 let keys =
6788 json_keys(&serde_json::to_value(parsed.document_global_summary.unwrap()).unwrap());
6789
6790 assert_eq!(
6791 keys,
6792 vec![
6793 "accessories",
6794 "camera",
6795 "gravity",
6796 "light",
6797 "offset",
6798 "offsetEnd",
6799 "selfShadow",
6800 "settings",
6801 "source"
6802 ]
6803 );
6804 }
6805
6806 #[test]
6807 fn pmm_initial_camera_keyframe_json_schema_is_stable() {
6808 let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
6809 let global = parsed.document_global_summary.unwrap();
6810 let keys =
6811 json_keys(&serde_json::to_value(global.camera.initial_keyframe.unwrap()).unwrap());
6812
6813 assert_eq!(
6814 keys,
6815 vec![
6816 "angle",
6817 "angleByteLength",
6818 "angleOffset",
6819 "byteLength",
6820 "distance",
6821 "distanceOffset",
6822 "fov",
6823 "fovOffset",
6824 "frameIndex",
6825 "index",
6826 "interpolation",
6827 "interpolationByteLength",
6828 "interpolationOffset",
6829 "kind",
6830 "lookAt",
6831 "lookAtByteLength",
6832 "lookAtOffset",
6833 "nextKeyframeIndex",
6834 "offset",
6835 "parentModelBoneIndex",
6836 "parentModelBoneIndexOffset",
6837 "parentModelIndex",
6838 "parentModelIndexOffset",
6839 "payloadByteLength",
6840 "payloadBytes",
6841 "payloadOffset",
6842 "perspectiveView",
6843 "perspectiveViewOffset",
6844 "previousKeyframeIndex",
6845 "selected",
6846 "selectedOffset"
6847 ]
6848 );
6849 }
6850
6851 #[test]
6852 fn pmm_initial_light_keyframe_json_schema_is_stable() {
6853 let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
6854 let global = parsed.document_global_summary.unwrap();
6855 let keys =
6856 json_keys(&serde_json::to_value(global.light.initial_keyframe.unwrap()).unwrap());
6857
6858 assert_eq!(
6859 keys,
6860 vec![
6861 "byteLength",
6862 "color",
6863 "colorByteLength",
6864 "colorOffset",
6865 "direction",
6866 "directionByteLength",
6867 "directionOffset",
6868 "frameIndex",
6869 "index",
6870 "kind",
6871 "nextKeyframeIndex",
6872 "offset",
6873 "payloadByteLength",
6874 "payloadBytes",
6875 "payloadOffset",
6876 "previousKeyframeIndex",
6877 "selected",
6878 "selectedOffset"
6879 ]
6880 );
6881 }
6882
6883 #[test]
6884 fn pmm_initial_gravity_keyframe_json_schema_is_stable() {
6885 let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
6886 let global = parsed.document_global_summary.unwrap();
6887 let keys =
6888 json_keys(&serde_json::to_value(global.gravity.initial_keyframe.unwrap()).unwrap());
6889
6890 assert_eq!(
6891 keys,
6892 vec![
6893 "acceleration",
6894 "accelerationOffset",
6895 "byteLength",
6896 "direction",
6897 "directionByteLength",
6898 "directionOffset",
6899 "frameIndex",
6900 "index",
6901 "kind",
6902 "nextKeyframeIndex",
6903 "noise",
6904 "noiseEnabled",
6905 "noiseEnabledOffset",
6906 "noiseOffset",
6907 "offset",
6908 "payloadByteLength",
6909 "payloadBytes",
6910 "payloadOffset",
6911 "previousKeyframeIndex",
6912 "selected",
6913 "selectedOffset"
6914 ]
6915 );
6916 }
6917
6918 #[test]
6919 fn pmm_initial_self_shadow_keyframe_json_schema_is_stable() {
6920 let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
6921 let global = parsed.document_global_summary.unwrap();
6922 let keys =
6923 json_keys(&serde_json::to_value(global.self_shadow.initial_keyframe.unwrap()).unwrap());
6924
6925 assert_eq!(
6926 keys,
6927 vec![
6928 "byteLength",
6929 "distance",
6930 "distanceOffset",
6931 "frameIndex",
6932 "index",
6933 "kind",
6934 "mode",
6935 "modeOffset",
6936 "nextKeyframeIndex",
6937 "offset",
6938 "payloadByteLength",
6939 "payloadBytes",
6940 "payloadOffset",
6941 "previousKeyframeIndex",
6942 "selected",
6943 "selectedOffset"
6944 ]
6945 );
6946 }
6947
6948 #[test]
6949 fn pmm_document_track_summary_json_schema_is_stable() {
6950 let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
6951 let global = parsed.document_global_summary.unwrap();
6952 let keys = json_keys(&serde_json::to_value(global.camera).unwrap());
6953
6954 assert_eq!(
6955 keys,
6956 vec![
6957 "initialKeyframe",
6958 "initialKeyframes",
6959 "keyframeCountOffset",
6960 "keyframeSummaries",
6961 "keyframes",
6962 "keyframesEndOffset",
6963 "keyframesOffset",
6964 "offset",
6965 "offsetEnd",
6966 "stateEndOffset",
6967 "stateOffset"
6968 ]
6969 );
6970 }
6971
6972 #[test]
6973 fn pmm_document_settings_summary_json_schema_is_stable() {
6974 let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
6975 let global = parsed.document_global_summary.unwrap();
6976 let keys = json_keys(&serde_json::to_value(global.settings).unwrap());
6977
6978 assert_eq!(
6979 keys,
6980 vec![
6981 "accessoryIndexAfterModels",
6982 "audioEnabled",
6983 "audioPath",
6984 "audioPathOffset",
6985 "backgroundImageEnabled",
6986 "backgroundImageOffset",
6987 "backgroundImagePath",
6988 "backgroundImagePathOffset",
6989 "backgroundImageScaleFactor",
6990 "backgroundVideoEnabled",
6991 "backgroundVideoOffset",
6992 "backgroundVideoPath",
6993 "backgroundVideoPathOffset",
6994 "backgroundVideoScaleFactor",
6995 "beginFrameIndex",
6996 "beginFrameIndexEnabled",
6997 "beginFrameIndexEnabledOffset",
6998 "beginFrameIndexOffset",
6999 "blackBackgroundEnabled",
7000 "cameraLookAtModelBoneIndex",
7001 "cameraLookAtModelIndex",
7002 "cameraLookMode",
7003 "currentFrameIndex",
7004 "currentFrameIndexInTextField",
7005 "currentFrameIndexInTextFieldOffset",
7006 "currentFrameIndexOffset",
7007 "edgeColor",
7008 "editingMode",
7009 "endFrameIndex",
7010 "endFrameIndexEnabled",
7011 "endFrameIndexEnabledOffset",
7012 "endFrameIndexOffset",
7013 "followingLookAtEnabled",
7014 "gridAndAxisShown",
7015 "groundShadowBrightness",
7016 "groundShadowShown",
7017 "horizontalScroll",
7018 "horizontalScrollThumb",
7019 "informationShown",
7020 "loopEnabled",
7021 "modelSelectionFooterEndOffset",
7022 "modelSelectionFooterOffset",
7023 "modelSelectionFooterPresent",
7024 "offset",
7025 "offsetEnd",
7026 "physicsGroundEnabled",
7027 "physicsSimulationMode",
7028 "preferredFps",
7029 "screenCaptureMode",
7030 "translucentGroundShadowEnabled",
7031 "unknownMatrixEndOffset",
7032 "unknownMatrixOffset"
7033 ]
7034 );
7035 }
7036
7037 #[test]
7038 fn pmm_document_accessory_summary_json_schema_is_stable() {
7039 let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
7040 let global = parsed.document_global_summary.unwrap();
7041 let keys = json_keys(&serde_json::to_value(&global.accessories.accessories[0]).unwrap());
7042
7043 assert_eq!(
7044 keys,
7045 vec![
7046 "addBlendEnabled",
7047 "assetReferenceIndex",
7048 "documentAccessoryIndex",
7049 "drawOrderIndex",
7050 "initialKeyframe",
7051 "keyframeCountOffset",
7052 "keyframeSummaries",
7053 "keyframes",
7054 "keyframesEndOffset",
7055 "keyframesOffset",
7056 "name",
7057 "offset",
7058 "offsetEnd",
7059 "opacity",
7060 "parentModelBoneIndex",
7061 "parentModelIndex",
7062 "path",
7063 "pathOffset",
7064 "scaleFactor",
7065 "shadowEnabled",
7066 "slotIndex",
7067 "stateEndOffset",
7068 "stateOffset",
7069 "visible"
7070 ]
7071 );
7072 }
7073
7074 #[test]
7075 fn pmm_document_accessory_keyframe_summary_json_schema_is_stable() {
7076 let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
7077 let global = parsed.document_global_summary.unwrap();
7078 let keys = json_keys(
7079 &serde_json::to_value(&global.accessories.accessories[0].initial_keyframe).unwrap(),
7080 );
7081
7082 assert_eq!(
7083 keys,
7084 vec![
7085 "byteLength",
7086 "frameIndex",
7087 "index",
7088 "nextKeyframeIndex",
7089 "offset",
7090 "opacity",
7091 "orientation",
7092 "orientationByteLength",
7093 "orientationOffset",
7094 "packedOpacityVisibleOffset",
7095 "parentModelBoneIndex",
7096 "parentModelBoneIndexOffset",
7097 "parentModelIndex",
7098 "parentModelIndexOffset",
7099 "payloadByteLength",
7100 "payloadBytes",
7101 "payloadOffset",
7102 "previousKeyframeIndex",
7103 "scaleFactor",
7104 "scaleFactorOffset",
7105 "selected",
7106 "selectedOffset",
7107 "shadowEnabled",
7108 "shadowEnabledOffset",
7109 "translation",
7110 "translationByteLength",
7111 "translationOffset",
7112 "visible"
7113 ]
7114 );
7115 }
7116
7117 #[test]
7118 fn parses_pmm_v2_document_global_summary() {
7119 let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
7120 let global = parsed.document_global_summary.as_ref().unwrap();
7121
7122 assert_eq!(global.source, "nanoem/ext/document.c PMMv2 global layout");
7123 assert_eq!(global.camera.initial_keyframes, 1);
7124 assert_eq!(global.camera.keyframes, 1);
7125 match global.camera.initial_keyframe.as_ref().unwrap() {
7126 PmmDocumentKeyframeSummary::Camera {
7127 index,
7128 frame_index,
7129 distance,
7130 look_at,
7131 angle,
7132 parent_model_index,
7133 parent_model_bone_index,
7134 interpolation,
7135 perspective_view,
7136 fov,
7137 selected,
7138 offset,
7139 byte_length,
7140 payload_offset,
7141 payload_byte_length,
7142 payload_bytes,
7143 distance_offset,
7144 look_at_offset,
7145 look_at_byte_length,
7146 angle_offset,
7147 angle_byte_length,
7148 parent_model_index_offset,
7149 parent_model_bone_index_offset,
7150 interpolation_offset,
7151 interpolation_byte_length,
7152 perspective_view_offset,
7153 fov_offset,
7154 selected_offset,
7155 ..
7156 } => {
7157 assert_eq!(*index, None);
7158 assert_eq!(*frame_index, 0);
7159 assert_eq!(*offset, global.camera.offset);
7160 assert_eq!(*byte_length, 78);
7161 assert_eq!(*payload_offset, *offset + 12);
7162 assert_eq!(*payload_byte_length, *byte_length - 12);
7163 assert_eq!(payload_bytes.len(), *payload_byte_length);
7164 assert_eq!(&payload_bytes[..4], &(*distance).to_le_bytes()[..]);
7165 assert_eq!(payload_bytes.last().copied(), Some(*selected as u8));
7166 assert_eq!(*distance, 45.0);
7167 assert_eq!(*look_at, [0.0, 0.0, 0.0]);
7168 assert_eq!(*angle, [0.0, 0.0, 0.0]);
7169 assert_eq!(*parent_model_index, -1);
7170 assert_eq!(*parent_model_bone_index, -1);
7171 assert_eq!(interpolation.len(), 24);
7173 assert_eq!(*interpolation, [20u8; 24]);
7174 let interp_offset_in_payload = 4 + 12 + 12 + 4 + 4; assert_eq!(
7176 &payload_bytes[interp_offset_in_payload..interp_offset_in_payload + 24],
7177 &interpolation[..]
7178 );
7179 assert_eq!(
7180 &payload_bytes[interp_offset_in_payload..interp_offset_in_payload + 24],
7181 &[20u8; 24]
7182 );
7183 assert!(*perspective_view);
7184 assert_eq!(*fov, 30);
7185 assert!(!*selected);
7186
7187 assert_eq!(*distance_offset, *payload_offset);
7190 assert_eq!(*look_at_offset, *payload_offset + 4);
7191 assert_eq!(*look_at_byte_length, 12);
7192 assert_eq!(*angle_offset, *payload_offset + 16);
7193 assert_eq!(*angle_byte_length, 12);
7194 assert_eq!(*parent_model_index_offset, *payload_offset + 28);
7195 assert_eq!(*parent_model_bone_index_offset, *payload_offset + 32);
7196 assert_eq!(*interpolation_offset, *payload_offset + 36);
7197 assert_eq!(*interpolation_byte_length, 24);
7198 assert_eq!(*perspective_view_offset, *payload_offset + 60);
7199 assert_eq!(*fov_offset, *payload_offset + 61);
7200 assert_eq!(*selected_offset, *payload_offset + 65);
7201 let p0 = &payload_bytes;
7202 assert_eq!(
7203 &p0[*distance_offset - *payload_offset..*distance_offset - *payload_offset + 4],
7204 &45.0f32.to_le_bytes()
7205 );
7206 let la0 = *look_at_offset - *payload_offset;
7207 assert_eq!(&p0[la0..la0 + 12], &[0u8; 12]);
7208 let ang0 = *angle_offset - *payload_offset;
7209 assert_eq!(&p0[ang0..ang0 + 12], &[0u8; 12]);
7210 assert_eq!(
7211 &p0[*parent_model_index_offset - *payload_offset
7212 ..*parent_model_index_offset - *payload_offset + 4],
7213 &(-1i32).to_le_bytes()
7214 );
7215 assert_eq!(
7216 &p0[*parent_model_bone_index_offset - *payload_offset
7217 ..*parent_model_bone_index_offset - *payload_offset + 4],
7218 &(-1i32).to_le_bytes()
7219 );
7220 let ip0 = *interpolation_offset - *payload_offset;
7221 assert_eq!(&p0[ip0..ip0 + 24], &[20u8; 24]);
7222 assert_eq!(p0[*perspective_view_offset - *payload_offset], 0u8);
7223 assert_eq!(
7224 &p0[*fov_offset - *payload_offset..*fov_offset - *payload_offset + 4],
7225 &30i32.to_le_bytes()
7226 );
7227 assert_eq!(p0[*selected_offset - *payload_offset], 0u8);
7228 }
7229 other => panic!("unexpected camera keyframe summary: {other:?}"),
7230 }
7231 match &global.camera.keyframe_summaries[0] {
7232 PmmDocumentKeyframeSummary::Camera {
7233 index,
7234 frame_index,
7235 distance,
7236 parent_model_index,
7237 parent_model_bone_index,
7238 interpolation,
7239 selected,
7240 offset,
7241 byte_length,
7242 payload_offset,
7243 payload_byte_length,
7244 payload_bytes,
7245 distance_offset,
7246 look_at_offset,
7247 look_at_byte_length,
7248 angle_offset,
7249 angle_byte_length,
7250 parent_model_index_offset,
7251 parent_model_bone_index_offset,
7252 interpolation_offset,
7253 interpolation_byte_length,
7254 perspective_view_offset,
7255 fov_offset,
7256 selected_offset,
7257 ..
7258 } => {
7259 assert_eq!(*index, Some(42));
7260 assert_eq!(*frame_index, 42);
7261 assert_eq!(*offset, global.camera.keyframes_offset);
7262 assert_eq!(*byte_length, 82);
7263 assert_eq!(*payload_offset, *offset + 16);
7264 assert_eq!(*payload_byte_length, *byte_length - 16);
7265 assert_eq!(payload_bytes.len(), *payload_byte_length);
7266 assert_eq!(&payload_bytes[..4], &(*distance).to_le_bytes()[..]);
7267 assert_eq!(payload_bytes.last().copied(), Some(*selected as u8));
7268 assert_eq!(interpolation.len(), 24);
7270 assert_eq!(*interpolation, [20u8; 24]);
7271 let interp_offset_in_payload = 4 + 12 + 12 + 4 + 4; assert_eq!(
7273 &payload_bytes[interp_offset_in_payload..interp_offset_in_payload + 24],
7274 &interpolation[..]
7275 );
7276 assert_eq!(*parent_model_index, -1);
7277 assert_eq!(*parent_model_bone_index, -1);
7278
7279 assert_eq!(*distance_offset, *payload_offset);
7281 assert_eq!(*look_at_offset, *payload_offset + 4);
7282 assert_eq!(*look_at_byte_length, 12);
7283 assert_eq!(*angle_offset, *payload_offset + 16);
7284 assert_eq!(*angle_byte_length, 12);
7285 assert_eq!(*parent_model_index_offset, *payload_offset + 28);
7286 assert_eq!(*parent_model_bone_index_offset, *payload_offset + 32);
7287 assert_eq!(*interpolation_offset, *payload_offset + 36);
7288 assert_eq!(*interpolation_byte_length, 24);
7289 assert_eq!(*perspective_view_offset, *payload_offset + 60);
7290 assert_eq!(*fov_offset, *payload_offset + 61);
7291 assert_eq!(*selected_offset, *payload_offset + 65);
7292 let p1 = &payload_bytes;
7293 assert_eq!(
7294 &p1[*distance_offset - *payload_offset..*distance_offset - *payload_offset + 4],
7295 &(*distance).to_le_bytes()[..]
7296 );
7297 let la1 = *look_at_offset - *payload_offset;
7298 assert_eq!(&p1[la1..la1 + 12], &[0u8; 12]);
7299 let ang1 = *angle_offset - *payload_offset;
7300 assert_eq!(&p1[ang1..ang1 + 12], &[0u8; 12]);
7301 assert_eq!(
7302 &p1[*parent_model_index_offset - *payload_offset
7303 ..*parent_model_index_offset - *payload_offset + 4],
7304 &(-1i32).to_le_bytes()
7305 );
7306 assert_eq!(
7307 &p1[*parent_model_bone_index_offset - *payload_offset
7308 ..*parent_model_bone_index_offset - *payload_offset + 4],
7309 &(-1i32).to_le_bytes()
7310 );
7311 let ip1 = *interpolation_offset - *payload_offset;
7312 assert_eq!(&p1[ip1..ip1 + 24], &[20u8; 24]);
7313 assert_eq!(p1[*perspective_view_offset - *payload_offset], 0u8);
7314 assert_eq!(
7315 &p1[*fov_offset - *payload_offset..*fov_offset - *payload_offset + 4],
7316 &30i32.to_le_bytes()
7317 );
7318 assert_eq!(p1[*selected_offset - *payload_offset], 0u8);
7319 }
7320 other => panic!("unexpected additional camera keyframe summary: {other:?}"),
7321 }
7322 assert_eq!(global.light.initial_keyframes, 1);
7323 assert_eq!(global.light.keyframes, 1);
7324 match global.light.initial_keyframe.as_ref().unwrap() {
7325 PmmDocumentKeyframeSummary::Light {
7326 color,
7327 direction,
7328 selected,
7329 offset,
7330 byte_length,
7331 payload_offset,
7332 payload_byte_length,
7333 payload_bytes,
7334 color_offset,
7335 color_byte_length,
7336 direction_offset,
7337 direction_byte_length,
7338 selected_offset,
7339 ..
7340 } => {
7341 assert_eq!(*offset, global.light.offset);
7342 assert_eq!(*byte_length, 37);
7343 assert_eq!(*payload_offset, *offset + 12);
7344 assert_eq!(*payload_byte_length, *byte_length - 12);
7345 assert_eq!(payload_bytes.len(), *payload_byte_length);
7346 let mut color_bytes = Vec::with_capacity(12);
7347 for c in color {
7348 color_bytes.extend_from_slice(&c.to_le_bytes());
7349 }
7350 assert_eq!(&payload_bytes[..12], &color_bytes[..]);
7351 assert_eq!(payload_bytes.last().copied(), Some(*selected as u8));
7352 assert_eq!(*color, [0.6, 0.6, 0.6]);
7353 assert_eq!(*direction, [-0.5, -1.0, 0.5]);
7354 assert!(!*selected);
7355
7356 assert_eq!(*color_offset, *payload_offset);
7358 assert_eq!(*color_byte_length, 12);
7359 assert_eq!(*direction_offset, *payload_offset + 12);
7360 assert_eq!(*direction_byte_length, 12);
7361 assert_eq!(*selected_offset, *payload_offset + 24);
7362 let p0 = &payload_bytes;
7363 let mut color_bytes = Vec::with_capacity(12);
7364 for c in color {
7365 color_bytes.extend_from_slice(&c.to_le_bytes());
7366 }
7367 assert_eq!(
7368 &p0[*color_offset - *payload_offset..*color_offset - *payload_offset + 12],
7369 &color_bytes[..]
7370 );
7371 let d0 = *direction_offset - *payload_offset;
7372 assert_eq!(
7373 &p0[d0..d0 + 12],
7374 &[-0.5f32, -1.0, 0.5]
7375 .iter()
7376 .flat_map(|f| f.to_le_bytes())
7377 .collect::<Vec<_>>()[..]
7378 );
7379 assert_eq!(p0[*selected_offset - *payload_offset], 0u8);
7380 }
7381 other => panic!("unexpected light keyframe summary: {other:?}"),
7382 }
7383 match &global.light.keyframe_summaries[0] {
7384 PmmDocumentKeyframeSummary::Light {
7385 index,
7386 frame_index,
7387 color,
7388 selected,
7389 offset,
7390 byte_length,
7391 payload_offset,
7392 payload_byte_length,
7393 payload_bytes,
7394 color_offset,
7395 color_byte_length,
7396 direction_offset,
7397 direction_byte_length,
7398 selected_offset,
7399 ..
7400 } => {
7401 assert_eq!(*index, Some(43));
7402 assert_eq!(*frame_index, 43);
7403 assert_eq!(*offset, global.light.keyframes_offset);
7404 assert_eq!(*byte_length, 41);
7405 assert_eq!(*payload_offset, *offset + 16);
7406 assert_eq!(*payload_byte_length, *byte_length - 16);
7407 assert_eq!(payload_bytes.len(), *payload_byte_length);
7408 let mut color_bytes = Vec::with_capacity(12);
7409 for c in color {
7410 color_bytes.extend_from_slice(&c.to_le_bytes());
7411 }
7412 assert_eq!(&payload_bytes[..12], &color_bytes[..]);
7413 assert_eq!(payload_bytes.last().copied(), Some(*selected as u8));
7414
7415 assert_eq!(*color_offset, *payload_offset);
7417 assert_eq!(*color_byte_length, 12);
7418 assert_eq!(*direction_offset, *payload_offset + 12);
7419 assert_eq!(*direction_byte_length, 12);
7420 assert_eq!(*selected_offset, *payload_offset + 24);
7421 let p1 = &payload_bytes;
7422 let mut color_bytes2 = Vec::with_capacity(12);
7423 for c in color {
7424 color_bytes2.extend_from_slice(&c.to_le_bytes());
7425 }
7426 assert_eq!(
7427 &p1[*color_offset - *payload_offset..*color_offset - *payload_offset + 12],
7428 &color_bytes2[..]
7429 );
7430 let d1 = *direction_offset - *payload_offset;
7431 assert_eq!(
7432 &p1[d1..d1 + 12],
7433 &[-0.5f32, -1.0, 0.5]
7434 .iter()
7435 .flat_map(|f| f.to_le_bytes())
7436 .collect::<Vec<_>>()[..]
7437 );
7438 assert_eq!(p1[*selected_offset - *payload_offset], 0u8);
7439 }
7440 other => panic!("unexpected additional light keyframe summary: {other:?}"),
7441 }
7442 assert_eq!(global.accessories.accessory_count, 1);
7443 assert_eq!(global.accessories.keyframes, 1);
7444 let accessory = &global.accessories.accessories[0];
7445 assert_eq!(accessory.slot_index, 0);
7446 assert_eq!(accessory.document_accessory_index, 0);
7447 assert_eq!(accessory.name, "Stage");
7448 assert_eq!(accessory.path, "UserFile\\Accessory\\stage.x");
7449 assert_eq!(accessory.keyframes, 1);
7450 assert!(accessory.visible);
7451 assert_eq!(accessory.opacity, 1.0);
7452 assert_eq!(accessory.scale_factor, 10.0);
7453 assert!(accessory.shadow_enabled);
7454 assert!(!accessory.add_blend_enabled);
7455 assert_eq!(accessory.initial_keyframe.index, None);
7456 assert_eq!(accessory.initial_keyframe.frame_index, 0);
7457 assert_eq!(
7458 accessory.initial_keyframe.offset,
7459 accessory.offset + 1 + 100 + 256 + 1
7460 );
7461 assert_eq!(accessory.initial_keyframe.byte_length, 51);
7462 assert_eq!(
7463 accessory.initial_keyframe.payload_offset,
7464 accessory.initial_keyframe.offset + 12
7465 );
7466 assert_eq!(accessory.initial_keyframe.payload_byte_length, 39);
7467 assert_eq!(accessory.initial_keyframe.payload_bytes.len(), 39);
7468 assert_eq!(
7469 accessory.initial_keyframe.payload_bytes.len(),
7470 accessory.initial_keyframe.payload_byte_length
7471 );
7472 assert_eq!(accessory.initial_keyframe.payload_bytes[0], 1);
7473 assert_eq!(
7474 &accessory.initial_keyframe.payload_bytes
7475 [accessory.initial_keyframe.payload_bytes.len() - 2..],
7476 &[1, 0]
7477 );
7478 assert_eq!(accessory.initial_keyframe.translation, [1.0, 2.0, 3.0]);
7479 assert_eq!(accessory.initial_keyframe.orientation, [0.1, 0.2, 0.3]);
7480 assert_eq!(
7482 accessory.initial_keyframe.packed_opacity_visible_offset,
7483 accessory.initial_keyframe.payload_offset
7484 );
7485 assert_eq!(
7486 accessory.initial_keyframe.selected_offset,
7487 accessory.initial_keyframe.payload_offset + 38
7488 );
7489 assert_eq!(accessory.initial_keyframe.translation_byte_length, 12);
7490 assert_eq!(accessory.initial_keyframe.orientation_byte_length, 12);
7491 assert_eq!(accessory.keyframe_summaries[0].index, Some(46));
7492 assert_eq!(accessory.keyframe_summaries[0].frame_index, 46);
7493 assert_eq!(
7494 accessory.keyframe_summaries[0].offset,
7495 accessory.keyframes_offset
7496 );
7497 assert_eq!(accessory.keyframe_summaries[0].byte_length, 55);
7498 assert_eq!(
7499 accessory.keyframe_summaries[0].payload_offset,
7500 accessory.keyframe_summaries[0].offset + 16
7501 );
7502 assert_eq!(accessory.keyframe_summaries[0].payload_byte_length, 39);
7503 assert_eq!(accessory.keyframe_summaries[0].payload_bytes.len(), 39);
7504 assert_eq!(
7505 accessory.keyframe_summaries[0].payload_bytes.len(),
7506 accessory.keyframe_summaries[0].payload_byte_length
7507 );
7508 assert_eq!(accessory.keyframe_summaries[0].payload_bytes[0], 1);
7509 assert_eq!(
7510 &accessory.keyframe_summaries[0].payload_bytes
7511 [accessory.keyframe_summaries[0].payload_bytes.len() - 2..],
7512 &[1, 0]
7513 );
7514 assert_eq!(
7516 accessory.keyframe_summaries[0].packed_opacity_visible_offset,
7517 accessory.keyframe_summaries[0].payload_offset
7518 );
7519 assert_eq!(
7520 accessory.keyframe_summaries[0].scale_factor_offset,
7521 accessory.keyframe_summaries[0].payload_offset + 33
7522 );
7523 assert_eq!(
7524 accessory.keyframe_summaries[0].shadow_enabled_offset,
7525 accessory.keyframe_summaries[0].payload_offset + 37
7526 );
7527 assert_eq!(global.gravity.initial_keyframes, 1);
7528 assert_eq!(global.gravity.keyframes, 1);
7529 match global.gravity.initial_keyframe.as_ref().unwrap() {
7530 PmmDocumentKeyframeSummary::Gravity {
7531 noise_enabled,
7532 noise,
7533 acceleration,
7534 direction,
7535 selected,
7536 offset,
7537 byte_length,
7538 payload_offset,
7539 payload_byte_length,
7540 payload_bytes,
7541 noise_enabled_offset,
7542 noise_offset,
7543 acceleration_offset,
7544 direction_offset,
7545 direction_byte_length,
7546 selected_offset,
7547 ..
7548 } => {
7549 assert_eq!(*offset, global.gravity.state_end_offset.unwrap());
7550 assert_eq!(*byte_length, 34);
7551 assert_eq!(*payload_offset, *offset + 12);
7552 assert_eq!(*payload_byte_length, *byte_length - 12);
7553 assert_eq!(payload_bytes.len(), *payload_byte_length);
7554 assert_eq!(payload_bytes[0], *noise_enabled as u8);
7555 assert_eq!(payload_bytes.last().copied(), Some(*selected as u8));
7556 assert!(!*noise_enabled);
7557 assert_eq!(*noise, 10);
7558 assert_eq!(*acceleration, 9.8);
7559 assert_eq!(*direction, [0.0, -1.0, 0.0]);
7560 assert!(!*selected);
7561
7562 assert_eq!(*noise_enabled_offset, *payload_offset);
7564 assert_eq!(*noise_offset, *payload_offset + 1);
7565 assert_eq!(*acceleration_offset, *payload_offset + 5);
7566 assert_eq!(*direction_offset, *payload_offset + 9);
7567 assert_eq!(*direction_byte_length, 12);
7568 assert_eq!(*selected_offset, *payload_offset + 21);
7569 let p0 = &payload_bytes;
7570 assert_eq!(
7571 p0[*noise_enabled_offset - *payload_offset],
7572 *noise_enabled as u8
7573 );
7574 assert_eq!(
7575 &p0[*noise_offset - *payload_offset..*noise_offset - *payload_offset + 4],
7576 &10i32.to_le_bytes()
7577 );
7578 assert_eq!(
7579 &p0[*acceleration_offset - *payload_offset
7580 ..*acceleration_offset - *payload_offset + 4],
7581 &9.8f32.to_le_bytes()
7582 );
7583 let dir0 = *direction_offset - *payload_offset;
7584 assert_eq!(
7585 &p0[dir0..dir0 + 12],
7586 &[0.0f32, -1.0, 0.0]
7587 .iter()
7588 .flat_map(|f| f.to_le_bytes())
7589 .collect::<Vec<_>>()[..]
7590 );
7591 assert_eq!(p0[*selected_offset - *payload_offset], 0u8);
7592 }
7593 other => panic!("unexpected gravity keyframe summary: {other:?}"),
7594 }
7595 match &global.gravity.keyframe_summaries[0] {
7596 PmmDocumentKeyframeSummary::Gravity {
7597 index,
7598 frame_index,
7599 noise_enabled,
7600 selected,
7601 offset,
7602 byte_length,
7603 payload_offset,
7604 payload_byte_length,
7605 payload_bytes,
7606 noise_enabled_offset,
7607 noise_offset,
7608 acceleration_offset,
7609 direction_offset,
7610 direction_byte_length,
7611 selected_offset,
7612 ..
7613 } => {
7614 assert_eq!(*index, Some(44));
7615 assert_eq!(*frame_index, 44);
7616 assert_eq!(*offset, global.gravity.keyframes_offset);
7617 assert_eq!(*byte_length, 38);
7618 assert_eq!(*payload_offset, *offset + 16);
7619 assert_eq!(*payload_byte_length, *byte_length - 16);
7620 assert_eq!(payload_bytes.len(), *payload_byte_length);
7621 assert_eq!(payload_bytes[0], *noise_enabled as u8);
7622 assert_eq!(payload_bytes.last().copied(), Some(*selected as u8));
7623
7624 assert_eq!(*noise_enabled_offset, *payload_offset);
7626 assert_eq!(*noise_offset, *payload_offset + 1);
7627 assert_eq!(*acceleration_offset, *payload_offset + 5);
7628 assert_eq!(*direction_offset, *payload_offset + 9);
7629 assert_eq!(*direction_byte_length, 12);
7630 assert_eq!(*selected_offset, *payload_offset + 21);
7631 let p1 = &payload_bytes;
7632 assert_eq!(
7633 p1[*noise_enabled_offset - *payload_offset],
7634 *noise_enabled as u8
7635 );
7636 assert_eq!(
7637 &p1[*noise_offset - *payload_offset..*noise_offset - *payload_offset + 4],
7638 &10i32.to_le_bytes()
7639 );
7640 assert_eq!(
7641 &p1[*acceleration_offset - *payload_offset
7642 ..*acceleration_offset - *payload_offset + 4],
7643 &9.8f32.to_le_bytes()
7644 );
7645 let dir1 = *direction_offset - *payload_offset;
7646 assert_eq!(
7647 &p1[dir1..dir1 + 12],
7648 &[0.0f32, -1.0, 0.0]
7649 .iter()
7650 .flat_map(|f| f.to_le_bytes())
7651 .collect::<Vec<_>>()[..]
7652 );
7653 assert_eq!(p1[*selected_offset - *payload_offset], 0u8);
7654 }
7655 other => panic!("unexpected additional gravity keyframe summary: {other:?}"),
7656 }
7657 assert_eq!(global.self_shadow.initial_keyframes, 1);
7658 assert_eq!(global.self_shadow.keyframes, 1);
7659 match global.self_shadow.initial_keyframe.as_ref().unwrap() {
7660 PmmDocumentKeyframeSummary::SelfShadow {
7661 mode,
7662 distance,
7663 selected,
7664 offset,
7665 byte_length,
7666 payload_offset,
7667 payload_byte_length,
7668 payload_bytes,
7669 mode_offset,
7670 distance_offset,
7671 selected_offset,
7672 ..
7673 } => {
7674 assert_eq!(*offset, global.self_shadow.state_end_offset.unwrap());
7675 assert_eq!(*byte_length, 18);
7676 assert_eq!(*payload_offset, *offset + 12);
7677 assert_eq!(*payload_byte_length, *byte_length - 12);
7678 assert_eq!(payload_bytes.len(), *payload_byte_length);
7679 assert_eq!(payload_bytes[0], *mode);
7680 assert_eq!(payload_bytes.last().copied(), Some(*selected as u8));
7681 assert_eq!(*mode, 1);
7682 assert_eq!(*distance, 8875.0);
7683 assert!(!*selected);
7684
7685 assert_eq!(*mode_offset, *payload_offset);
7687 assert_eq!(*distance_offset, *payload_offset + 1);
7688 assert_eq!(*selected_offset, *payload_offset + 5);
7689 let p0 = &payload_bytes;
7690 assert_eq!(p0[*mode_offset - *payload_offset], *mode);
7691 assert_eq!(
7692 &p0[*distance_offset - *payload_offset..*distance_offset - *payload_offset + 4],
7693 &8875.0f32.to_le_bytes()
7694 );
7695 assert_eq!(p0[*selected_offset - *payload_offset], 0u8);
7696 }
7697 other => panic!("unexpected self-shadow keyframe summary: {other:?}"),
7698 }
7699 match &global.self_shadow.keyframe_summaries[0] {
7700 PmmDocumentKeyframeSummary::SelfShadow {
7701 index,
7702 frame_index,
7703 mode,
7704 selected,
7705 offset,
7706 byte_length,
7707 payload_offset,
7708 payload_byte_length,
7709 payload_bytes,
7710 mode_offset,
7711 distance_offset,
7712 selected_offset,
7713 ..
7714 } => {
7715 assert_eq!(*index, Some(45));
7716 assert_eq!(*frame_index, 45);
7717 assert_eq!(*offset, global.self_shadow.keyframes_offset);
7718 assert_eq!(*byte_length, 22);
7719 assert_eq!(*payload_offset, *offset + 16);
7720 assert_eq!(*payload_byte_length, *byte_length - 16);
7721 assert_eq!(payload_bytes.len(), *payload_byte_length);
7722 assert_eq!(payload_bytes[0], *mode);
7723 assert_eq!(payload_bytes.last().copied(), Some(*selected as u8));
7724
7725 assert_eq!(*mode_offset, *payload_offset);
7727 assert_eq!(*distance_offset, *payload_offset + 1);
7728 assert_eq!(*selected_offset, *payload_offset + 5);
7729 let p1 = &payload_bytes;
7730 assert_eq!(p1[*mode_offset - *payload_offset], *mode);
7731 assert_eq!(
7732 &p1[*distance_offset - *payload_offset..*distance_offset - *payload_offset + 4],
7733 &8875.0f32.to_le_bytes()
7734 );
7735 assert_eq!(p1[*selected_offset - *payload_offset], 0u8);
7736 }
7737 other => panic!("unexpected additional self-shadow keyframe summary: {other:?}"),
7738 }
7739 assert_eq!(global.settings.current_frame_index, 12);
7740 assert_eq!(global.settings.horizontal_scroll, 34);
7741 assert_eq!(global.settings.horizontal_scroll_thumb, 56);
7742 assert_eq!(global.settings.begin_frame_index, 3);
7743 assert_eq!(global.settings.end_frame_index, 120);
7744 assert!(global.settings.audio_enabled);
7745 assert!(global.settings.grid_and_axis_shown);
7746 assert!(global.settings.physics_ground_enabled);
7747 assert!(global.settings.model_selection_footer_present);
7748 assert!(global.offset_end > global.offset);
7749 assert!(global.camera.offset < global.light.offset);
7750 assert!(global.light.offset < global.accessories.offset);
7751 assert!(global.gravity.offset < global.self_shadow.offset);
7752 }
7753
7754 #[test]
7755 fn parses_pmm_project_graph_only_when_full_document_and_global_summary_present() {
7756 let parsed_minimal = parse_pmm_manifest(
7758 b"Polygon Movie maker 0002\0UserFile\\Model\\miku.pmx\0UserFile\\Motion\\walk.vmd\0",
7759 )
7760 .unwrap();
7761 assert!(parsed_minimal.project_graph.is_none());
7762
7763 let parsed_settings = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
7764 assert!(parsed_settings.project_graph.is_none());
7765
7766 let parsed_doc_only = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
7767 assert!(parsed_doc_only.project_graph.is_none());
7768 assert!(parsed_doc_only.document_summary.is_some());
7769 assert!(parsed_doc_only.document_global_summary.is_none());
7770
7771 let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
7773 let graph = parsed
7774 .project_graph
7775 .as_ref()
7776 .expect("project_graph must be Some for pmm_with_document_global_summary");
7777 assert_eq!(
7778 graph.source,
7779 "mmd-anim-format first PMMv2 document graph DTO slice"
7780 );
7781 assert_eq!(graph.version, "0002");
7782 assert_eq!(graph.parsed_version, Some(2));
7783
7784 let doc = parsed.document_summary.as_ref().unwrap();
7786 let glob = parsed.document_global_summary.as_ref().unwrap();
7787 assert_eq!(graph.model_counts.models, doc.counts.models);
7788 assert_eq!(graph.model_counts.bones, doc.counts.bones);
7789 assert_eq!(graph.model_counts.morphs, doc.counts.morphs);
7790 assert_eq!(graph.model_counts.bone_keyframes, doc.counts.bone_keyframes);
7791 assert_eq!(
7792 graph.model_counts.morph_keyframes,
7793 doc.counts.morph_keyframes
7794 );
7795 assert_eq!(graph.accessory_count, glob.accessories.accessory_count);
7796 assert_eq!(graph.accessory_keyframes, glob.accessories.keyframes);
7797 assert_eq!(graph.models.len(), doc.models.len());
7798 assert_eq!(graph.global.camera.keyframes, glob.camera.keyframes);
7799 }
7800
7801 #[test]
7802 fn pmm_project_scene_settings_synthetic_pmmv2_full_fixture() {
7803 let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
7804 let graph = parsed
7805 .project_graph
7806 .as_ref()
7807 .expect("project_graph must be Some for synthetic PMMv2 full fixture");
7808 let ss = &graph.scene_settings;
7809 let glob = parsed.document_global_summary.as_ref().unwrap();
7810 let settings = &glob.settings;
7811
7812 assert_eq!(ss.offset, settings.offset);
7814 assert_eq!(ss.offset_end, settings.offset_end);
7815
7816 assert_eq!(ss.current_frame_index, settings.current_frame_index);
7818 assert_eq!(
7819 ss.current_frame_index_in_text_field,
7820 settings.current_frame_index_in_text_field
7821 );
7822 assert_eq!(
7823 ss.begin_frame_index_enabled,
7824 settings.begin_frame_index_enabled
7825 );
7826 assert_eq!(ss.end_frame_index_enabled, settings.end_frame_index_enabled);
7827 assert_eq!(ss.begin_frame_index, settings.begin_frame_index);
7828 assert_eq!(ss.end_frame_index, settings.end_frame_index);
7829 assert_eq!(ss.preferred_fps, settings.preferred_fps);
7830 assert_eq!(ss.loop_enabled, settings.loop_enabled);
7831
7832 assert_eq!(ss.audio_enabled, settings.audio_enabled);
7834 assert_eq!(ss.audio_path, settings.audio_path);
7835 assert_eq!(
7836 ss.background_video_enabled,
7837 settings.background_video_enabled
7838 );
7839 assert_eq!(ss.background_video_path, settings.background_video_path);
7840 assert_eq!(
7841 ss.background_image_enabled,
7842 settings.background_image_enabled
7843 );
7844 assert_eq!(ss.background_image_path, settings.background_image_path);
7845
7846 assert!(
7848 ss.audio_path.is_empty(),
7849 "synthetic fixture uses empty audio path"
7850 );
7851 assert!(
7852 ss.background_video_path.is_empty(),
7853 "synthetic fixture uses empty background video path"
7854 );
7855 assert!(
7856 ss.background_image_path.is_empty(),
7857 "synthetic fixture uses empty background image path"
7858 );
7859 assert!(
7860 ss.audio_asset_reference_index.is_none(),
7861 "empty audio path must resolve to None asset reference index"
7862 );
7863 assert!(
7864 ss.background_video_asset_reference_index.is_none(),
7865 "empty background video path must resolve to None asset reference index"
7866 );
7867 assert!(
7868 ss.background_image_asset_reference_index.is_none(),
7869 "empty background image path must resolve to None asset reference index"
7870 );
7871 }
7872
7873 #[test]
7874 fn pmm_project_asset_bindings_synthetic_pmmv2_full_fixture() {
7875 let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
7876 let graph = parsed
7877 .project_graph
7878 .as_ref()
7879 .expect("project_graph must be Some for synthetic PMMv2 full fixture");
7880 let doc = parsed.document_summary.as_ref().unwrap();
7881 let glob = parsed.document_global_summary.as_ref().unwrap();
7882
7883 assert_eq!(
7885 graph.asset_bindings.len(),
7886 2,
7887 "synthetic PMMv2 full fixture must have exactly 2 asset bindings (1 model + 1 accessory)"
7888 );
7889 let model_bindings: Vec<_> = graph
7890 .asset_bindings
7891 .iter()
7892 .filter(|b| b.scope == "model")
7893 .collect();
7894 let acc_bindings: Vec<_> = graph
7895 .asset_bindings
7896 .iter()
7897 .filter(|b| b.scope == "accessory")
7898 .collect();
7899 let scene_bindings: Vec<_> = graph
7900 .asset_bindings
7901 .iter()
7902 .filter(|b| b.scope == "sceneSettings")
7903 .collect();
7904 assert_eq!(model_bindings.len(), 1);
7905 assert_eq!(acc_bindings.len(), 1);
7906 assert_eq!(
7907 scene_bindings.len(),
7908 0,
7909 "no sceneSettings binding exists because the synthetic fixture paths are empty"
7910 );
7911
7912 let model0 = &doc.models[0];
7914 let mb = model_bindings[0];
7915 assert_eq!(mb.scope, "model");
7916 assert_eq!(mb.asset_kind, "model");
7917 assert_eq!(mb.owner_index, Some(model0.slot_index));
7918 assert_eq!(mb.document_index, Some(model0.document_model_index));
7919 assert_eq!(mb.owner_name.as_deref(), Some(model0.name.as_str()));
7920 assert_eq!(mb.path, model0.path);
7921 assert_eq!(mb.path_offset, Some(model0.path_offset));
7922 assert_eq!(mb.asset_reference_index, model0.asset_reference_index);
7923 if let Some(idx) = mb.asset_reference_index {
7924 let ar = &graph.asset_references[idx];
7925 assert_eq!(mb.asset_reference_offset, Some(ar.offset));
7926 assert_eq!(mb.asset_reference_end_offset, Some(ar.offset_end));
7927 assert_eq!(mb.asset_reference_confidence, Some(ar.confidence));
7928 }
7929
7930 let acc0 = &glob.accessories.accessories[0];
7932 let ab = acc_bindings[0];
7933 assert_eq!(ab.scope, "accessory");
7934 assert_eq!(ab.asset_kind, "accessory");
7935 assert_eq!(ab.owner_index, Some(acc0.slot_index));
7936 assert_eq!(ab.document_index, Some(acc0.document_accessory_index));
7937 assert_eq!(ab.owner_name.as_deref(), Some(acc0.name.as_str()));
7938 assert_eq!(ab.path, acc0.path);
7939 assert_eq!(ab.path_offset, Some(acc0.path_offset));
7940 assert_eq!(ab.asset_reference_index, acc0.asset_reference_index);
7941 if let Some(idx) = ab.asset_reference_index {
7942 let ar = &graph.asset_references[idx];
7943 assert_eq!(ab.asset_reference_offset, Some(ar.offset));
7944 assert_eq!(ab.asset_reference_end_offset, Some(ar.offset_end));
7945 assert_eq!(ab.asset_reference_confidence, Some(ar.confidence));
7946 }
7947 }
7948
7949 #[test]
7950 fn pmm_project_export_readiness_synthetic_pmmv2_full_fixture() {
7951 let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
7952 let graph = parsed
7953 .project_graph
7954 .as_ref()
7955 .expect("project_graph must be Some for synthetic PMMv2 full fixture");
7956 let cov = &graph.byte_coverage;
7957 let r = &graph.export_readiness;
7958
7959 assert!(
7960 r.lossless_parsed_byte_export_supported,
7961 "lossless parsed-byte export supported"
7962 );
7963 assert!(
7964 !r.semantic_graph_export_supported,
7965 "semantic graph export must be false (full exporter unfinished)"
7966 );
7967 assert!(
7968 r.source_byte_preservation_required,
7969 "source byte preservation required"
7970 );
7971 assert_eq!(
7972 r.blocker_count,
7973 r.blockers.len(),
7974 "blocker_count must equal blockers.len()"
7975 );
7976
7977 let semantic_blocker = r
7979 .blockers
7980 .iter()
7981 .find(|b| b.code == "PMM_SEMANTIC_EXPORTER_UNFINISHED")
7982 .expect("PMM_SEMANTIC_EXPORTER_UNFINISHED blocker must be present");
7983 assert_eq!(semantic_blocker.severity, "blocker");
7984 assert!(semantic_blocker.scope.is_none());
7985 assert!(semantic_blocker.kind.is_none());
7986 assert!(
7987 semantic_blocker
7988 .message
7989 .contains("semantic graph export/editing is not yet supported"),
7990 "message must state full semantic graph export/editing is not yet supported"
7991 );
7992
7993 let has_gap_blocker = r
7995 .blockers
7996 .iter()
7997 .any(|b| b.code == "PMM_DECODED_BYTE_GAPS_REMAIN");
7998 assert_eq!(
7999 has_gap_blocker,
8000 cov.gap_count > 0,
8001 "PMM_DECODED_BYTE_GAPS_REMAIN presence must match gap_count > 0"
8002 );
8003 }
8004
8005 #[test]
8006 fn pmm_project_track_reference_synthetic_pmmv2_full_fixture() {
8007 let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
8008 let graph = parsed
8009 .project_graph
8010 .as_ref()
8011 .expect("project_graph must be Some for synthetic PMMv2 full fixture");
8012
8013 assert_eq!(
8015 graph.track_references.len(),
8016 8,
8017 "synthetic PMMv2 full fixture must have exactly 8 track references"
8018 );
8019 let model_tracks: Vec<_> = graph
8020 .track_references
8021 .iter()
8022 .filter(|r| r.scope == "model")
8023 .collect();
8024 let global_tracks: Vec<_> = graph
8025 .track_references
8026 .iter()
8027 .filter(|r| r.scope == "global")
8028 .collect();
8029 let accessory_tracks: Vec<_> = graph
8030 .track_references
8031 .iter()
8032 .filter(|r| r.scope == "accessory")
8033 .collect();
8034 assert_eq!(
8035 model_tracks.len(),
8036 3,
8037 "per document model: bone + morph + model track groups"
8038 );
8039 assert_eq!(
8040 global_tracks.len(),
8041 4,
8042 "global: camera + light + gravity + selfShadow"
8043 );
8044 assert_eq!(
8045 accessory_tracks.len(),
8046 1,
8047 "each decoded accessory contributes one accessory track group"
8048 );
8049
8050 let doc = parsed.document_summary.as_ref().unwrap();
8051 let glob = parsed.document_global_summary.as_ref().unwrap();
8052
8053 let bone_ref = graph
8055 .track_references
8056 .iter()
8057 .find(|r| r.scope == "model" && r.track_kind == "bone")
8058 .expect("model bone track ref");
8059 let model0 = &doc.models[0];
8060 assert_eq!(bone_ref.owner_index, Some(model0.slot_index));
8061 assert_eq!(bone_ref.document_index, Some(model0.document_model_index));
8062 assert_eq!(bone_ref.owner_name.as_deref(), Some(model0.name.as_str()));
8063 assert_eq!(bone_ref.initial_keyframes, model0.initial_bone_keyframes);
8064 assert_eq!(bone_ref.keyframes, model0.bone_keyframes);
8065 assert_eq!(
8066 bone_ref.initial_keyframes_offset,
8067 Some(model0.sections.initial_bone_keyframes_offset)
8068 );
8069 assert_eq!(
8070 bone_ref.keyframe_count_offset,
8071 Some(model0.sections.bone_keyframe_count_offset)
8072 );
8073 assert_eq!(
8074 bone_ref.keyframes_offset,
8075 model0.sections.bone_keyframes_offset
8076 );
8077 assert_eq!(
8078 bone_ref.keyframes_end_offset,
8079 model0.sections.bone_keyframes_end_offset
8080 );
8081
8082 let cam_ref = graph
8084 .track_references
8085 .iter()
8086 .find(|r| r.scope == "global" && r.track_kind == "camera")
8087 .expect("global camera track ref");
8088 assert_eq!(cam_ref.initial_keyframes, glob.camera.initial_keyframes);
8089 assert_eq!(cam_ref.keyframes, glob.camera.keyframes);
8090 assert_eq!(cam_ref.keyframes_offset, glob.camera.keyframes_offset);
8091 assert_eq!(
8092 cam_ref.keyframes_end_offset,
8093 glob.camera.keyframes_end_offset
8094 );
8095 assert_eq!(cam_ref.initial_keyframes_offset, Some(glob.camera.offset));
8097 assert_eq!(
8098 cam_ref.keyframe_count_offset,
8099 Some(glob.camera.keyframe_count_offset)
8100 );
8101
8102 let acc_ref = graph
8104 .track_references
8105 .iter()
8106 .find(|r| r.scope == "accessory")
8107 .expect("accessory track ref");
8108 let acc0 = &glob.accessories.accessories[0];
8109 assert_eq!(acc_ref.owner_index, Some(acc0.slot_index));
8110 assert_eq!(acc_ref.document_index, Some(acc0.document_accessory_index));
8111 assert_eq!(acc_ref.owner_name.as_deref(), Some(acc0.name.as_str()));
8112 assert_eq!(acc_ref.initial_keyframes, 1);
8113 assert_eq!(acc_ref.keyframes, acc0.keyframes);
8114 assert_eq!(
8115 acc_ref.initial_keyframes_offset,
8116 Some(acc0.initial_keyframe.offset)
8117 );
8118 assert_eq!(
8119 acc_ref.keyframe_count_offset,
8120 Some(acc0.keyframe_count_offset)
8121 );
8122 assert_eq!(acc_ref.keyframes_offset, acc0.keyframes_offset);
8123 assert_eq!(acc_ref.keyframes_end_offset, acc0.keyframes_end_offset);
8124 }
8125
8126 #[test]
8127 fn pmm_project_keyframe_reference_synthetic_pmmv2_full_fixture() {
8128 let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
8129 let graph = parsed
8130 .project_graph
8131 .as_ref()
8132 .expect("project_graph must be Some for synthetic PMMv2 full fixture");
8133
8134 assert_eq!(
8139 graph.keyframe_references.len(),
8140 16,
8141 "synthetic PMMv2 full fixture must have exactly 16 keyframe references (derived from decoded summaries: 6 model + 8 global + 2 accessory)"
8142 );
8143
8144 let model_kfs: Vec<_> = graph
8145 .keyframe_references
8146 .iter()
8147 .filter(|r| r.scope == "model")
8148 .collect();
8149 let global_kfs: Vec<_> = graph
8150 .keyframe_references
8151 .iter()
8152 .filter(|r| r.scope == "global")
8153 .collect();
8154 let accessory_kfs: Vec<_> = graph
8155 .keyframe_references
8156 .iter()
8157 .filter(|r| r.scope == "accessory")
8158 .collect();
8159 assert_eq!(
8160 model_kfs.len(),
8161 6,
8162 "per document model: bone/morph/model each initial+additional"
8163 );
8164 assert_eq!(
8165 global_kfs.len(),
8166 8,
8167 "global: camera/light/gravity/selfShadow each initial+additional"
8168 );
8169 assert_eq!(
8170 accessory_kfs.len(),
8171 2,
8172 "each decoded accessory contributes initial + additional accessory keyframes"
8173 );
8174
8175 let doc = parsed.document_summary.as_ref().unwrap();
8176 let glob = parsed.document_global_summary.as_ref().unwrap();
8177
8178 let init_model_bone_kf = graph
8180 .keyframe_references
8181 .iter()
8182 .find(|r| r.scope == "model" && r.track_kind == "bone" && r.initial)
8183 .expect("initial model bone keyframe ref");
8184 let model0 = &doc.models[0];
8185 let src_init_bone = &model0.initial_bone_keyframe_summaries[0];
8186 assert_eq!(init_model_bone_kf.owner_index, Some(model0.slot_index));
8187 assert_eq!(
8188 init_model_bone_kf.document_index,
8189 Some(model0.document_model_index)
8190 );
8191 assert_eq!(
8192 init_model_bone_kf.owner_name.as_deref(),
8193 Some(model0.name.as_str())
8194 );
8195 assert!(init_model_bone_kf.initial);
8196 assert_eq!(init_model_bone_kf.keyframe_index, src_init_bone.index);
8197 assert_eq!(init_model_bone_kf.frame_index, src_init_bone.frame_index);
8198 assert_eq!(
8199 init_model_bone_kf.previous_keyframe_index,
8200 src_init_bone.previous_keyframe_index
8201 );
8202 assert_eq!(
8203 init_model_bone_kf.next_keyframe_index,
8204 src_init_bone.next_keyframe_index
8205 );
8206 assert_eq!(init_model_bone_kf.offset, src_init_bone.offset);
8207 assert_eq!(init_model_bone_kf.byte_length, src_init_bone.byte_length);
8208 assert_eq!(
8209 init_model_bone_kf.payload_offset,
8210 src_init_bone.payload_offset
8211 );
8212 assert_eq!(
8213 init_model_bone_kf.payload_byte_length,
8214 src_init_bone.payload_byte_length
8215 );
8216
8217 let add_global_cam_kf = graph
8219 .keyframe_references
8220 .iter()
8221 .find(|r| r.scope == "global" && r.track_kind == "camera" && !r.initial)
8222 .expect("additional global camera keyframe ref");
8223 let src_add_cam = match &glob.camera.keyframe_summaries[0] {
8224 PmmDocumentKeyframeSummary::Camera {
8225 index,
8226 frame_index,
8227 previous_keyframe_index,
8228 next_keyframe_index,
8229 offset,
8230 byte_length,
8231 payload_offset,
8232 payload_byte_length,
8233 ..
8234 } => (
8235 *index,
8236 *frame_index,
8237 *previous_keyframe_index,
8238 *next_keyframe_index,
8239 *offset,
8240 *byte_length,
8241 *payload_offset,
8242 *payload_byte_length,
8243 ),
8244 other => panic!("unexpected add cam summary: {other:?}"),
8245 };
8246 assert!(!add_global_cam_kf.initial);
8247 assert_eq!(add_global_cam_kf.keyframe_index, src_add_cam.0);
8248 assert_eq!(add_global_cam_kf.frame_index, src_add_cam.1);
8249 assert_eq!(add_global_cam_kf.previous_keyframe_index, src_add_cam.2);
8250 assert_eq!(add_global_cam_kf.next_keyframe_index, src_add_cam.3);
8251 assert_eq!(add_global_cam_kf.offset, src_add_cam.4);
8252 assert_eq!(add_global_cam_kf.byte_length, src_add_cam.5);
8253 assert_eq!(add_global_cam_kf.payload_offset, src_add_cam.6);
8254 assert_eq!(add_global_cam_kf.payload_byte_length, src_add_cam.7);
8255
8256 let init_acc_kf = graph
8258 .keyframe_references
8259 .iter()
8260 .find(|r| r.scope == "accessory" && r.initial)
8261 .expect("initial accessory keyframe ref");
8262 let acc0 = &glob.accessories.accessories[0];
8263 let src_init_acc = &acc0.initial_keyframe;
8264 assert_eq!(init_acc_kf.owner_index, Some(acc0.slot_index));
8265 assert_eq!(
8266 init_acc_kf.document_index,
8267 Some(acc0.document_accessory_index)
8268 );
8269 assert_eq!(init_acc_kf.owner_name.as_deref(), Some(acc0.name.as_str()));
8270 assert!(init_acc_kf.initial);
8271 assert_eq!(init_acc_kf.keyframe_index, src_init_acc.index);
8272 assert_eq!(init_acc_kf.frame_index, src_init_acc.frame_index);
8273 assert_eq!(
8274 init_acc_kf.previous_keyframe_index,
8275 src_init_acc.previous_keyframe_index
8276 );
8277 assert_eq!(
8278 init_acc_kf.next_keyframe_index,
8279 src_init_acc.next_keyframe_index
8280 );
8281 assert_eq!(init_acc_kf.offset, src_init_acc.offset);
8282 assert_eq!(init_acc_kf.byte_length, src_init_acc.byte_length);
8283 assert_eq!(init_acc_kf.payload_offset, src_init_acc.payload_offset);
8284 assert_eq!(
8285 init_acc_kf.payload_byte_length,
8286 src_init_acc.payload_byte_length
8287 );
8288 }
8289
8290 #[test]
8291 fn pmm_project_byte_coverage_synthetic_pmmv2_full_fixture() {
8292 let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
8293 let graph = parsed
8294 .project_graph
8295 .as_ref()
8296 .expect("project_graph must be Some for synthetic PMMv2 full fixture");
8297
8298 let cov = &graph.byte_coverage;
8299 let doc = parsed.document_summary.as_ref().unwrap();
8300 let glob = parsed.document_global_summary.as_ref().unwrap();
8301
8302 assert_eq!(
8304 cov.range_count, 8,
8305 "synthetic PMMv2 full fixture must have exactly 8 byte coverage ranges (1 model + 1 global root + 4 global tracks + 1 accessory block + 1 accessory)"
8306 );
8307 assert_eq!(cov.ranges.len(), 8);
8308
8309 let first_model = &doc.models[0];
8311 assert_eq!(
8312 cov.offset, first_model.offset,
8313 "coverage offset must equal first model offset"
8314 );
8315
8316 assert_eq!(
8318 cov.offset_end, glob.offset_end,
8319 "coverage offsetEnd must equal global offset_end"
8320 );
8321
8322 assert_eq!(cov.byte_length, cov.offset_end - cov.offset);
8324
8325 let cam_range = cov
8327 .ranges
8328 .iter()
8329 .find(|r| r.scope == "global" && r.kind == "camera")
8330 .expect("camera track byte range in coverage");
8331 assert_eq!(cam_range.offset, glob.camera.offset);
8332 assert_eq!(cam_range.offset_end, glob.camera.offset_end);
8333 assert_eq!(
8334 cam_range.byte_length,
8335 glob.camera.offset_end - glob.camera.offset
8336 );
8337 assert_eq!(cam_range.scope, "global");
8338 assert_eq!(cam_range.kind, "camera");
8339 assert!(cam_range.owner_index.is_none());
8340 assert!(cam_range.document_index.is_none());
8341
8342 let acc_range = cov
8344 .ranges
8345 .iter()
8346 .find(|r| r.scope == "accessory")
8347 .expect("accessory byte range in coverage");
8348 let acc0 = &glob.accessories.accessories[0];
8349 assert_eq!(acc_range.owner_index, Some(acc0.slot_index));
8350 assert_eq!(
8351 acc_range.document_index,
8352 Some(acc0.document_accessory_index)
8353 );
8354 assert_eq!(acc_range.name.as_deref(), Some(acc0.name.as_str()));
8355 assert_eq!(acc_range.offset, acc0.offset);
8356 assert_eq!(acc_range.offset_end, acc0.offset_end);
8357 assert_eq!(acc_range.byte_length, acc0.offset_end - acc0.offset);
8358 assert_eq!(acc_range.scope, "accessory");
8359 assert_eq!(acc_range.kind, "accessory");
8360
8361 assert!(
8363 cov.covered_byte_length <= cov.byte_length,
8364 "covered_byte_length must not exceed byte_length"
8365 );
8366 assert!(
8367 (0.0..=1.0).contains(&cov.coverage_ratio),
8368 "coverage_ratio must be in [0.0, 1.0], got {}",
8369 cov.coverage_ratio
8370 );
8371 assert_eq!(
8372 cov.gap_count,
8373 cov.gaps.len(),
8374 "gap_count must equal gaps.len()"
8375 );
8376 let cov_span_start = cov.offset;
8377 let cov_span_end = cov.offset_end;
8378 for (i, g) in cov.gaps.iter().enumerate() {
8379 assert_eq!(g.scope, "unknown", "gap[{}] scope must be unknown", i);
8380 assert_eq!(g.kind, "gap", "gap[{}] kind must be gap", i);
8381 assert!(
8382 g.byte_length > 0,
8383 "gap[{}] must have nonzero byte_length",
8384 i
8385 );
8386 assert!(
8387 g.offset >= cov_span_start && g.offset_end <= cov_span_end,
8388 "gap[{}] must be inside coverage span [{}, {})",
8389 i,
8390 cov_span_start,
8391 cov_span_end
8392 );
8393 assert_eq!(g.offset_end - g.offset, g.byte_length);
8394 assert!(g.owner_index.is_none());
8395 assert!(g.document_index.is_none());
8396 assert!(g.name.is_none());
8397 }
8398
8399 assert_eq!(cov.range_count, 8);
8401 assert_eq!(cov.ranges.len(), 8);
8402 let model_range = cov
8403 .ranges
8404 .iter()
8405 .find(|r| r.scope == "model")
8406 .expect("model byte range still present in ranges after adding coverage metrics");
8407 assert_eq!(model_range.scope, "model");
8408 assert_eq!(model_range.kind, "model");
8409 assert!(model_range.owner_index.is_some());
8410 }
8411
8412 #[test]
8413 fn pmm_document_model_summary_json_schema_is_stable() {
8414 let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
8415 let document = parsed.document_summary.unwrap();
8416 let keys = json_keys(&serde_json::to_value(&document.models[0]).unwrap());
8417
8418 assert_eq!(
8419 keys,
8420 vec![
8421 "assetReferenceIndex",
8422 "blendEnabled",
8423 "boneCount",
8424 "boneKeyframeSummaries",
8425 "boneKeyframes",
8426 "boneNames",
8427 "boneStateSummaries",
8428 "constraintBoneCount",
8429 "constraintStateSummaries",
8430 "documentModelIndex",
8431 "drawOrderIndex",
8432 "edgeWidth",
8433 "englishName",
8434 "initialBoneKeyframeSummaries",
8435 "initialBoneKeyframes",
8436 "initialModelKeyframe",
8437 "initialModelKeyframes",
8438 "initialMorphKeyframeSummaries",
8439 "initialMorphKeyframes",
8440 "lastFrameIndex",
8441 "modelKeyframeSummaries",
8442 "modelKeyframes",
8443 "morphCount",
8444 "morphKeyframeSummaries",
8445 "morphKeyframes",
8446 "morphNames",
8447 "morphStateSummaries",
8448 "name",
8449 "offset",
8450 "offsetEnd",
8451 "outsideParentStateSummaries",
8452 "outsideParentSubjectBoneCount",
8453 "path",
8454 "pathOffset",
8455 "sections",
8456 "selectedBoneIndex",
8457 "selectedMorphIndices",
8458 "selfShadowEnabled",
8459 "slotIndex",
8460 "transformOrderIndex",
8461 "verticalScroll",
8462 "visible"
8463 ]
8464 );
8465 }
8466
8467 #[test]
8468 fn pmm_document_model_keyframe_summary_json_schema_is_stable() {
8469 let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
8470 let document = parsed.document_summary.unwrap();
8471 let keys =
8472 json_keys(&serde_json::to_value(&document.models[0].initial_model_keyframe).unwrap());
8473
8474 assert_eq!(
8475 keys,
8476 vec![
8477 "byteLength",
8478 "constraintStateCount",
8479 "constraintStates",
8480 "constraintStatesByteLength",
8481 "constraintStatesOffset",
8482 "frameIndex",
8483 "index",
8484 "nextKeyframeIndex",
8485 "offset",
8486 "outsideParentIndexCount",
8487 "outsideParentIndices",
8488 "outsideParentIndicesByteLength",
8489 "outsideParentIndicesOffset",
8490 "payloadByteLength",
8491 "payloadBytes",
8492 "payloadOffset",
8493 "previousKeyframeIndex",
8494 "selfShadowEnabled",
8495 "selfShadowEnabledOffset",
8496 "visible",
8497 "visibleOffset"
8498 ]
8499 );
8500 }
8501
8502 #[test]
8503 fn pmm_outside_parent_index_summary_json_schema_is_stable() {
8504 let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
8505 let document = parsed.document_summary.unwrap();
8506 let kf = &document.models[0].initial_model_keyframe;
8507 let keys = json_keys(&serde_json::to_value(&kf.outside_parent_indices[0]).unwrap());
8508
8509 assert_eq!(keys, vec!["parentModelBoneIndex", "parentModelIndex"]);
8510 }
8511
8512 #[test]
8513 fn pmm_document_model_sections_json_schema_is_stable() {
8514 let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
8515 let document = parsed.document_summary.unwrap();
8516 let keys = json_keys(&serde_json::to_value(&document.models[0].sections).unwrap());
8517
8518 assert_eq!(
8519 keys,
8520 vec![
8521 "boneKeyframeCountOffset",
8522 "boneKeyframesEndOffset",
8523 "boneKeyframesOffset",
8524 "boneStatesOffset",
8525 "constraintStatesOffset",
8526 "initialBoneKeyframesOffset",
8527 "initialModelKeyframeOffset",
8528 "initialMorphKeyframesOffset",
8529 "modelKeyframeCountOffset",
8530 "modelKeyframesEndOffset",
8531 "modelKeyframesOffset",
8532 "morphKeyframeCountOffset",
8533 "morphKeyframesEndOffset",
8534 "morphKeyframesOffset",
8535 "morphStatesOffset",
8536 "outsideParentStatesOffset"
8537 ]
8538 );
8539 }
8540
8541 #[test]
8542 fn pmm_diagnostic_json_schema_is_stable() {
8543 let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
8544 let keys = json_keys(&serde_json::to_value(&parsed.diagnostics[0]).unwrap());
8545
8546 assert_eq!(keys, vec!["code", "level", "message"]);
8547 }
8548
8549 #[test]
8550 fn parses_pmm_project_settings_header_slice() {
8551 let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
8552
8553 assert_eq!(parsed.project_settings.screen_width, Some(1920));
8554 assert_eq!(parsed.project_settings.screen_height, Some(1080));
8555 assert_eq!(parsed.project_settings.timeline_frame_count, Some(424));
8556 assert_eq!(parsed.project_settings.frame_rate, Some(30.0));
8557 assert_eq!(parsed.timeline.start_frame, Some(0));
8558 assert_eq!(parsed.timeline.end_frame_exclusive, Some(424));
8559 assert_eq!(parsed.timeline.frame_count, Some(424));
8560 assert_eq!(parsed.timeline.frame_rate, Some(30.0));
8561 assert_eq!(parsed.timeline.duration_seconds, Some(424.0 / 30.0));
8562 assert_eq!(
8563 parsed.display_state.model_slot_flags,
8564 vec![1, 1, 2, 0, 0, 0, 0, 0]
8565 );
8566 assert_eq!(parsed.display_state.layout, "pmm-v2-flags");
8567 assert_eq!(parsed.display_state.declared_model_slot_count, Some(8));
8568 assert_eq!(parsed.display_state.model_slot_count, 8);
8569 assert_eq!(parsed.display_state.non_zero_model_slot_count, 3);
8570 assert_eq!(parsed.display_state.accessory_slot_count, None);
8571 assert_eq!(parsed.model_paths, vec!["UserFile/Model/miku.pmx"]);
8572 assert_eq!(parsed.motion_paths, vec!["UserFile/Motion/walk.vmd"]);
8573 assert_eq!(parsed.audio_paths, vec!["UserFile/Audio/song.wav"]);
8574 assert_eq!(parsed.model_assets.len(), 1);
8575 assert_eq!(parsed.model_assets[0].reference_index, 0);
8576 assert_eq!(parsed.model_assets[0].kind_index, 0);
8577 assert_eq!(parsed.model_assets[0].file_name, "miku.pmx");
8578 assert_eq!(parsed.model_assets[0].confidence, "high");
8579 assert!(parsed.model_assets[0].offset_end > parsed.model_assets[0].offset);
8580 assert_eq!(parsed.motion_assets[0].reference_index, 1);
8581 assert_eq!(parsed.motion_assets[0].kind_index, 0);
8582 assert_eq!(parsed.audio_assets[0].reference_index, 2);
8583 assert_eq!(parsed.audio_assets[0].extension, "wav");
8584 assert_eq!(parsed.diagnostics.len(), 4);
8585 assert_eq!(parsed.diagnostics[0].code, "PMM_PROJECT_GRAPH_PARTIAL");
8586 assert_eq!(parsed.diagnostics[1].code, "PMM_ASSET_REFERENCES_SCAN");
8587 assert!(parsed.diagnostics[1].message.contains("references=3"));
8588 assert_eq!(
8589 parsed.diagnostics[2].code,
8590 "PMM_MODEL_SLOT_COUNT_PARTIAL_DECODE"
8591 );
8592 assert_eq!(parsed.diagnostics[3].code, "PMM_ASSET_COUNT_MISMATCH");
8593 }
8594
8595 #[test]
8596 fn pmm_asset_summary_groups_manifest_derived_references() {
8597 let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
8598 let summary = &parsed.asset_summary;
8599
8600 assert_eq!(summary.reference_count, 3);
8601 assert_eq!(summary.high_confidence_count, 3);
8602 assert_eq!(summary.medium_confidence_count, 0);
8603 assert_eq!(summary.low_confidence_count, 0);
8604 assert_eq!(summary.kind_counts.get("model"), Some(&1));
8605 assert_eq!(summary.kind_counts.get("motion"), Some(&1));
8606 assert_eq!(summary.kind_counts.get("audio"), Some(&1));
8607 assert_eq!(summary.extension_counts.get("pmx"), Some(&1));
8608 assert_eq!(summary.extension_counts.get("vmd"), Some(&1));
8609 assert_eq!(summary.extension_counts.get("wav"), Some(&1));
8610 assert_eq!(summary.confidence_counts.get("high"), Some(&3));
8611 }
8612
8613 #[test]
8614 fn parses_pmm_v2_document_model_keyframe_summary() {
8615 let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
8616 let document = parsed.document_summary.as_ref().unwrap();
8617
8618 assert_eq!(document.source, "nanoem/ext/document.c PMMv2 layout");
8619 assert_eq!(document.selected_model_index, 0);
8620 assert_eq!(document.model_count, 1);
8621 assert_eq!(document.counts.models, 1);
8622 assert_eq!(document.counts.bones, 1);
8623 assert_eq!(document.counts.morphs, 1);
8624 assert_eq!(document.counts.initial_bone_keyframes, 1);
8625 assert_eq!(document.counts.bone_keyframes, 1);
8626 assert_eq!(document.counts.initial_morph_keyframes, 1);
8627 assert_eq!(document.counts.morph_keyframes, 1);
8628 assert_eq!(document.counts.initial_model_keyframes, 1);
8629 assert_eq!(document.counts.model_keyframes, 1);
8630
8631 let model = &document.models[0];
8632 assert_eq!(model.slot_index, 0);
8633 assert_eq!(model.document_model_index, 0);
8634 assert_eq!(model.name, "Miku");
8635 assert_eq!(model.english_name, "MikuEn");
8636 assert_eq!(model.path, "UserFile\\Model\\miku.pmx");
8637 assert_eq!(
8638 model.path_offset,
8639 model.offset + 1 + 1 + "Miku".len() + 1 + "MikuEn".len()
8640 );
8641 let asset_reference_index = model.asset_reference_index.unwrap();
8642 assert_eq!(
8643 parsed.asset_references[asset_reference_index].normalized_path,
8644 "UserFile/Model/miku.pmx"
8645 );
8646 assert_eq!(model.bone_count, 1);
8647 assert_eq!(model.bone_names, vec!["center".to_owned()]);
8648 assert_eq!(model.morph_count, 1);
8649 assert_eq!(model.morph_names, vec!["smile".to_owned()]);
8650 assert_eq!(model.constraint_bone_count, 1);
8651 assert_eq!(model.outside_parent_subject_bone_count, 1);
8652 assert_eq!(model.bone_keyframes, 1);
8653 assert_eq!(model.morph_keyframes, 1);
8654 assert_eq!(model.model_keyframes, 1);
8655 assert_eq!(model.initial_model_keyframe.index, None);
8656 assert_eq!(model.initial_model_keyframe.frame_index, 0);
8657 assert_eq!(model.initial_model_keyframe.previous_keyframe_index, 0);
8658 assert_eq!(model.initial_model_keyframe.next_keyframe_index, 0);
8659 assert_eq!(
8660 model.initial_model_keyframe.offset,
8661 model.sections.initial_model_keyframe_offset
8662 );
8663 assert_eq!(model.initial_model_keyframe.byte_length, 23);
8664 assert_eq!(
8665 model.initial_model_keyframe.payload_offset,
8666 model.initial_model_keyframe.offset + 12
8667 );
8668 assert_eq!(model.initial_model_keyframe.payload_byte_length, 11);
8669 assert_eq!(model.initial_model_keyframe.payload_bytes.len(), 11);
8670 assert_eq!(
8671 model.initial_model_keyframe.payload_bytes.len(),
8672 model.initial_model_keyframe.payload_byte_length
8673 );
8674 assert_model_keyframe_payload_layout(&model.initial_model_keyframe);
8675 assert_eq!(
8676 model.initial_model_keyframe.payload_bytes[0],
8677 model.initial_model_keyframe.visible as u8
8678 );
8679 assert_eq!(
8680 model.initial_model_keyframe.payload_bytes[10],
8681 model.initial_model_keyframe.self_shadow_enabled as u8
8682 );
8683 assert!(model.initial_model_keyframe.visible);
8684 assert_eq!(model.initial_model_keyframe.constraint_states, vec![true]);
8685 assert_eq!(
8686 model.initial_model_keyframe.outside_parent_indices[0].parent_model_index,
8687 2
8688 );
8689 assert_eq!(
8690 model.initial_model_keyframe.outside_parent_indices[0].parent_model_bone_index,
8691 3
8692 );
8693 assert!(model.initial_model_keyframe.self_shadow_enabled);
8694 assert_eq!(model.model_keyframe_summaries.len(), 1);
8695 assert_eq!(model.model_keyframe_summaries[0].index, Some(30));
8696 assert_eq!(model.model_keyframe_summaries[0].frame_index, 30);
8697 assert_eq!(
8698 model.model_keyframe_summaries[0].offset,
8699 model.sections.model_keyframes_offset
8700 );
8701 assert_eq!(model.model_keyframe_summaries[0].byte_length, 27);
8702 assert_eq!(
8703 model.model_keyframe_summaries[0].payload_offset,
8704 model.model_keyframe_summaries[0].offset + 16
8705 );
8706 assert_eq!(model.model_keyframe_summaries[0].payload_byte_length, 11);
8707 assert_eq!(model.model_keyframe_summaries[0].payload_bytes.len(), 11);
8708 assert_eq!(
8709 model.model_keyframe_summaries[0].payload_bytes.len(),
8710 model.model_keyframe_summaries[0].payload_byte_length
8711 );
8712 assert_model_keyframe_payload_layout(&model.model_keyframe_summaries[0]);
8713 assert_eq!(
8714 model.model_keyframe_summaries[0].payload_bytes[0],
8715 model.model_keyframe_summaries[0].visible as u8
8716 );
8717 assert_eq!(
8718 model.model_keyframe_summaries[0].payload_bytes[10],
8719 model.model_keyframe_summaries[0].self_shadow_enabled as u8
8720 );
8721 assert!(model.model_keyframe_summaries[0].visible);
8722 assert_eq!(
8723 model.model_keyframe_summaries[0].constraint_states,
8724 vec![true]
8725 );
8726 assert_eq!(
8727 model.model_keyframe_summaries[0].outside_parent_indices[0].parent_model_index,
8728 2
8729 );
8730 assert_eq!(
8731 model.model_keyframe_summaries[0].outside_parent_indices[0].parent_model_bone_index,
8732 3
8733 );
8734 assert!(model.model_keyframe_summaries[0].self_shadow_enabled);
8735 assert_eq!(model.draw_order_index, 0);
8736 assert_eq!(model.transform_order_index, 0);
8737 assert_eq!(model.selected_bone_index, 0);
8738 assert_eq!(model.selected_morph_indices, [-1, -1, -1, -1]);
8739 assert_eq!(model.vertical_scroll, 0);
8740 assert_eq!(model.last_frame_index, 30);
8741 assert!(model.visible);
8742 assert!(model.blend_enabled);
8743 assert_eq!(model.edge_width, 1.0);
8744 assert!(!model.self_shadow_enabled);
8745 assert!(model.offset_end > model.offset);
8746 assert!(model.path_offset > model.offset);
8747 assert!(model.sections.initial_bone_keyframes_offset > model.path_offset);
8748 assert!(model.sections.bone_keyframes_offset > model.sections.bone_keyframe_count_offset);
8749 assert!(model.sections.bone_keyframes_end_offset > model.sections.bone_keyframes_offset);
8750 assert!(model.sections.morph_keyframes_offset > model.sections.morph_keyframe_count_offset);
8751 assert!(model.sections.model_keyframes_offset > model.sections.model_keyframe_count_offset);
8752 assert!(
8753 model.sections.outside_parent_states_offset >= model.sections.constraint_states_offset
8754 );
8755 let document_diagnostic = parsed
8756 .diagnostics
8757 .iter()
8758 .find(|diagnostic| diagnostic.code == "PMM_DOCUMENT_SUMMARY_PARTIAL")
8759 .unwrap();
8760 assert!(
8761 document_diagnostic
8762 .message
8763 .contains("2 total bone keyframe(s) (1 additional)")
8764 );
8765 assert!(
8766 document_diagnostic
8767 .message
8768 .contains("2 total morph keyframe(s) (1 additional)")
8769 );
8770 }
8771
8772 #[test]
8773 fn pmm_document_bone_keyframe_summary_json_schema_is_stable() {
8774 let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
8775 let document = parsed.document_summary.unwrap();
8776 let model = &document.models[0];
8777 let keys =
8778 json_keys(&serde_json::to_value(&model.initial_bone_keyframe_summaries[0]).unwrap());
8779
8780 assert_eq!(
8781 keys,
8782 vec![
8783 "byteLength",
8784 "frameIndex",
8785 "index",
8786 "interpolation",
8787 "nextKeyframeIndex",
8788 "offset",
8789 "orientation",
8790 "payloadByteLength",
8791 "payloadBytes",
8792 "payloadOffset",
8793 "physicsDisabled",
8794 "previousKeyframeIndex",
8795 "selected",
8796 "translation"
8797 ]
8798 );
8799 }
8800
8801 #[test]
8802 fn builds_runtime_clip_from_pmm_document_keyframe_chains() {
8803 let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
8804 let mut model = parsed.document_summary.unwrap().models.remove(0);
8805 model.initial_bone_keyframe_summaries[0].next_keyframe_index = 1;
8806 model.bone_keyframe_summaries[0].index = Some(1);
8807 model.initial_morph_keyframe_summaries[0].next_keyframe_index = 1;
8808 model.morph_keyframe_summaries[0].index = Some(1);
8809
8810 let clip = build_pmm_document_model_clip(&model).unwrap();
8811
8812 assert_eq!(clip.bone_tracks().len(), 1);
8813 let bone_track = clip.find_bone_track(BoneIndex(0)).unwrap();
8814 assert_eq!(bone_track.keyframe_count(), 2);
8815 assert_eq!(bone_track.frame_range(), Some((0, 30)));
8816 assert_eq!(bone_track.sample(0.0).unwrap().0, Vec3A::ZERO);
8817 assert_eq!(bone_track.sample(30.0).unwrap().1, Quat::IDENTITY);
8818
8819 assert_eq!(clip.morph_tracks().len(), 1);
8820 let morph_track = clip.find_morph_track(MorphIndex(0)).unwrap();
8821 assert_eq!(morph_track.keyframe_count(), 2);
8822 assert!((morph_track.sample(0.0).unwrap() - 0.5).abs() < 1.0e-6);
8823 assert!((morph_track.sample(30.0).unwrap() - 0.5).abs() < 1.0e-6);
8824 }
8825
8826 #[test]
8827 fn pmm_document_clip_builder_ignores_unlinked_deleted_keyframes() {
8828 let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
8829 let mut model = parsed.document_summary.unwrap().models.remove(0);
8830 model.bone_keyframe_summaries[0].index = Some(1);
8831 model.bone_keyframe_summaries[0].previous_keyframe_index = 0;
8832 model.bone_keyframe_summaries[0].next_keyframe_index = 0;
8833 model.morph_keyframe_summaries[0].index = Some(1);
8834 model.morph_keyframe_summaries[0].previous_keyframe_index = 0;
8835 model.morph_keyframe_summaries[0].next_keyframe_index = 0;
8836
8837 let clip = build_pmm_document_model_clip(&model).unwrap();
8838
8839 assert_eq!(
8840 clip.find_bone_track(BoneIndex(0)).unwrap().keyframe_count(),
8841 1
8842 );
8843 assert_eq!(
8844 clip.find_morph_track(MorphIndex(0))
8845 .unwrap()
8846 .keyframe_count(),
8847 1
8848 );
8849 }
8850
8851 #[test]
8852 fn pmm_document_clip_builder_rejects_disconnected_bone_chain() {
8853 let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
8854 let mut model = parsed.document_summary.unwrap().models.remove(0);
8855 model.bone_keyframe_summaries[0].index = Some(1);
8856 model.bone_keyframe_summaries[0].previous_keyframe_index = 7;
8857 model.bone_keyframe_summaries[0].next_keyframe_index = 0;
8858 model.morph_keyframe_summaries.clear();
8859
8860 let error = build_pmm_document_model_clip(&model).unwrap_err();
8861
8862 assert!(
8863 error
8864 .to_string()
8865 .contains("1 disconnected additional bone keyframe")
8866 );
8867 }
8868
8869 #[test]
8870 fn pmm_document_clip_builder_rejects_disconnected_morph_chain() {
8871 let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
8872 let mut model = parsed.document_summary.unwrap().models.remove(0);
8873 model.bone_keyframe_summaries.clear();
8874 model.morph_keyframe_summaries[0].index = Some(1);
8875 model.morph_keyframe_summaries[0].previous_keyframe_index = 7;
8876 model.morph_keyframe_summaries[0].next_keyframe_index = 0;
8877
8878 let error = build_pmm_document_model_clip(&model).unwrap_err();
8879
8880 assert!(
8881 error
8882 .to_string()
8883 .contains("1 disconnected additional morph keyframe")
8884 );
8885 }
8886
8887 #[test]
8888 fn pmm_document_clip_builder_rejects_missing_linked_keyframe() {
8889 let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
8890 let mut model = parsed.document_summary.unwrap().models.remove(0);
8891 model.initial_bone_keyframe_summaries[0].next_keyframe_index = 999;
8892 model.bone_keyframe_summaries[0].index = Some(1);
8893
8894 let error = build_pmm_document_model_clip(&model).unwrap_err();
8895
8896 assert!(error.to_string().contains("missing index 999"));
8897 }
8898
8899 #[test]
8900 fn pmm_document_clip_builder_rejects_negative_bone_keyframe_index() {
8901 let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
8902 let mut model = parsed.document_summary.unwrap().models.remove(0);
8903 model.bone_keyframe_summaries[0].index = Some(-1);
8904
8905 let error = build_pmm_document_model_clip(&model).unwrap_err();
8906
8907 assert!(
8908 error
8909 .to_string()
8910 .contains("additional bone keyframe index -1 must be non-negative")
8911 );
8912 }
8913
8914 #[test]
8915 fn pmm_document_clip_builder_rejects_negative_morph_keyframe_index() {
8916 let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
8917 let mut model = parsed.document_summary.unwrap().models.remove(0);
8918 model.bone_keyframe_summaries.clear();
8919 model.morph_keyframe_summaries[0].index = Some(-1);
8920
8921 let error = build_pmm_document_model_clip(&model).unwrap_err();
8922
8923 assert!(
8924 error
8925 .to_string()
8926 .contains("additional morph keyframe index -1 must be non-negative")
8927 );
8928 }
8929
8930 #[test]
8931 fn pmm_document_clip_builder_rejects_wrong_bone_keyframe_backlink() {
8932 let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
8933 let mut model = parsed.document_summary.unwrap().models.remove(0);
8934 model.initial_bone_keyframe_summaries[0].next_keyframe_index = 1;
8935 model.bone_keyframe_summaries[0].index = Some(1);
8936 model.bone_keyframe_summaries[0].previous_keyframe_index = 99;
8937
8938 let error = build_pmm_document_model_clip(&model).unwrap_err();
8939
8940 assert!(
8941 error
8942 .to_string()
8943 .contains("PMM bone 0 keyframe index 1 has previous index 99; expected 0")
8944 );
8945 }
8946
8947 #[test]
8948 fn pmm_document_clip_builder_rejects_wrong_morph_keyframe_backlink() {
8949 let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
8950 let mut model = parsed.document_summary.unwrap().models.remove(0);
8951 model.bone_keyframe_summaries.clear();
8952 model.initial_morph_keyframe_summaries[0].next_keyframe_index = 1;
8953 model.morph_keyframe_summaries[0].index = Some(1);
8954 model.morph_keyframe_summaries[0].previous_keyframe_index = 99;
8955
8956 let error = build_pmm_document_model_clip(&model).unwrap_err();
8957
8958 assert!(
8959 error
8960 .to_string()
8961 .contains("PMM morph 0 keyframe index 1 has previous index 99; expected 0")
8962 );
8963 }
8964
8965 #[test]
8966 fn pmm_document_clip_builder_rejects_bone_index_in_initial_namespace() {
8967 let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
8968 let model = parsed.document_summary.unwrap().models.remove(0);
8969
8970 let error = build_pmm_document_model_clip(&model).unwrap_err();
8971
8972 assert!(error.to_string().contains(
8973 "PMM additional bone keyframe index 0 overlaps initial bone track index namespace"
8974 ));
8975 }
8976
8977 #[test]
8978 fn pmm_document_clip_builder_rejects_morph_index_in_initial_namespace() {
8979 let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
8980 let mut model = parsed.document_summary.unwrap().models.remove(0);
8981 model.bone_keyframe_summaries.clear();
8982
8983 let error = build_pmm_document_model_clip(&model).unwrap_err();
8984
8985 assert!(error.to_string().contains(
8986 "PMM additional morph keyframe index 0 overlaps initial morph track index namespace"
8987 ));
8988 }
8989
8990 #[test]
8991 fn builds_nonzero_track_two_hop_pmm_chains() {
8992 let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
8993 let mut model = parsed.document_summary.unwrap().models.remove(0);
8994
8995 model.bone_count = 2;
8996 model.initial_bone_keyframe_summaries[0].next_keyframe_index = 0;
8997 let mut bone_track_one_initial = model.initial_bone_keyframe_summaries[0].clone();
8998 bone_track_one_initial.next_keyframe_index = 2;
8999 model
9000 .initial_bone_keyframe_summaries
9001 .push(bone_track_one_initial);
9002 let mut bone_first = model.bone_keyframe_summaries[0].clone();
9003 bone_first.index = Some(2);
9004 bone_first.frame_index = 15;
9005 bone_first.previous_keyframe_index = 1;
9006 bone_first.next_keyframe_index = 3;
9007 let mut bone_second = bone_first.clone();
9008 bone_second.index = Some(3);
9009 bone_second.frame_index = 30;
9010 bone_second.previous_keyframe_index = 2;
9011 bone_second.next_keyframe_index = 0;
9012 model.bone_keyframe_summaries = vec![bone_first, bone_second];
9013
9014 model.morph_count = 2;
9015 model.initial_morph_keyframe_summaries[0].next_keyframe_index = 0;
9016 let mut morph_track_one_initial = model.initial_morph_keyframe_summaries[0].clone();
9017 morph_track_one_initial.next_keyframe_index = 2;
9018 model
9019 .initial_morph_keyframe_summaries
9020 .push(morph_track_one_initial);
9021 let mut morph_first = model.morph_keyframe_summaries[0].clone();
9022 morph_first.index = Some(2);
9023 morph_first.frame_index = 15;
9024 morph_first.previous_keyframe_index = 1;
9025 morph_first.next_keyframe_index = 3;
9026 let mut morph_second = morph_first.clone();
9027 morph_second.index = Some(3);
9028 morph_second.frame_index = 30;
9029 morph_second.previous_keyframe_index = 2;
9030 morph_second.next_keyframe_index = 0;
9031 model.morph_keyframe_summaries = vec![morph_first, morph_second];
9032
9033 let clip = build_pmm_document_model_clip(&model).unwrap();
9034
9035 let bone_track = clip.find_bone_track(BoneIndex(1)).unwrap();
9036 assert_eq!(bone_track.keyframe_count(), 3);
9037 assert_eq!(bone_track.frame_range(), Some((0, 30)));
9038 let morph_track = clip.find_morph_track(MorphIndex(1)).unwrap();
9039 assert_eq!(morph_track.keyframe_count(), 3);
9040 assert_eq!(morph_track.frame_range(), Some((0, 30)));
9041 }
9042
9043 #[test]
9044 fn pmm_document_clip_builder_rejects_indexed_initial_bone_keyframe() {
9045 let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
9046 let mut model = parsed.document_summary.unwrap().models.remove(0);
9047 model.initial_bone_keyframe_summaries[0].index = Some(0);
9048
9049 let error = build_pmm_document_model_clip(&model).unwrap_err();
9050
9051 assert!(
9052 error
9053 .to_string()
9054 .contains("PMM bone 0 initial keyframe must not have explicit index 0")
9055 );
9056 }
9057
9058 #[test]
9059 fn decodes_pmm_bone_interpolation_as_contiguous_curves() {
9060 let interpolation = [1, 2, 3, 4, 11, 12, 13, 14, 21, 22, 23, 24, 31, 32, 33, 34];
9061
9062 let (position, rotation) = decode_pmm_bone_interpolation(interpolation);
9063
9064 assert_eq!(
9065 position.x,
9066 InterpolationScalar {
9067 x1: 1,
9068 y1: 2,
9069 x2: 3,
9070 y2: 4
9071 }
9072 );
9073 assert_eq!(
9074 position.y,
9075 InterpolationScalar {
9076 x1: 11,
9077 y1: 12,
9078 x2: 13,
9079 y2: 14
9080 }
9081 );
9082 assert_eq!(
9083 position.z,
9084 InterpolationScalar {
9085 x1: 21,
9086 y1: 22,
9087 x2: 23,
9088 y2: 24
9089 }
9090 );
9091 assert_eq!(
9092 rotation,
9093 InterpolationScalar {
9094 x1: 31,
9095 y1: 32,
9096 x2: 33,
9097 y2: 34
9098 }
9099 );
9100 }
9101
9102 #[test]
9103 fn pmm_document_morph_keyframe_summary_json_schema_is_stable() {
9104 let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
9105 let document = parsed.document_summary.unwrap();
9106 let model = &document.models[0];
9107 let keys =
9108 json_keys(&serde_json::to_value(&model.initial_morph_keyframe_summaries[0]).unwrap());
9109
9110 assert_eq!(
9111 keys,
9112 vec![
9113 "byteLength",
9114 "frameIndex",
9115 "index",
9116 "nextKeyframeIndex",
9117 "offset",
9118 "payloadByteLength",
9119 "payloadBytes",
9120 "payloadOffset",
9121 "previousKeyframeIndex",
9122 "selected",
9123 "weight"
9124 ]
9125 );
9126 }
9127
9128 #[test]
9129 fn parses_pmm_v2_document_bone_morph_keyframe_summaries() {
9130 let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
9131 let document = parsed.document_summary.as_ref().unwrap();
9132 let model = &document.models[0];
9133
9134 assert_eq!(model.initial_bone_keyframe_summaries.len(), 1);
9135 let ibk = &model.initial_bone_keyframe_summaries[0];
9136 assert_eq!(ibk.index, None);
9137 assert_eq!(ibk.frame_index, 0);
9138 assert_eq!(ibk.previous_keyframe_index, 0);
9139 assert_eq!(ibk.next_keyframe_index, 0);
9140 assert_eq!(ibk.interpolation, [20u8; 16]);
9141 assert_eq!(ibk.translation, [0.0, 0.0, 0.0]);
9142 assert_eq!(ibk.orientation, [0.0, 0.0, 0.0, 1.0]);
9143 assert!(!ibk.physics_disabled);
9144 assert!(!ibk.selected);
9145 assert_eq!(ibk.byte_length, 58);
9146 assert_eq!(ibk.offset, model.sections.initial_bone_keyframes_offset);
9147 assert_eq!(ibk.payload_offset, ibk.offset + 12);
9148 assert_eq!(ibk.payload_byte_length, 46);
9149 assert_eq!(ibk.payload_bytes.len(), 46);
9150 assert_eq!(ibk.payload_bytes.len(), ibk.payload_byte_length);
9151 assert_eq!(&ibk.payload_bytes[..16], &ibk.interpolation[..]);
9152
9153 assert_eq!(model.bone_keyframe_summaries.len(), 1);
9154 let bk = &model.bone_keyframe_summaries[0];
9155 assert_eq!(bk.index, Some(0));
9156 assert_eq!(bk.frame_index, 30);
9157 assert_eq!(bk.previous_keyframe_index, 0);
9158 assert_eq!(bk.next_keyframe_index, 0);
9159 assert_eq!(bk.interpolation, [20u8; 16]);
9160 assert_eq!(bk.translation, [0.0, 0.0, 0.0]);
9161 assert_eq!(bk.orientation, [0.0, 0.0, 0.0, 1.0]);
9162 assert!(!bk.physics_disabled);
9163 assert!(!bk.selected);
9164 assert_eq!(bk.byte_length, 62);
9165 assert_eq!(bk.offset, model.sections.bone_keyframes_offset);
9166 assert_eq!(bk.payload_offset, bk.offset + 16);
9167 assert_eq!(bk.payload_byte_length, 46);
9168 assert_eq!(bk.payload_bytes.len(), 46);
9169 assert_eq!(bk.payload_bytes.len(), bk.payload_byte_length);
9170 assert_eq!(&bk.payload_bytes[..16], &bk.interpolation[..]);
9171
9172 assert_eq!(model.initial_morph_keyframe_summaries.len(), 1);
9173 let imk = &model.initial_morph_keyframe_summaries[0];
9174 assert_eq!(imk.index, None);
9175 assert_eq!(imk.frame_index, 0);
9176 assert_eq!(imk.previous_keyframe_index, 0);
9177 assert_eq!(imk.next_keyframe_index, 0);
9178 assert!((imk.weight - 0.5).abs() < 1e-6);
9179 assert!(!imk.selected);
9180 assert_eq!(imk.byte_length, 17);
9181 assert_eq!(imk.offset, model.sections.initial_morph_keyframes_offset);
9182 assert_eq!(imk.payload_offset, imk.offset + 12);
9183 assert_eq!(imk.payload_byte_length, 5);
9184 assert_eq!(imk.payload_bytes.len(), 5);
9185 assert_eq!(imk.payload_bytes.len(), imk.payload_byte_length);
9186 let imk_weight_bytes = f32::from_le_bytes([
9187 imk.payload_bytes[0],
9188 imk.payload_bytes[1],
9189 imk.payload_bytes[2],
9190 imk.payload_bytes[3],
9191 ]);
9192 assert!((imk_weight_bytes - imk.weight).abs() < 1e-6);
9193 assert_eq!(imk.payload_bytes[4], imk.selected as u8);
9194
9195 assert_eq!(model.morph_keyframe_summaries.len(), 1);
9196 let mk = &model.morph_keyframe_summaries[0];
9197 assert_eq!(mk.index, Some(0));
9198 assert_eq!(mk.frame_index, 15);
9199 assert_eq!(mk.previous_keyframe_index, 0);
9200 assert_eq!(mk.next_keyframe_index, 0);
9201 assert!((mk.weight - 0.5).abs() < 1e-6);
9202 assert!(!mk.selected);
9203 assert_eq!(mk.byte_length, 21);
9204 assert_eq!(mk.offset, model.sections.morph_keyframes_offset);
9205 assert_eq!(mk.payload_offset, mk.offset + 16);
9206 assert_eq!(mk.payload_byte_length, 5);
9207 assert_eq!(mk.payload_bytes.len(), 5);
9208 assert_eq!(mk.payload_bytes.len(), mk.payload_byte_length);
9209 let mk_weight_bytes = f32::from_le_bytes([
9210 mk.payload_bytes[0],
9211 mk.payload_bytes[1],
9212 mk.payload_bytes[2],
9213 mk.payload_bytes[3],
9214 ]);
9215 assert!((mk_weight_bytes - mk.weight).abs() < 1e-6);
9216 assert_eq!(mk.payload_bytes[4], mk.selected as u8);
9217 }
9218
9219 #[test]
9220 fn pmm_v2_document_summary_rejects_selected_model_index_out_of_range() {
9221 let mut data = pmm_with_document_summary();
9222 data[53] = 1;
9223
9224 let parsed = parse_pmm_manifest(&data).unwrap();
9225
9226 assert!(parsed.document_summary.is_none());
9227 }
9228
9229 #[test]
9230 fn parses_initial_pmm_model_slot_slice() {
9231 let parsed = parse_pmm_manifest(&pmm_with_initial_model_slot()).unwrap();
9232
9233 assert_eq!(parsed.model_slots.len(), 1);
9234 let slot = &parsed.model_slots[0];
9235 assert_eq!(slot.slot_index, 0);
9236 assert_eq!(slot.display_slot_index, Some(1));
9237 assert_eq!(slot.offset, 56);
9238 assert_eq!(slot.model_path_offset, 71);
9239 assert_eq!(slot.trailing_zero_padding_bytes, 0);
9240 assert_eq!(slot.next_non_zero_offset, Some(slot.offset_end + 1));
9241 assert_eq!(slot.name, "TestModel");
9242 assert_eq!(slot.name_bytes, b"TestModel");
9243 assert_eq!(slot.english_name, "Base");
9244 assert_eq!(slot.english_name_bytes, b"Base");
9245 assert_eq!(
9246 slot.model_path,
9247 "C:\\MMDDev\\data\\unittest\\test_1bone_cube.pmx"
9248 );
9249 assert_eq!(
9250 slot.normalized_path,
9251 "C:/MMDDev/data/unittest/test_1bone_cube.pmx"
9252 );
9253 assert_eq!(slot.asset_reference_index, Some(0));
9254 assert_eq!(slot.confidence, "high");
9255 assert_eq!(parsed.asset_references[0].offset, 56);
9256 assert_eq!(parsed.motion_paths, vec!["UserFile/Motion/walk.vmd"]);
9257 assert!(
9258 parsed
9259 .diagnostics
9260 .iter()
9261 .any(|diagnostic| diagnostic.code == "PMM_MODEL_SLOT_INITIAL_SLICE")
9262 );
9263 assert!(
9264 parsed
9265 .diagnostics
9266 .iter()
9267 .any(|diagnostic| diagnostic.code == "PMM_MODEL_SLOT_COUNT_PARTIAL_DECODE")
9268 );
9269 assert!(
9270 parsed
9271 .diagnostics
9272 .iter()
9273 .any(|diagnostic| diagnostic.code == "PMM_MODEL_SLOT_SCAN_STOPPED")
9274 );
9275 }
9276
9277 #[test]
9278 fn parses_sequential_pmm_model_slot_header_slice() {
9279 let parsed = parse_pmm_manifest(&pmm_with_two_initial_model_slots()).unwrap();
9280
9281 assert_eq!(parsed.model_slots.len(), 2);
9282 assert_eq!(parsed.model_slots[0].slot_index, 0);
9283 assert_eq!(parsed.model_slots[1].slot_index, 1);
9284 assert_eq!(parsed.model_slots[0].display_slot_index, Some(0));
9285 assert_eq!(parsed.model_slots[1].display_slot_index, Some(1));
9286 assert_eq!(parsed.model_slots[0].trailing_zero_padding_bytes, 0);
9287 assert_eq!(
9288 parsed.model_slots[0].next_non_zero_offset,
9289 Some(parsed.model_slots[1].offset)
9290 );
9291 assert_eq!(
9292 parsed.model_slots[1].offset,
9293 parsed.model_slots[0].offset_end + 1
9294 );
9295 assert_eq!(parsed.model_slots[1].name, "Sour");
9296 assert_eq!(
9297 parsed.model_slots[1].model_path,
9298 "G:\\MikuMikuDance\\MMD Models\\Sour\\sour.pmx"
9299 );
9300 assert_eq!(
9301 parsed.model_slots[1].normalized_path,
9302 "G:/MikuMikuDance/MMD Models/Sour/sour.pmx"
9303 );
9304 assert_eq!(parsed.model_slots[1].asset_reference_index, Some(1));
9305 assert_eq!(
9306 parsed.asset_references[1].offset,
9307 parsed.model_slots[1].offset
9308 );
9309 assert!(
9310 !parsed
9311 .diagnostics
9312 .iter()
9313 .any(|diagnostic| diagnostic.code == "PMM_MODEL_SLOT_SCAN_STOPPED")
9314 );
9315 }
9316
9317 #[test]
9318 fn pmm_model_slot_scan_stops_when_next_asset_reference_is_not_matched() {
9319 let data = pmm_with_two_initial_model_slots();
9320 let display_state = parse_display_state(&data, Some(2));
9321 let references = extract_asset_references(&data);
9322 let scan = parse_model_slots_from_header(&data, &references[..1], &display_state);
9323
9324 assert_eq!(scan.slots.len(), 1);
9325 let stop = scan.stop.unwrap();
9326 assert_eq!(stop.offset, scan.slots[0].offset_end + 1);
9327 assert_eq!(stop.reason, "asset_reference_not_matched");
9328 }
9329
9330 #[test]
9331 fn pmm_model_slot_scan_stops_at_implausible_next_name_length() {
9332 let mut data = pmm_with_initial_model_slot();
9333 let slot_end = parse_pmm_manifest(&data).unwrap().model_slots[0].offset_end;
9334 data.truncate(slot_end + 1);
9335 data.push(255);
9336 data.extend_from_slice(b"UserFile\\Motion\\walk.vmd\0");
9337 let parsed = parse_pmm_manifest(&data).unwrap();
9338
9339 assert_eq!(parsed.model_slots.len(), 1);
9340 let diagnostic = parsed
9341 .diagnostics
9342 .iter()
9343 .find(|diagnostic| diagnostic.code == "PMM_MODEL_SLOT_SCAN_STOPPED")
9344 .unwrap();
9345 assert!(diagnostic.message.contains("missing_name"));
9346 }
9347
9348 #[test]
9349 fn pmm_model_slot_scan_stops_at_null_byte_in_next_name() {
9350 let mut data = pmm_with_initial_model_slot();
9351 let slot_end = parse_pmm_manifest(&data).unwrap().model_slots[0].offset_end;
9352 data.truncate(slot_end + 1);
9353 data.push(4);
9354 data.extend_from_slice(b"A\0BC");
9355 data.extend_from_slice(b"UserFile\\Motion\\walk.vmd\0");
9356 let parsed = parse_pmm_manifest(&data).unwrap();
9357
9358 assert_eq!(parsed.model_slots.len(), 1);
9359 let diagnostic = parsed
9360 .diagnostics
9361 .iter()
9362 .find(|diagnostic| diagnostic.code == "PMM_MODEL_SLOT_SCAN_STOPPED")
9363 .unwrap();
9364 assert!(diagnostic.message.contains("missing_name"));
9365 }
9366
9367 #[test]
9368 fn pmm_model_slot_scan_records_zero_padding_before_binary_graph() {
9369 let mut data = pmm_with_initial_model_slot();
9370 let slot_end = parse_pmm_manifest(&data).unwrap().model_slots[0].offset_end;
9371 data.truncate(slot_end + 1);
9372 data.extend_from_slice(&[0; 12]);
9373 data.extend_from_slice(&[15, 34, 3, 0, 0, 8, 145, 128]);
9374 let parsed = parse_pmm_manifest(&data).unwrap();
9375
9376 assert_eq!(parsed.model_slots.len(), 1);
9377 assert_eq!(parsed.model_slots[0].trailing_zero_padding_bytes, 12);
9378 assert_eq!(
9379 parsed.model_slots[0].next_non_zero_offset,
9380 Some(slot_end + 13)
9381 );
9382 let diagnostic = parsed
9383 .diagnostics
9384 .iter()
9385 .find(|diagnostic| diagnostic.code == "PMM_MODEL_SLOT_SCAN_STOPPED")
9386 .unwrap();
9387 assert!(diagnostic.message.contains("missing_name"));
9388 }
9389
9390 #[test]
9391 fn parses_pmm_numeric_version() {
9392 let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
9393
9394 assert_eq!(parsed.version, "0002");
9395 assert_eq!(parsed.parsed_version, Some(2));
9396 }
9397
9398 #[test]
9399 fn rejects_invalid_pmm_magic() {
9400 let err = parse_pmm_manifest(b"not a PMM project").unwrap_err();
9401
9402 assert_eq!(err, ImportError::InvalidMagic { format: "PMM" });
9403 }
9404
9405 #[test]
9406 fn parses_pmm_v1_display_count_layout() {
9407 let parsed = parse_pmm_manifest(&pmm_v1_with_display_counts()).unwrap();
9408
9409 assert_eq!(parsed.version, "0001");
9410 assert_eq!(parsed.parsed_version, Some(1));
9411 assert_eq!(parsed.project_settings.screen_width, Some(512));
9412 assert_eq!(parsed.project_settings.screen_height, Some(288));
9413 assert_eq!(parsed.project_settings.timeline_frame_count, Some(435));
9414 assert_eq!(parsed.project_settings.frame_rate, Some(35.0));
9415 assert_eq!(parsed.display_state.layout, "pmm-v1-counts");
9416 assert!(parsed.display_state.model_slot_flags.is_empty());
9417 assert_eq!(parsed.display_state.declared_model_slot_count, Some(8));
9418 assert_eq!(parsed.display_state.model_slot_count, 8);
9419 assert_eq!(parsed.display_state.non_zero_model_slot_count, 8);
9420 assert_eq!(parsed.display_state.accessory_slot_count, Some(9));
9421 assert_eq!(parsed.header_text_entries.len(), 2);
9422 assert_eq!(parsed.header_text_entries[0].index, 0);
9423 assert_eq!(parsed.header_text_entries[0].offset, 54);
9424 assert_eq!(parsed.header_text_entries[0].text, "Dummy");
9425 assert_eq!(parsed.header_text_entries[0].text_bytes, b"Dummy");
9426 assert_eq!(parsed.header_text_entries[1].text, "maker Dummy");
9427 assert!(
9428 !parsed
9429 .diagnostics
9430 .iter()
9431 .any(|diagnostic| diagnostic.code == "PMM_MODEL_SLOT_COUNT_PARTIAL_DECODE")
9432 );
9433 assert!(parsed.model_slots.is_empty());
9434 assert!(
9435 parsed
9436 .diagnostics
9437 .iter()
9438 .any(|diagnostic| diagnostic.code == "PMM_ASSET_COUNT_MISMATCH")
9439 );
9440 assert!(
9441 !parsed
9442 .diagnostics
9443 .iter()
9444 .any(|diagnostic| diagnostic.code == "PMM_DISPLAY_STATE_UNPLAUSIBLE")
9445 );
9446 }
9447
9448 #[test]
9449 fn pmm_v1_invalid_display_counts_do_not_fall_through_to_v2_flags() {
9450 let mut data = pmm_v1_with_display_counts();
9451 data[52] = 255;
9452 data[53] = 255;
9453 data[54] = 1;
9454 data[55] = 1;
9455 let parsed = parse_pmm_manifest(&data).unwrap();
9456
9457 assert_eq!(parsed.display_state.layout, "unknown");
9458 assert!(parsed.display_state.model_slot_flags.is_empty());
9459 assert_eq!(parsed.display_state.declared_model_slot_count, None);
9460 assert_eq!(parsed.display_state.model_slot_count, 0);
9461 assert_eq!(parsed.display_state.accessory_slot_count, None);
9462 assert!(
9463 parsed
9464 .diagnostics
9465 .iter()
9466 .any(|diagnostic| diagnostic.code == "PMM_DISPLAY_STATE_UNPLAUSIBLE")
9467 );
9468 }
9469
9470 #[test]
9471 fn pmm_v1_header_text_entries_require_asset_boundary() {
9472 let mut data = b"Polygon Movie maker 0001".to_vec();
9473 data.resize(54, 0);
9474 data.extend_from_slice(b"Header\0Without asset boundary\0");
9475 let parsed = parse_pmm_manifest(&data).unwrap();
9476
9477 assert!(parsed.asset_references.is_empty());
9478 assert!(parsed.header_text_entries.is_empty());
9479 }
9480
9481 #[test]
9482 fn pmm_v1_header_text_entries_allow_empty_region_before_asset() {
9483 let mut data = b"Polygon Movie maker 0001".to_vec();
9484 data.resize(54, 0);
9485 data.extend_from_slice(b"\0\0\0UserFile\\Model\\miku.pmd\0");
9486 let parsed = parse_pmm_manifest(&data).unwrap();
9487
9488 assert_eq!(parsed.model_paths, vec!["UserFile/Model/miku.pmd"]);
9489 assert!(parsed.header_text_entries.is_empty());
9490 }
9491
9492 #[test]
9493 fn pmm_v1_header_text_entries_ignore_unterminated_chunk_before_asset() {
9494 let mut data = b"Polygon Movie maker 0001".to_vec();
9495 data.resize(54, 0);
9496 data.extend_from_slice(b"Unterminated header UserFile\\Model\\miku.pmd\0");
9497 let parsed = parse_pmm_manifest(&data).unwrap();
9498
9499 assert_eq!(parsed.model_paths, vec!["UserFile/Model/miku.pmd"]);
9500 assert!(parsed.header_text_entries.is_empty());
9501 }
9502
9503 #[test]
9504 fn parses_pmm_bare_asset_file_names() {
9505 let data = b"Polygon Movie maker 0002\0miku.pmx\0walk.vmd\0song.wav\0stage.avi\0.X\0";
9506 let parsed = parse_pmm_manifest(data).unwrap();
9507
9508 assert_eq!(parsed.model_paths, vec!["miku.pmx"]);
9509 assert_eq!(parsed.motion_paths, vec!["walk.vmd"]);
9510 assert_eq!(parsed.audio_paths, vec!["song.wav"]);
9511 assert_eq!(parsed.video_paths, vec!["stage.avi"]);
9512 assert!(parsed.accessory_paths.is_empty());
9513 assert_eq!(parsed.model_assets[0].confidence, "medium");
9514 assert_eq!(parsed.model_assets[0].file_name, "miku.pmx");
9515 assert_eq!(parsed.motion_assets[0].kind_index, 0);
9516 assert_eq!(parsed.audio_assets[0].reference_index, 2);
9517 assert_eq!(parsed.video_assets[0].reference_index, 3);
9518 assert_eq!(parsed.video_assets[0].kind_index, 0);
9519 }
9520
9521 #[test]
9522 fn parses_pmm_background_video_asset_paths() {
9523 let data =
9524 b"Polygon Movie maker 0002\0UserFile\\Background\\stage.avs\0C:\\Video\\movie.avi\0";
9525 let parsed = parse_pmm_manifest(data).unwrap();
9526
9527 assert_eq!(
9528 parsed.video_paths,
9529 vec!["UserFile/Background/stage.avs", "C:/Video/movie.avi"]
9530 );
9531 assert_eq!(parsed.video_assets.len(), 2);
9532 assert_eq!(parsed.video_assets[0].extension, "avs");
9533 assert_eq!(parsed.video_assets[0].confidence, "high");
9534 assert_eq!(parsed.video_assets[1].extension, "avi");
9535 assert_eq!(parsed.video_assets[1].file_name, "movie.avi");
9536 }
9537
9538 #[test]
9539 fn parses_pmm_common_audio_image_and_video_asset_extensions() {
9540 let data = b"Polygon Movie maker 0002\0UserFile\\Audio\\song.mp3\0UserFile\\Audio\\loop.ogg\0UserFile\\Texture\\toon.png\0UserFile\\Texture\\photo.jpg\0UserFile\\Texture\\poster.jpeg\0UserFile\\Texture\\normal.dds\0UserFile\\Movie\\clip.mp4\0UserFile\\Movie\\stage.wmv\0";
9541 let parsed = parse_pmm_manifest(data).unwrap();
9542
9543 assert_eq!(
9544 parsed.audio_paths,
9545 vec!["UserFile/Audio/song.mp3", "UserFile/Audio/loop.ogg"]
9546 );
9547 assert_eq!(
9548 parsed.image_paths,
9549 vec![
9550 "UserFile/Texture/toon.png",
9551 "UserFile/Texture/photo.jpg",
9552 "UserFile/Texture/poster.jpeg",
9553 "UserFile/Texture/normal.dds"
9554 ]
9555 );
9556 assert_eq!(
9557 parsed.video_paths,
9558 vec!["UserFile/Movie/clip.mp4", "UserFile/Movie/stage.wmv"]
9559 );
9560 assert_eq!(parsed.audio_assets[0].extension, "mp3");
9561 assert_eq!(parsed.audio_assets[1].extension, "ogg");
9562 assert_eq!(parsed.image_assets[0].extension, "png");
9563 assert_eq!(parsed.image_assets[1].extension, "jpg");
9564 assert_eq!(parsed.image_assets[2].extension, "jpeg");
9565 assert_eq!(parsed.image_assets[3].extension, "dds");
9566 assert_eq!(parsed.video_assets[0].extension, "mp4");
9567 assert_eq!(parsed.video_assets[1].extension, "wmv");
9568 }
9569
9570 #[test]
9571 fn pmm_asset_scanner_rejects_partial_common_media_extensions() {
9572 let data = b"Polygon Movie maker 0002\0song.mp3extra\0toon.png2\0poster.jpeg_backup\0movie.mp4.tmp\0UserFile\\Audio\\song.mp3\0";
9573 let parsed = parse_pmm_manifest(data).unwrap();
9574
9575 assert_eq!(parsed.audio_paths, vec!["UserFile/Audio/song.mp3"]);
9576 assert!(parsed.image_paths.is_empty());
9577 assert!(parsed.video_paths.is_empty());
9578 }
9579
9580 #[test]
9581 fn parses_pmm_asset_paths_with_spaces() {
9582 let data = b"Polygon Movie maker 0002\0Label C:\\MikuMikuDance\\MMD Models\\Append Miku.pmx\0UserFile\\Motion\\walk cycle.vmd\0";
9583 let parsed = parse_pmm_manifest(data).unwrap();
9584
9585 assert_eq!(
9586 parsed.model_paths,
9587 vec!["C:/MikuMikuDance/MMD Models/Append Miku.pmx"]
9588 );
9589 assert_eq!(parsed.motion_paths, vec!["UserFile/Motion/walk cycle.vmd"]);
9590 assert_eq!(
9591 parsed.model_assets[0].path,
9592 "C:\\MikuMikuDance\\MMD Models\\Append Miku.pmx"
9593 );
9594 assert_eq!(parsed.model_assets[0].file_name, "Append Miku.pmx");
9595 assert_eq!(parsed.motion_assets[0].file_name, "walk cycle.vmd");
9596 }
9597
9598 #[test]
9599 fn pmm_asset_reference_marks_replacement_char_as_low_confidence() {
9600 let data = "Polygon Movie maker 0002\0C:\\Models\\bad�name.pmx\0"
9601 .as_bytes()
9602 .to_vec();
9603 let parsed = parse_pmm_manifest(&data).unwrap();
9604
9605 assert_eq!(parsed.model_assets[0].confidence, "low");
9606 }
9607
9608 #[test]
9609 fn pmm_asset_reference_marks_root_relative_fragment_as_low_confidence() {
9610 let data = b"Polygon Movie maker 0002\0\\Model\\miku.pmx\0";
9611 let parsed = parse_pmm_manifest(data).unwrap();
9612
9613 assert_eq!(parsed.model_assets[0].normalized_path, "/Model/miku.pmx");
9614 assert_eq!(parsed.model_assets[0].confidence, "low");
9615 }
9616
9617 #[test]
9618 fn pmm_asset_reference_marks_missing_drive_letter_fragment_as_low_confidence() {
9619 let data =
9620 b"Polygon Movie maker 0002\0:\\Develop\\MMDDev\\data\\unittest\\test_1bone_cube.pmx\0";
9621 let parsed = parse_pmm_manifest(data).unwrap();
9622
9623 assert_eq!(
9624 parsed.model_assets[0].normalized_path,
9625 ":/Develop/MMDDev/data/unittest/test_1bone_cube.pmx"
9626 );
9627 assert_eq!(parsed.model_assets[0].confidence, "low");
9628 }
9629
9630 #[test]
9631 fn pmm_asset_scan_keeps_complete_and_fragmented_duplicate_paths_separate() {
9632 let data = b"Polygon Movie maker 0002\0C:\\MMDDev\\data\\unittest\\test_1bone_cube.pmx\0:\\MMDDev\\data\\unittest\\test_1bone_cube.pmx\0";
9633 let parsed = parse_pmm_manifest(data).unwrap();
9634
9635 assert_eq!(parsed.model_assets.len(), 2);
9636 assert_eq!(
9637 parsed.model_assets[0].normalized_path,
9638 "C:/MMDDev/data/unittest/test_1bone_cube.pmx"
9639 );
9640 assert_eq!(parsed.model_assets[0].confidence, "high");
9641 assert_eq!(
9642 parsed.model_assets[1].normalized_path,
9643 ":/MMDDev/data/unittest/test_1bone_cube.pmx"
9644 );
9645 assert_eq!(parsed.model_assets[1].confidence, "low");
9646 assert!(
9647 parsed
9648 .diagnostics
9649 .iter()
9650 .any(|diagnostic| diagnostic.code == "PMM_ASSET_REFERENCE_DUPLICATE")
9651 );
9652 }
9653
9654 #[test]
9655 fn pmm_asset_reference_keeps_offset_range() {
9656 let data = b"Polygon Movie maker 0002\0Label UserFile\\Model\\miku.pmx\0";
9657 let parsed = parse_pmm_manifest(data).unwrap();
9658
9659 let reference = &parsed.asset_references[0];
9660 assert_eq!(reference.offset, 25);
9661 assert_eq!(reference.offset_end, data.len() - 1);
9662 assert_eq!(&data[reference.offset_end..reference.offset_end + 1], b"\0");
9663 }
9664
9665 #[test]
9666 fn pmm_asset_reference_offset_end_allows_truncated_final_chunk() {
9667 let data = b"Polygon Movie maker 0002\0UserFile\\Model\\miku.pmx";
9668 let parsed = parse_pmm_manifest(data).unwrap();
9669
9670 assert_eq!(parsed.asset_references[0].offset_end, data.len());
9671 }
9672
9673 #[test]
9674 fn pmm_diagnostics_flags_duplicate_asset_by_filename() {
9675 let data = b"Polygon Movie maker 0002\0C:\\Models\\miku.pmx\0UserFile\\Model\\miku.pmx\0UserFile\\Motion\\walk.vmd\0";
9676 let parsed = parse_pmm_manifest(data).unwrap();
9677
9678 assert!(
9679 parsed
9680 .diagnostics
9681 .iter()
9682 .any(|diagnostic| diagnostic.code == "PMM_ASSET_REFERENCE_DUPLICATE")
9683 );
9684 }
9685
9686 #[test]
9687 fn pmm_diagnostics_flags_missing_motion_when_models_present() {
9688 let data = b"Polygon Movie maker 0002\0UserFile\\Model\\miku.pmx\0";
9689 let parsed = parse_pmm_manifest(data).unwrap();
9690
9691 assert!(
9692 parsed
9693 .diagnostics
9694 .iter()
9695 .any(|diagnostic| diagnostic.code == "PMM_MOTION_REFERENCES_NOT_FOUND_IN_SCAN")
9696 );
9697 }
9698
9699 #[test]
9700 fn pmm_diagnostics_flags_declared_asset_count_mismatch() {
9701 let parsed = parse_pmm_manifest(&pmm_v1_with_display_counts()).unwrap();
9702 let mismatches = parsed
9703 .diagnostics
9704 .iter()
9705 .filter(|diagnostic| diagnostic.code == "PMM_ASSET_COUNT_MISMATCH")
9706 .collect::<Vec<_>>();
9707
9708 assert_eq!(mismatches.len(), 2);
9709 assert_eq!(mismatches[0].level, "info");
9710 assert_eq!(mismatches[1].level, "info");
9711 assert!(mismatches[0].message.contains("8 model slot"));
9712 assert!(mismatches[0].message.contains("1 model reference"));
9713 assert!(mismatches[1].message.contains("9 accessory slot"));
9714 assert!(mismatches[1].message.contains("1 accessory reference"));
9715 }
9716
9717 #[test]
9718 fn pmm_display_state_derives_active_and_empty_slot_indices_from_flags() {
9719 let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
9720 let ds = &parsed.display_state;
9721
9722 assert_eq!(ds.layout, "pmm-v2-flags");
9723 assert_eq!(ds.model_slot_flags, vec![1, 1, 2, 0, 0, 0, 0, 0]);
9724 assert_eq!(ds.selected_model_index, Some(0));
9725 assert_eq!(ds.document_model_count, Some(3));
9726 let expand_flags = ds.document_expand_flags.as_ref().unwrap();
9727 assert!(expand_flags.editing_cla);
9728 assert!(expand_flags.camera_panel);
9729 assert!(expand_flags.light_panel);
9730 assert!(!expand_flags.accessory_panel);
9731 assert!(!expand_flags.self_shadow_panel);
9732 assert_eq!(ds.model_slot_flag_entries.len(), 8);
9733 assert_eq!(ds.model_slot_flag_entries[0].slot_index, 0);
9734 assert_eq!(ds.model_slot_flag_entries[0].flag, 1);
9735 assert!(ds.model_slot_flag_entries[0].active);
9736 assert_eq!(ds.model_slot_flag_entries[3].slot_index, 3);
9737 assert_eq!(ds.model_slot_flag_entries[3].flag, 0);
9738 assert!(!ds.model_slot_flag_entries[3].active);
9739 assert_eq!(ds.active_model_slot_indices, vec![0, 1, 2]);
9740 assert_eq!(ds.empty_model_slot_indices, vec![3, 4, 5, 6, 7]);
9741 }
9742
9743 #[test]
9744 fn pmm_display_state_flag_counts_are_grouped_and_stable() {
9745 let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
9746 let counts = &parsed.display_state.model_slot_flag_counts;
9747
9748 assert_eq!(counts.get(&0), Some(&5));
9749 assert_eq!(counts.get(&1), Some(&2));
9750 assert_eq!(counts.get(&2), Some(&1));
9751 assert_eq!(counts.len(), 3);
9752
9753 let json = serde_json::to_value(counts).unwrap();
9754 let keys: Vec<&str> = json
9755 .as_object()
9756 .unwrap()
9757 .keys()
9758 .map(|k| k.as_str())
9759 .collect();
9760 assert_eq!(keys, vec!["0", "1", "2"]);
9761 }
9762
9763 #[test]
9764 fn pmm_v1_display_state_derived_arrays_are_empty_when_no_raw_flags() {
9765 let parsed = parse_pmm_manifest(&pmm_v1_with_display_counts()).unwrap();
9766 let ds = &parsed.display_state;
9767
9768 assert_eq!(ds.layout, "pmm-v1-counts");
9769 assert!(ds.model_slot_flags.is_empty());
9770 assert!(ds.model_slot_flag_entries.is_empty());
9771 assert!(ds.document_expand_flags.is_none());
9772 assert_eq!(ds.selected_model_index, None);
9773 assert_eq!(ds.document_model_count, None);
9774 assert!(ds.active_model_slot_indices.is_empty());
9775 assert!(ds.empty_model_slot_indices.is_empty());
9776 assert!(ds.model_slot_flag_counts.is_empty());
9777 }
9778
9779 #[test]
9780 fn pmm_unknown_display_state_derived_arrays_are_empty() {
9781 let mut data = pmm_with_project_settings();
9782 data[46] = 7;
9783 let parsed = parse_pmm_manifest(&data).unwrap();
9784 let ds = &parsed.display_state;
9785
9786 assert_eq!(ds.layout, "unknown");
9787 assert!(ds.active_model_slot_indices.is_empty());
9788 assert!(ds.empty_model_slot_indices.is_empty());
9789 assert!(ds.model_slot_flag_counts.is_empty());
9790 assert!(ds.model_slot_flag_entries.is_empty());
9791 }
9792
9793 #[test]
9794 fn pmm_display_state_all_active_slots() {
9795 let flags: &[u8] = &[1, 1, 1, 1, 1, 1, 1, 1];
9796 let active = active_slot_indices(flags);
9797 let empty = empty_slot_indices(flags);
9798 let counts = slot_flag_counts(flags);
9799
9800 assert_eq!(active, vec![0, 1, 2, 3, 4, 5, 6, 7]);
9801 assert!(empty.is_empty());
9802 assert_eq!(counts.get(&1), Some(&8));
9803 assert_eq!(counts.len(), 1);
9804 }
9805
9806 #[test]
9807 fn pmm_display_state_all_empty_slots() {
9808 let flags: &[u8] = &[0, 0, 0, 0, 0, 0, 0, 0];
9809 let active = active_slot_indices(flags);
9810 let empty = empty_slot_indices(flags);
9811 let counts = slot_flag_counts(flags);
9812
9813 assert!(active.is_empty());
9814 assert_eq!(empty, vec![0, 1, 2, 3, 4, 5, 6, 7]);
9815 assert_eq!(counts.get(&0), Some(&8));
9816 assert_eq!(counts.len(), 1);
9817 }
9818
9819 #[test]
9820 fn pmm_document_bone_state_summary_json_schema_is_stable() {
9821 let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
9822 let document = parsed.document_summary.unwrap();
9823 let model = &document.models[0];
9824 let keys = json_keys(&serde_json::to_value(&model.bone_state_summaries[0]).unwrap());
9825
9826 assert_eq!(
9827 keys,
9828 vec![
9829 "dirty",
9830 "orientation",
9831 "physicsDisabled",
9832 "selected",
9833 "translation"
9834 ]
9835 );
9836 }
9837
9838 #[test]
9839 fn pmm_document_morph_state_summary_json_schema_is_stable() {
9840 let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
9841 let document = parsed.document_summary.unwrap();
9842 let model = &document.models[0];
9843 let keys = json_keys(&serde_json::to_value(&model.morph_state_summaries[0]).unwrap());
9844
9845 assert_eq!(keys, vec!["weight"]);
9846 }
9847
9848 #[test]
9849 fn pmm_document_constraint_state_summary_json_schema_is_stable() {
9850 let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
9851 let document = parsed.document_summary.unwrap();
9852 let model = &document.models[0];
9853 let keys = json_keys(&serde_json::to_value(&model.constraint_state_summaries[0]).unwrap());
9854
9855 assert_eq!(keys, vec!["enabled"]);
9856 }
9857
9858 #[test]
9859 fn pmm_document_outside_parent_state_summary_json_schema_is_stable() {
9860 let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
9861 let document = parsed.document_summary.unwrap();
9862 let model = &document.models[0];
9863 let keys =
9864 json_keys(&serde_json::to_value(&model.outside_parent_state_summaries[0]).unwrap());
9865
9866 assert_eq!(
9867 keys,
9868 vec![
9869 "parentModelBoneIndex",
9870 "parentModelIndex",
9871 "subjectBoneIndex",
9872 "targetModelIndex"
9873 ]
9874 );
9875 }
9876
9877 #[test]
9878 fn parses_pmm_v2_document_model_state_block_summaries() {
9879 let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
9880 let document = parsed.document_summary.as_ref().unwrap();
9881 let model = &document.models[0];
9882
9883 assert_eq!(model.bone_state_summaries.len(), 1);
9884 let bs = &model.bone_state_summaries[0];
9885 assert_eq!(bs.translation, [1.0, 2.0, 3.0]);
9886 assert_eq!(bs.orientation, [0.0, 0.0, 0.0, 1.0]);
9887 assert!(bs.dirty);
9888 assert!(!bs.physics_disabled);
9889 assert!(bs.selected);
9890
9891 assert_eq!(model.morph_state_summaries.len(), 1);
9892 let ms = &model.morph_state_summaries[0];
9893 assert!((ms.weight - 0.75).abs() < 1e-6);
9894
9895 assert_eq!(model.constraint_state_summaries.len(), 1);
9896 let cs = &model.constraint_state_summaries[0];
9897 assert!(cs.enabled);
9898
9899 assert_eq!(model.outside_parent_state_summaries.len(), 1);
9900 let ops = &model.outside_parent_state_summaries[0];
9901 assert_eq!(ops.parent_model_index, 0);
9902 assert_eq!(ops.parent_model_bone_index, 5);
9903 assert_eq!(ops.subject_bone_index, 7);
9904 assert_eq!(ops.target_model_index, 1);
9905 }
9906
9907 #[test]
9908 fn pmm_asset_scanner_requires_extension_and_userfile_boundaries() {
9909 let data = b"Polygon Movie maker 0002\0C:\\notuserfile\\Model\\fake.pmx\0dummy.x_v2\0model.x_v2.pmd\0";
9910 let parsed = parse_pmm_manifest(data).unwrap();
9911
9912 assert_eq!(
9913 parsed.model_paths,
9914 vec!["C:/notuserfile/Model/fake.pmx", "model.x_v2.pmd"]
9915 );
9916 assert!(parsed.accessory_paths.is_empty());
9917 }
9918
9919 #[test]
9920 fn ignores_implausible_pmm_model_slot_flags() {
9921 let mut data = pmm_with_project_settings();
9922 data[46] = 7;
9923 let parsed = parse_pmm_manifest(&data).unwrap();
9924
9925 assert!(parsed.display_state.model_slot_flags.is_empty());
9926 assert_eq!(parsed.display_state.layout, "unknown");
9927 assert_eq!(parsed.display_state.declared_model_slot_count, None);
9928 assert_eq!(parsed.display_state.model_slot_count, 0);
9929 assert_eq!(parsed.display_state.non_zero_model_slot_count, 0);
9930 assert_eq!(parsed.display_state.accessory_slot_count, None);
9931 assert_eq!(parsed.diagnostics.len(), 3);
9932 assert_eq!(parsed.diagnostics[2].code, "PMM_DISPLAY_STATE_UNPLAUSIBLE");
9933 }
9934
9935 #[test]
9936 fn parses_pmm_v2_accessory_keyframe_scalar_summaries() {
9937 let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
9938 let global = parsed.document_global_summary.as_ref().unwrap();
9939 let accessory = &global.accessories.accessories[0];
9940
9941 assert_eq!(accessory.keyframe_summaries.len(), 1);
9942
9943 let initial = &accessory.initial_keyframe;
9944 assert_eq!(initial.index, None);
9945 assert_eq!(initial.frame_index, 0);
9946 assert!(initial.visible);
9947 assert_eq!(initial.opacity, 1.0);
9948 assert_eq!(initial.parent_model_index, -1);
9949 assert_eq!(initial.parent_model_bone_index, -1);
9950 assert_eq!(initial.translation, [1.0, 2.0, 3.0]);
9951 assert_eq!(initial.orientation, [0.1, 0.2, 0.3]);
9952 assert_eq!(initial.scale_factor, 10.0);
9953 assert!(initial.shadow_enabled);
9954 assert!(!initial.selected);
9955 assert_eq!(initial.payload_bytes.len(), 39);
9956 assert_eq!(initial.payload_bytes.len(), initial.payload_byte_length);
9957 assert_eq!(initial.payload_bytes[0], 1);
9958 assert_eq!(
9959 &initial.payload_bytes[initial.payload_bytes.len() - 2..],
9960 &[1, 0]
9961 );
9962
9963 assert_eq!(
9966 initial.packed_opacity_visible_offset,
9967 initial.payload_offset
9968 );
9969 assert_eq!(
9970 initial.parent_model_index_offset,
9971 initial.payload_offset + 1
9972 );
9973 assert_eq!(
9974 initial.parent_model_bone_index_offset,
9975 initial.payload_offset + 5
9976 );
9977 assert_eq!(initial.translation_offset, initial.payload_offset + 9);
9978 assert_eq!(initial.translation_byte_length, 12);
9979 assert_eq!(initial.orientation_offset, initial.payload_offset + 21);
9980 assert_eq!(initial.orientation_byte_length, 12);
9981 assert_eq!(initial.scale_factor_offset, initial.payload_offset + 33);
9982 assert_eq!(initial.shadow_enabled_offset, initial.payload_offset + 37);
9983 assert_eq!(initial.selected_offset, initial.payload_offset + 38);
9984 let p0 = &initial.payload_bytes;
9985 assert_eq!(
9986 p0[initial.packed_opacity_visible_offset - initial.payload_offset],
9987 1u8
9988 );
9989 let pmi0 = initial.parent_model_index_offset - initial.payload_offset;
9990 assert_eq!(&p0[pmi0..pmi0 + 4], &(-1i32).to_le_bytes());
9991 let pbi0 = initial.parent_model_bone_index_offset - initial.payload_offset;
9992 assert_eq!(&p0[pbi0..pbi0 + 4], &(-1i32).to_le_bytes());
9993 let t0 = initial.translation_offset - initial.payload_offset;
9994 assert_eq!(&p0[t0..t0 + 4], &1.0f32.to_le_bytes());
9995 let o0 = initial.orientation_offset - initial.payload_offset;
9996 assert_eq!(&p0[o0 + 8..o0 + 12], &0.3f32.to_le_bytes());
9997 let sf0 = initial.scale_factor_offset - initial.payload_offset;
9998 assert_eq!(&p0[sf0..sf0 + 4], &10.0f32.to_le_bytes());
9999 assert_eq!(
10000 p0[initial.shadow_enabled_offset - initial.payload_offset],
10001 1
10002 );
10003 assert_eq!(p0[initial.selected_offset - initial.payload_offset], 0);
10004
10005 let kf = &accessory.keyframe_summaries[0];
10006 assert_eq!(kf.index, Some(46));
10007 assert_eq!(kf.frame_index, 46);
10008 assert!(kf.visible);
10009 assert_eq!(kf.opacity, 1.0);
10010 assert_eq!(kf.translation, [1.0, 2.0, 3.0]);
10011 assert_eq!(kf.orientation, [0.1, 0.2, 0.3]);
10012 assert_eq!(kf.scale_factor, 10.0);
10013 assert!(kf.shadow_enabled);
10014 assert!(!kf.selected);
10015 assert_eq!(kf.payload_bytes.len(), 39);
10016 assert_eq!(kf.payload_bytes.len(), kf.payload_byte_length);
10017 assert_eq!(kf.payload_bytes[0], 1);
10018 assert_eq!(&kf.payload_bytes[kf.payload_bytes.len() - 2..], &[1, 0]);
10019
10020 assert_eq!(kf.packed_opacity_visible_offset, kf.payload_offset);
10022 assert_eq!(kf.parent_model_index_offset, kf.payload_offset + 1);
10023 assert_eq!(kf.parent_model_bone_index_offset, kf.payload_offset + 5);
10024 assert_eq!(kf.translation_offset, kf.payload_offset + 9);
10025 assert_eq!(kf.translation_byte_length, 12);
10026 assert_eq!(kf.orientation_offset, kf.payload_offset + 21);
10027 assert_eq!(kf.orientation_byte_length, 12);
10028 assert_eq!(kf.scale_factor_offset, kf.payload_offset + 33);
10029 assert_eq!(kf.shadow_enabled_offset, kf.payload_offset + 37);
10030 assert_eq!(kf.selected_offset, kf.payload_offset + 38);
10031 let p1 = &kf.payload_bytes;
10032 assert_eq!(
10033 p1[kf.packed_opacity_visible_offset - kf.payload_offset],
10034 1u8
10035 );
10036 let pmi1 = kf.parent_model_index_offset - kf.payload_offset;
10037 assert_eq!(&p1[pmi1..pmi1 + 4], &(-1i32).to_le_bytes());
10038 let pbi1 = kf.parent_model_bone_index_offset - kf.payload_offset;
10039 assert_eq!(&p1[pbi1..pbi1 + 4], &(-1i32).to_le_bytes());
10040 let t1 = kf.translation_offset - kf.payload_offset;
10041 assert_eq!(&p1[t1..t1 + 4], &1.0f32.to_le_bytes());
10042 let o1 = kf.orientation_offset - kf.payload_offset;
10043 assert_eq!(&p1[o1 + 8..o1 + 12], &0.3f32.to_le_bytes());
10044 let sf1 = kf.scale_factor_offset - kf.payload_offset;
10045 assert_eq!(&p1[sf1..sf1 + 4], &10.0f32.to_le_bytes());
10046 assert_eq!(p1[kf.shadow_enabled_offset - kf.payload_offset], 1);
10047 assert_eq!(p1[kf.selected_offset - kf.payload_offset], 0);
10048 }
10049
10050 #[test]
10051 fn parses_ik_multi_bone_pmm_fixture_document_summary_and_keyframes() {
10052 let data = include_bytes!("../fixtures/pmm/ik_multi_bone_from_pmx_vmd.pmm");
10053 let parsed = parse_pmm_manifest(data).unwrap();
10054
10055 let document = parsed
10056 .document_summary
10057 .as_ref()
10058 .expect("document summary must exist for this fixture");
10059 assert_eq!(document.model_count, 1);
10060 assert_eq!(document.counts.models, 1);
10061 assert_eq!(document.counts.bones, 3);
10062 assert_eq!(document.counts.morphs, 0);
10063 assert_eq!(document.counts.initial_bone_keyframes, 3);
10064 assert_eq!(document.counts.bone_keyframes, 2);
10065 assert_eq!(document.counts.initial_morph_keyframes, 0);
10066 assert_eq!(document.counts.morph_keyframes, 0);
10067 assert_eq!(document.counts.initial_model_keyframes, 1);
10068 assert_eq!(document.counts.model_keyframes, 0);
10069
10070 assert_eq!(document.models.len(), 1);
10071 let model = &document.models[0];
10072 assert_eq!(model.bone_count, 3);
10073 assert_eq!(model.morph_count, 0);
10074 assert_eq!(model.initial_bone_keyframes, 3);
10075 assert_eq!(model.bone_keyframes, 2);
10076 assert_eq!(model.initial_morph_keyframes, 0);
10077 assert_eq!(model.morph_keyframes, 0);
10078 assert_eq!(model.initial_model_keyframes, 1);
10079 assert_eq!(model.model_keyframes, 0);
10080
10081 let model_path = &model.path;
10084 assert_eq!(
10085 model_path, "..\\pmx\\ik_multi_axis_limit.pmx",
10086 "model path must be the corrected resolvable relative path"
10087 );
10088
10089 assert_eq!(model.bone_keyframe_summaries.len(), 2);
10091
10092 let link15 = model
10093 .bone_keyframe_summaries
10094 .iter()
10095 .find(|kf| kf.frame_index == 15)
10096 .expect("link_root frame 15 must be present as additional keyframe");
10097 assert_eq!(link15.previous_keyframe_index, 0);
10098 assert_eq!(link15.next_keyframe_index, 0);
10099 assert_eq!(link15.translation, [0.05, 0.0, 0.0]);
10100 assert_eq!(link15.orientation, [0.0, 0.0, 0.0, 1.0]);
10101 assert_eq!(
10102 link15.interpolation,
10103 [
10104 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160
10105 ]
10106 );
10107 assert!(!link15.physics_disabled);
10108
10109 let eff30 = model
10110 .bone_keyframe_summaries
10111 .iter()
10112 .find(|kf| kf.frame_index == 30)
10113 .expect("effector frame 30 must be present as additional keyframe");
10114 assert_eq!(eff30.previous_keyframe_index, 1);
10115 assert_eq!(eff30.next_keyframe_index, 0);
10116 assert_eq!(eff30.translation, [0.0, 0.1, 0.0]);
10117 assert_eq!(eff30.orientation, [0.0, 0.0, 0.0, 1.0]);
10118 assert!(!eff30.physics_disabled);
10119
10120 for kf in &model.initial_bone_keyframe_summaries {
10122 assert!(!kf.physics_disabled);
10123 }
10124 for kf in &model.bone_keyframe_summaries {
10125 assert!(!kf.physics_disabled);
10126 }
10127
10128 let s = &model.sections;
10130 assert!(s.initial_bone_keyframes_offset < s.bone_keyframe_count_offset);
10131 assert!(s.bone_keyframe_count_offset < s.bone_keyframes_offset);
10132 assert!(s.bone_keyframes_offset < s.bone_keyframes_end_offset);
10133 assert!(s.initial_morph_keyframes_offset <= s.morph_keyframe_count_offset);
10134 assert!(s.morph_keyframes_offset <= s.morph_keyframes_end_offset);
10135 assert!(model.offset_end > model.offset);
10136 assert!(s.bone_keyframes_end_offset > s.bone_keyframes_offset);
10137 }
10138
10139 #[test]
10140 fn pmm_ik_multi_bone_fixture_has_expected_document_summary() {
10141 let data = include_bytes!("../fixtures/pmm/ik_multi_bone_from_pmx_vmd.pmm");
10142 let parsed = parse_pmm_manifest(data).unwrap();
10143
10144 assert_eq!(
10145 parsed.byte_length, 2125,
10146 "byte_length must match the checked-in PMM fixture"
10147 );
10148
10149 let document = parsed
10150 .document_summary
10151 .as_ref()
10152 .expect("document summary must exist for this fixture");
10153 assert_eq!(document.selected_model_index, 0);
10154
10155 let c = &document.counts;
10156 assert_eq!(c.models, 1);
10157 assert_eq!(c.bones, 3);
10158 assert_eq!(c.morphs, 0);
10159 assert_eq!(c.bone_keyframes, 2);
10160 assert_eq!(c.morph_keyframes, 0);
10161
10162 assert_eq!(document.models.len(), 1);
10163 let model = &document.models[0];
10164 assert_eq!(model.document_model_index, 0);
10165 assert_eq!(model.name, "ik_multi_axis_limit_fixture");
10166 assert_eq!(model.english_name, "ik_multi_axis_limit_fixture");
10167 assert_eq!(model.path, "..\\pmx\\ik_multi_axis_limit.pmx");
10168 assert_eq!(model.path_offset, 112);
10169 assert_eq!(model.bone_count, 3);
10170 assert_eq!(model.morph_count, 0);
10171 assert_eq!(model.last_frame_index, 30);
10172
10173 let s = &model.sections;
10174 assert_eq!(s.initial_bone_keyframes_offset, 449);
10175 assert_eq!(s.bone_keyframe_count_offset, 623);
10176 assert_eq!(s.bone_keyframes_offset, 627);
10177 assert_eq!(s.bone_keyframes_end_offset, 751);
10178
10179 assert_eq!(model.initial_bone_keyframe_summaries.len(), 3);
10180 for ibk in model.initial_bone_keyframe_summaries.iter() {
10181 assert_eq!(ibk.byte_length, 58);
10182 assert_eq!(ibk.payload_offset, ibk.offset + 12);
10183 assert_eq!(ibk.payload_byte_length, 46);
10184 assert_eq!(ibk.payload_bytes.len(), ibk.payload_byte_length);
10185 assert_eq!(&ibk.payload_bytes[..16], &ibk.interpolation[..]);
10186 }
10187 assert_eq!(model.initial_bone_keyframe_summaries[0].offset, 449);
10188 assert_eq!(model.initial_bone_keyframe_summaries[1].offset, 507);
10189 assert_eq!(model.initial_bone_keyframe_summaries[2].offset, 565);
10190
10191 let imkf = &model.initial_model_keyframe;
10192 assert_eq!(imkf.offset, 755);
10193 assert_eq!(imkf.byte_length, 14);
10194
10195 let link15 = model
10196 .bone_keyframe_summaries
10197 .iter()
10198 .find(|kf| kf.frame_index == 15)
10199 .expect("link_root frame 15 must be present");
10200 assert_eq!(link15.offset, 627);
10201 assert_eq!(link15.byte_length, 62);
10202 assert_eq!(link15.payload_offset, link15.offset + 16);
10203 assert_eq!(link15.payload_byte_length, 46);
10204 assert_eq!(link15.payload_bytes.len(), link15.payload_byte_length);
10205 assert_eq!(&link15.payload_bytes[..16], &link15.interpolation[..]);
10206 assert_eq!(link15.previous_keyframe_index, 0);
10207 assert_eq!(link15.next_keyframe_index, 0);
10208 assert_eq!(link15.translation, [0.05, 0.0, 0.0]);
10209
10210 let eff30 = model
10211 .bone_keyframe_summaries
10212 .iter()
10213 .find(|kf| kf.frame_index == 30)
10214 .expect("effector frame 30 must be present");
10215 assert_eq!(eff30.offset, 689);
10216 assert_eq!(eff30.byte_length, 62);
10217 assert_eq!(eff30.payload_offset, eff30.offset + 16);
10218 assert_eq!(eff30.payload_byte_length, 46);
10219 assert_eq!(eff30.payload_bytes.len(), eff30.payload_byte_length);
10220 assert_eq!(&eff30.payload_bytes[..16], &eff30.interpolation[..]);
10221 assert_eq!(eff30.previous_keyframe_index, 1);
10222 assert_eq!(eff30.next_keyframe_index, 0);
10223 assert_eq!(eff30.translation, [0.0, 0.1, 0.0]);
10224 }
10225
10226 #[test]
10227 fn exports_pmm_scene_from_pmx_vmd_roundtrips_ik_multi_bone_nondefault() {
10228 let pmx = include_bytes!("../fixtures/pmx/ik_multi_axis_limit.pmx");
10229 let model = crate::pmx::parse_pmx_model(pmx).unwrap();
10230
10231 let vmd = include_bytes!("../fixtures/vmd/ik_multi_bone_nondefault.vmd");
10232 let motion = crate::vmd::parse_vmd_animation(vmd).unwrap();
10233
10234 let report = export_pmm_scene_from_pmx_vmd(
10235 &model,
10236 &motion,
10237 "..\\pmx\\ik_multi_axis_limit.pmx",
10239 &PmmSceneExportOptions::default(),
10240 );
10241
10242 let reparsed = parse_pmm_manifest(&report.bytes).unwrap();
10243 let document = reparsed
10244 .document_summary
10245 .as_ref()
10246 .expect("exported PMM must contain document summary");
10247 assert_eq!(document.model_count, 1);
10248 assert_eq!(document.counts.models, 1);
10249 assert_eq!(document.counts.bones, 3);
10250 assert_eq!(document.counts.morphs, 0);
10251
10252 let m = &document.models[0];
10253 assert_eq!(m.bone_count, 3);
10254 assert_eq!(m.morph_count, 0);
10255 assert_eq!(m.path, "..\\pmx\\ik_multi_axis_limit.pmx");
10256
10257 let link15 = m
10259 .bone_keyframe_summaries
10260 .iter()
10261 .find(|kf| kf.frame_index == 15)
10262 .expect("exported PMM must contain link_root frame 15");
10263 assert_eq!(link15.translation, [0.05, 0.0, 0.0]);
10264 assert_eq!(link15.orientation, [0.0, 0.0, 0.0, 1.0]);
10265 assert_eq!(
10266 link15.interpolation,
10267 [
10268 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160
10269 ]
10270 );
10271 assert!(!link15.physics_disabled);
10272
10273 let eff30 = m
10274 .bone_keyframe_summaries
10275 .iter()
10276 .find(|kf| kf.frame_index == 30)
10277 .expect("exported PMM must contain effector frame 30");
10278 assert_eq!(eff30.translation, [0.0, 0.1, 0.0]);
10279 assert_eq!(eff30.orientation, [0.0, 0.0, 0.0, 1.0]);
10280 assert!(!eff30.physics_disabled);
10281
10282 assert_eq!(m.initial_bone_keyframes, 3);
10284 assert_eq!(m.bone_keyframes, 2);
10285
10286 assert_eq!(m.constraint_bone_count, 1);
10287 assert_eq!(m.outside_parent_subject_bone_count, 2);
10288 assert_eq!(m.initial_model_keyframe.constraint_states, vec![true]);
10289 for outside_parent in &m.initial_model_keyframe.outside_parent_indices {
10290 assert_eq!(outside_parent.parent_model_index, -1);
10291 assert_eq!(outside_parent.parent_model_bone_index, 0);
10292 }
10293 assert_eq!(m.constraint_state_summaries.len(), 1);
10294 assert!(m.constraint_state_summaries[0].enabled);
10295 assert_eq!(m.outside_parent_state_summaries.len(), 2);
10296 assert_eq!(m.outside_parent_state_summaries[0].parent_model_index, 0);
10297 assert_eq!(
10298 m.outside_parent_state_summaries[0].parent_model_bone_index,
10299 0
10300 );
10301 assert_eq!(m.outside_parent_state_summaries[0].subject_bone_index, -1);
10302 assert_eq!(m.outside_parent_state_summaries[0].target_model_index, 0);
10303
10304 let fixed_tracks_offset = m.path_offset + 256;
10305 assert_eq!(reparsed.source_bytes[fixed_tracks_offset], 2);
10306 }
10307
10308 #[test]
10309 fn pmm_export_model_structure_matches_pmx_bone_flags_and_display_frames() {
10310 let pmx = include_bytes!("../fixtures/pmx/ik_multi_axis_limit.pmx");
10311 let model = crate::pmx::parse_pmx_model(pmx).unwrap();
10312 let structure = pmm_export_model_structure_from_pmx(&model);
10313
10314 assert_eq!(structure.fixed_tracks, 2);
10315 assert_eq!(structure.constraint_bone_indices, vec![2]);
10316 assert_eq!(structure.outside_parent_subject_bone_indices, vec![-1, 2]);
10317 assert!(pmm_is_outside_parent_subject_bone(&model.skeleton.bones[2]));
10318 assert!(!pmm_is_outside_parent_subject_bone(
10319 &model.skeleton.bones[0]
10320 ));
10321 assert!(!pmm_is_outside_parent_subject_bone(
10322 &model.skeleton.bones[1]
10323 ));
10324 }
10325}