//! Omitted Imports #[derive(GodotClass)] #[class(base = EditorImportPlugin, tool, init)] pub struct AnimationImporter { base: Base<EditorImportPlugin>, } #[derive(GodotConvert, Export, Var, Default)] #[godot(via = GString)] pub enum AnimationNameStyle { #[default] SnakeCase, CamelCase, } #[derive(Clone, Deserialize)] pub struct AnimationData<'import> { pub asset_path: &'import str, pub frame_size: i32, pub default_frame_time: f32, pub frame_time_override: HashMap<&'import str, f32>, pub should_loop: HashSet<&'import str>, } #[godot_api] impl IEditorImportPlugin for AnimationImporter { fn get_importer_name(&self) -> GString { GString::from("cresthollow.imports.animation") } fn get_visible_name(&self) -> GString { GString::from("JSON Animation Library") } fn get_preset_count(&self) -> i32 { 1 } fn get_preset_name(&self, _preset_index: i32) -> GString { GString::from("Default") } fn get_recognized_extensions(&self) -> PackedStringArray { PackedStringArray::from(&[GString::from("animdata")]) } fn get_import_options(&self, _path: GString, _preset_index: i32) -> Array<Dictionary> { array![ &dict! { "name": "Outputs", "default_value": "", "usage": PropertyUsageFlags::GROUP, }, &dict! { "name": "animation_name_style", "default_value": AnimationNameStyle::default(), }, ] } fn get_save_extension(&self) -> GString { GString::from("res") } fn get_resource_type(&self) -> GString { GString::from("AnimationLibrary") } fn get_priority(&self) -> f32 { 2.0 } fn get_import_order(&self) -> i32 { ImportOrder::SCENE.ord() } fn get_option_visibility(&self, _path: GString, _option_name: StringName, _options: Dictionary) -> bool { true } fn import( &self, source_file: GString, save_path: GString, _options: Dictionary, _platform_variants: Array<GString>, mut gen_files: Array<GString>, ) -> Error { let output_path = GString::from(format!("{}.{}", save_path, self.get_save_extension())); let atlas_path = resolve_atlas_path(&PathBuf::from(source_file.to_string())); let resource = match FileAccess::open(&source_file, ModeFlags::READ) { Some(file) => file, None => { godot_error!("[Anim] Failed to open file: {}", source_file); return Error::ERR_FILE_CANT_OPEN; } }; check_error!("[Anim] File Error: {:?}", resource.get_error()); let buffer = resource.read_to_buffer(); let animation_data: AnimationData = unwrap_error!( "[Anim] Invalid animation data found: {:?}", serde_json::from_slice(buffer.as_slice()).comp() ); let mut animation_library = resolve_animation_library(&output_path); let sheet_folder = resolve_folder_path(&PathBuf::from(source_file.to_string()), animation_data.asset_path); let found_paths = resolve_sheet_paths(&sheet_folder); let atlas_stitcher = AtlasBatch::new(found_paths.as_slice()); let atlas_spec: GeneratedAtlasSpec = unwrap_error!( "[Anim] Failed to create atlas layout: {:?}", atlas_stitcher.allocate_space() ); let generated_atlas: DynamicImage = unwrap_error!( "[Anim] Failed to generate image atlas: {:?}", AtlasBatch::generate_atlas(&atlas_spec) ); let system_atlas_path = ProjectSettings::singleton().globalize_path(&atlas_path.display().godot()); let mut writer = match std::fs::File::create(system_atlas_path.to_string()) { Ok(file) => file, Err(err) => { godot_error!("[Anim] Failed to create output texture file: {}", err); return Error::ERR_FILE_CANT_OPEN; } }; unwrap_error!( "[Anim] Failed to write generated atlas: {:?}", generated_atlas.write_to(&mut writer, ImageFormat::Png).comp() ); gen_files.push(&atlas_path.display().godot()); godot_print!( "[Anim] Created texture atlas with tile dimensions: {}x{}", atlas_spec.width / animation_data.frame_size, atlas_spec.height / animation_data.frame_size ); build_animation_library(&mut animation_library, &animation_data, &atlas_spec); ResourceSaver::singleton() .save_ex(&animation_library) .path(&output_path) .done() } } fn resolve_sheet_paths(base_folder: &Path) -> Vec<String> { let mut dir = match DirAccess::open(&base_folder.display().godot()) { Some(value) => value, None => return Vec::new(), }; dir.set_include_hidden(false); dir.set_include_navigational(false); let file_list = dir.get_files(); file_list .as_slice() .iter() .filter_map(|filename| { if !filename.begins_with("_") && filename.ends_with(".png") { let file_path = base_folder.join(filename.to_string()); Some( ProjectSettings::singleton() .globalize_path(&file_path.display().godot()) .to_string(), ) } else { None } }) .collect() } fn resolve_animation_library(resource_path: &GString) -> Gd<AnimationLibrary> { if FileAccess::file_exists(resource_path) { match try_load(resource_path) { Ok(library) => { godot_print!("[Anim] Found animation library: {}", resource_path); library } Err(e) => { godot_error!("[Anim] Invalid animation library: {}; {}", resource_path, e); AnimationLibrary::new_gd() } } } else { AnimationLibrary::new_gd() } } fn resolve_folder_path(base: &Path, relative_path: &str) -> PathBuf { let mut base = base.to_path_buf(); base.pop(); base.join(relative_path) } fn resolve_atlas_path(base: &Path) -> PathBuf { let filename = match base.file_stem().and_then(|stem| stem.to_str()) { Some(stem) => format!("{}_atlas.png", stem), None => String::from("animation_atlas.png"), }; let mut base = base.to_path_buf(); base.pop(); base.push(filename); base }