diff --git a/crates/animation/src/lib.rs b/crates/animation/src/lib.rs index 522e8b05a2..b5ee83aef5 100644 --- a/crates/animation/src/lib.rs +++ b/crates/animation/src/lib.rs @@ -1,14 +1,17 @@ use std::{ - collections::HashMap, sync::Arc, time::{Duration, SystemTime} + collections::HashMap, + sync::Arc, + time::{Duration, SystemTime}, }; use convert_case::{Case, Casing}; use derive_more::Display; use kiwi_core::{asset_cache, hierarchy::children, time}; use kiwi_ecs::{components, query, Debuggable, EntityId, MakeDefault, Networked, Store, SystemGroup}; -use kiwi_model::{animation_binder, model, model_def, ModelDef}; +use kiwi_model::{animation_binder, model, model_from_url, ModelFromUrl}; use kiwi_std::{ - asset_cache::{AssetCache, AsyncAssetKeyExt}, asset_url::{AnimationAssetType, ModelAssetType, TypedAssetUrl} + asset_cache::{AssetCache, AsyncAssetKeyExt}, + asset_url::{AnimationAssetType, ModelAssetType, TypedAssetUrl}, }; use kiwi_ui::Editable; use serde::{Deserialize, Serialize}; @@ -28,7 +31,7 @@ components!("animation", { /// the animations base pose, so we apply the pose from the animations model to make sure they /// correspond @[Debuggable, Networked, Store] - animation_apply_base_pose: ModelDef, + animation_apply_base_pose: ModelFromUrl, @[Debuggable, Networked, Store] copy_animation_controller_to_children: (), @[Debuggable, Networked, Store] @@ -176,7 +179,9 @@ pub fn animation_systems() -> SystemGroup { if ctrlr.apply_base_pose { if let Some(action) = ctrlr.actions.get(0) { if let AnimationClipRef::FromModelAsset(def) = &action.clip { - world.add_component(id, animation_apply_base_pose(), ModelDef(def.model_crate().unwrap().model())).unwrap(); + world + .add_component(id, animation_apply_base_pose(), ModelFromUrl(def.model_crate().unwrap().model())) + .unwrap(); } } } @@ -203,7 +208,7 @@ pub fn animation_systems() -> SystemGroup { let mut in_error = Vec::new(); for (id, (controller, binder)) in q.iter(world, qs) { let retaget = world.get(id, animation_retargeting()).unwrap_or(AnimationRetargeting::None); - let model = world.get_ref(id, model_def()).map(|def| def.0.clone()).ok(); + let model = world.get_ref(id, model_from_url()).ok().and_then(|def| Some(TypedAssetUrl::parse(def).ok()?)); // Calc for action in controller.actions.iter() { match action.clip.get_clip(assets.clone(), retaget, model.clone()) { diff --git a/crates/animation/src/retargeting.rs b/crates/animation/src/retargeting.rs index 5ddf0c655b..fca5f55595 100644 --- a/crates/animation/src/retargeting.rs +++ b/crates/animation/src/retargeting.rs @@ -4,9 +4,11 @@ use anyhow::Context; use async_trait::async_trait; use kiwi_core::transform::{rotation, translation}; use kiwi_editor_derive::ElementEditor; -use kiwi_model::{Model, ModelDef}; +use kiwi_model::{Model, ModelFromUrl}; use kiwi_std::{ - asset_cache::{AssetCache, AssetKeepalive, AsyncAssetKey, AsyncAssetKeyExt, SyncAssetKeyExt}, asset_url::{AnimationAssetType, ModelAssetType, ServerBaseUrlKey, TypedAssetUrl}, download_asset::AssetError + asset_cache::{AssetCache, AssetKeepalive, AsyncAssetKey, AsyncAssetKeyExt, SyncAssetKeyExt}, + asset_url::{AnimationAssetType, ModelAssetType, ServerBaseUrlKey, TypedAssetUrl}, + download_asset::AssetError, }; use serde::{Deserialize, Serialize}; @@ -50,7 +52,7 @@ impl AsyncAssetKey, AssetError>> for AnimationClipReta let base_url = ServerBaseUrlKey.get(&assets); let clip_url: TypedAssetUrl = self.clip.resolve(&base_url).context("Failed to resolve clip url")?.into(); let anim_model = - ModelDef(clip_url.model_crate().context("Invalid clip url")?.model()).get(&assets).await.context("Failed to load model")?; + ModelFromUrl(clip_url.model_crate().context("Invalid clip url")?.model()).get(&assets).await.context("Failed to load model")?; let clip = AnimationClipFromUrl::new(clip_url.unwrap_abs(), true).get(&assets).await.context("No such clip")?; match self.translation_retargeting { AnimationRetargeting::None => Ok(clip), @@ -65,7 +67,7 @@ impl AsyncAssetKey, AssetError>> for AnimationClipReta .context("No retarget_model specified")? .resolve(&base_url) .context("Failed to resolve retarget url")?; - let retarget_model = ModelDef(retarget_model_url.into()).get(&assets).await.context("Failed to load retarget model")?; + let retarget_model = ModelFromUrl(retarget_model_url.into()).get(&assets).await.context("Failed to load retarget model")?; let mut clip = (*clip).clone(); let anim_root = anim_model.roots()[0]; let _retarget_root = retarget_model.roots()[0]; diff --git a/crates/model/src/lib.rs b/crates/model/src/lib.rs index 602b406cc2..29f3a370aa 100644 --- a/crates/model/src/lib.rs +++ b/crates/model/src/lib.rs @@ -5,19 +5,31 @@ use futures::StreamExt; use glam::{vec4, Vec3}; use itertools::Itertools; use kiwi_core::{ - asset_cache, async_ecs::{async_run, AsyncRun}, bounding::{local_bounding_aabb, world_bounding_aabb, world_bounding_sphere}, hierarchy::{children, despawn_recursive}, main_scene, runtime, transform::{get_world_position, inv_local_to_world, local_to_world, mesh_to_world} + asset_cache, + async_ecs::{async_run, AsyncRun}, + bounding::{local_bounding_aabb, world_bounding_aabb, world_bounding_sphere}, + hierarchy::{children, despawn_recursive}, + main_scene, runtime, + transform::{get_world_position, inv_local_to_world, local_to_world, mesh_to_world}, }; use kiwi_ecs::{ - components, query, ComponentDesc, Debuggable, Description, EntityData, EntityId, Name, Networked, Store, SystemGroup, World + components, query, ComponentDesc, Debuggable, Description, EntityData, EntityId, Name, Networked, Store, SystemGroup, World, }; use kiwi_gpu::mesh_buffer::GpuMeshFromUrl; use kiwi_renderer::{ - color, gpu_primitives, materials::{ - flat_material::{get_flat_shader, FlatMaterialKey}, pbr_material::{get_pbr_shader, PbrMaterialFromUrl} - }, primitives, RenderPrimitive, StandardShaderKey + color, gpu_primitives, + materials::{ + flat_material::{get_flat_shader, FlatMaterialKey}, + pbr_material::{get_pbr_shader, PbrMaterialFromUrl}, + }, + primitives, RenderPrimitive, StandardShaderKey, }; use kiwi_std::{ - asset_cache::{AssetCache, AsyncAssetKey, AsyncAssetKeyExt, SyncAssetKey, SyncAssetKeyExt}, asset_url::{AbsAssetUrl, AssetUrl, ModelAssetType, ServerBaseUrlKey, TypedAssetUrl}, download_asset::{AssetError, BytesFromUrl, JsonFromUrl}, log_result, math::Line + asset_cache::{AssetCache, AsyncAssetKey, AsyncAssetKeyExt, SyncAssetKey, SyncAssetKeyExt}, + asset_url::{AbsAssetUrl, AssetUrl, ModelAssetType, ServerBaseUrlKey, TypedAssetUrl}, + download_asset::{AssetError, BytesFromUrl, JsonFromUrl}, + log_result, + math::Line, }; use serde::{Deserialize, Serialize}; mod model; @@ -27,6 +39,7 @@ pub use model::*; use tokio::sync::Semaphore; use self::loading_material::{LoadingMaterialKey, LoadingShaderKey}; +use anyhow::Context; pub mod loading_material; @@ -37,8 +50,8 @@ components!("model", { animation_bind_id: String, model: Arc, - @[Debuggable, Networked, Store] - model_def: ModelDef, + @[Debuggable, Networked, Store, Name["Model from url"], Description["Load a model from the given url or relative path"]] + model_from_url: String, @[Networked, Store] pbr_renderer_primitives_from_url: Vec, @@ -59,7 +72,7 @@ components!("model", { async fn internal_spawn_models_from_defs( assets: &AssetCache, async_run: AsyncRun, - entities_with_models: HashMap)>, + entities_with_models: HashMap>, ) -> anyhow::Result<()> { // Meanwhile, spawn a spinning cube onto the entity. let cube = CubeMeshKey.get(assets); @@ -84,7 +97,7 @@ async fn internal_spawn_models_from_defs( .set_default(local_to_world()) .set_default(inv_local_to_world()); - let mut ids = entities_with_models.values().flat_map(|v| &v.1).copied().collect_vec(); + let mut ids = entities_with_models.values().flatten().copied().collect_vec(); let cube_fail = Arc::new(cube.clone().set(color(), vec4(1.0, 0.0, 0.0, 1.0))); @@ -97,9 +110,13 @@ async fn internal_spawn_models_from_defs( } }); - let iter = entities_with_models.into_values().map(|(k, ids)| async move { + let iter = entities_with_models.into_iter().map(|(k, ids)| async move { tracing::debug!("Loading model: {k:#?}"); - match k.get(assets).await { + let url = match TypedAssetUrl::parse(k).context("Failed to parse url") { + Ok(url) => url, + Err(e) => return (ids, Err(e)), + }; + match ModelFromUrl(url).get(assets).await.context("Failed to load model") { Ok(v) => (ids, Ok(v)), Err(e) => (ids, Err(e)), } @@ -149,7 +166,7 @@ pub fn model_systems() -> SystemGroup { SystemGroup::new( "model_systems", vec![ - query((children(),)).incl(model_def()).despawned().to_system(|q, world, qs, _| { + query((children(),)).incl(model_from_url()).despawned().to_system(|q, world, qs, _| { for (_, (children,)) in q.collect_cloned(world, qs) { for c in children { if world.has_component(c, is_model_node()) { @@ -158,16 +175,16 @@ pub fn model_systems() -> SystemGroup { } } }), - query(()).incl(model_def()).despawned().to_system(|q, world, qs, _| { + query(()).incl(model_from_url()).despawned().to_system(|q, world, qs, _| { for (id, _) in q.collect_cloned(world, qs) { remove_model(world, id); } }), - query((model_def().changed(),)).to_system(|q, world, qs, _| { - let mut new_models = HashMap::new(); + query((model_from_url().changed(),)).to_system(|q, world, qs, _| { + let mut new_models = HashMap::>::new(); for (id, (model_from_url,)) in q.iter(world, qs) { - let entry = new_models.entry(format!("{model_from_url:?}")).or_insert_with(|| (model_from_url.clone(), Vec::new())); - entry.1.push(id); + let entry = new_models.entry(model_from_url.clone()).or_default(); + entry.push(id); } if new_models.is_empty() { return; @@ -207,14 +224,14 @@ fn remove_model(world: &mut World, entity: EntityId) { } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ModelDef(pub TypedAssetUrl); -impl ModelDef { +pub struct ModelFromUrl(pub TypedAssetUrl); +impl ModelFromUrl { pub fn new(url: impl AsRef) -> anyhow::Result { Ok(Self(TypedAssetUrl::parse(url)?)) } } #[async_trait] -impl AsyncAssetKey, AssetError>> for ModelDef { +impl AsyncAssetKey, AssetError>> for ModelFromUrl { async fn load(self, assets: AssetCache) -> Result, AssetError> { let base_url = ServerBaseUrlKey.get(&assets); let url = self.0.clone().resolve(&base_url).unwrap(); diff --git a/crates/model_import/examples/model_loading.rs b/crates/model_import/examples/model_loading.rs index 7f9ee04f54..b6aea5a2fd 100644 --- a/crates/model_import/examples/model_loading.rs +++ b/crates/model_import/examples/model_loading.rs @@ -1,16 +1,21 @@ use glam::*; use kiwi_app::AppBuilder; use kiwi_core::{ - asset_cache, camera::{active_camera, far}, main_scene, transform::* + asset_cache, + camera::{active_camera, far}, + main_scene, + transform::*, }; use kiwi_ecs::{EntityData, World}; use kiwi_element::ElementComponentExt; -use kiwi_model::{model_def, ModelDef}; +use kiwi_model::{model_from_url, ModelFromUrl}; use kiwi_model_import::{MaterialFilter, ModelImportPipeline, ModelImportTransform, ModelTransform}; use kiwi_primitives::{Cube, Quad}; use kiwi_renderer::{color, materials::pbr_material::PbrMaterialFromUrl}; use kiwi_std::{ - asset_cache::AsyncAssetKeyExt, asset_url::{AbsAssetUrl, AssetUrl, TypedAssetUrl}, math::SphericalCoords + asset_cache::AsyncAssetKeyExt, + asset_url::{AbsAssetUrl, AssetUrl, TypedAssetUrl}, + math::SphericalCoords, }; use reqwest::Url; @@ -88,7 +93,7 @@ async fn init(world: &mut World) { let mut model_defs = Vec::new(); for pipeline in asset_pipelines.iter() { let model_url = pipeline.produce_local_model_url(&assets).await.unwrap(); - model_defs.push(ModelDef(TypedAssetUrl::new(Url::from_file_path(model_url).unwrap()))); + model_defs.push(ModelFromUrl(TypedAssetUrl::new(Url::from_file_path(model_url).unwrap()))); } // "Regular" spawning @@ -104,7 +109,7 @@ async fn init(world: &mut World) { for (i, mod_def) in model_defs.iter().enumerate() { let xy = vec2(i as f32 * 3., 3.); Cube.el().set(translation(), xy.extend(-0.9)).set(color(), vec4(0.3, 0.3, 0.3, 1.)).spawn_static(world); - EntityData::new().set(model_def(), mod_def.clone()).set(translation(), xy.extend(0.1)).spawn(world); + EntityData::new().set(model_from_url(), mod_def.0.to_string()).set(translation(), xy.extend(0.1)).spawn(world); } kiwi_cameras::spherical::new(vec3(0., 0., 0.), SphericalCoords::new(std::f32::consts::PI / 4., std::f32::consts::PI / 4., 5.)) diff --git a/crates/model_import/src/model_crate.rs b/crates/model_import/src/model_crate.rs index b9e62d8b81..1bd21ca1aa 100644 --- a/crates/model_import/src/model_crate.rs +++ b/crates/model_import/src/model_crate.rs @@ -7,20 +7,32 @@ use image::{ImageOutputFormat, RgbaImage}; use itertools::Itertools; use kiwi_animation::{animation_bind_id_from_name, AnimationClip}; use kiwi_core::{ - bounding::local_bounding_aabb, hierarchy::children, name, transform::{local_to_parent, local_to_world, mesh_to_local, TransformSystem} + bounding::local_bounding_aabb, + hierarchy::children, + name, + transform::{local_to_parent, local_to_world, mesh_to_local, TransformSystem}, }; use kiwi_ecs::{query, query_mut, Component, ComponentValue, EntityData, EntityId, FrameEvent, System, World}; use kiwi_model::{ - animation_bind_id, model_def, model_skin_ix, model_skins, pbr_renderer_primitives_from_url, Model, ModelDef, PbrRenderPrimitiveFromUrl + animation_bind_id, model_from_url, model_skin_ix, model_skins, pbr_renderer_primitives_from_url, Model, ModelFromUrl, + PbrRenderPrimitiveFromUrl, }; use kiwi_physics::{ - collider::{character_controller_height, character_controller_radius, collider, ColliderDef, ColliderFromUrls}, mesh::PhysxGeometryFromUrl, physx::PhysicsKey + collider::{character_controller_height, character_controller_radius, collider, ColliderDef, ColliderFromUrls}, + mesh::PhysxGeometryFromUrl, + physx::PhysicsKey, }; use kiwi_renderer::{ - double_sided, lod::{gpu_lod, lod_cutoffs}, materials::pbr_material::PbrMaterialFromUrl + double_sided, + lod::{gpu_lod, lod_cutoffs}, + materials::pbr_material::PbrMaterialFromUrl, }; use kiwi_std::{ - asset_cache::{AssetCache, SyncAssetKeyExt}, asset_url::AbsAssetUrl, download_asset::AssetsCacheDir, mesh::Mesh, shapes::AABB + asset_cache::{AssetCache, SyncAssetKeyExt}, + asset_url::AbsAssetUrl, + download_asset::AssetsCacheDir, + mesh::Mesh, + shapes::AABB, }; use ordered_float::Float; use physxx::{PxConvexFlag, PxConvexMeshDesc, PxDefaultMemoryOutputStream, PxMeshFlag, PxTriangleMeshDesc}; @@ -432,7 +444,7 @@ impl ModelCrate { } pub fn create_object_from_model(&mut self) { - self.create_object(EntityData::new().set(model_def(), ModelDef(dotdot_path(self.models.loc.path(ModelCrate::MAIN)).into()))) + self.create_object(EntityData::new().set(model_from_url(), dotdot_path(self.models.loc.path(ModelCrate::MAIN)).into())) } pub fn create_object(&mut self, data: EntityData) { diff --git a/crates/naturals/src/lib.rs b/crates/naturals/src/lib.rs index 42df2adb3e..6d8a1d53fa 100644 --- a/crates/naturals/src/lib.rs +++ b/crates/naturals/src/lib.rs @@ -12,7 +12,7 @@ use kiwi_core::{ transform::{local_to_world, translation}, }; use kiwi_ecs::{components, query, EntityData, EntityId, FnSystem, SystemGroup}; -use kiwi_model::{Model, ModelDef, ModelSpawnOpts, ModelSpawnRoot}; +use kiwi_model::{Model, ModelFromUrl, ModelSpawnOpts, ModelSpawnRoot}; use kiwi_renderer::color; use kiwi_std::{ asset_cache::{AssetCache, AsyncAssetKeyExt, SyncAssetKey, SyncAssetKeyExt}, @@ -162,7 +162,7 @@ async fn update_natural_layer( .0 .iter() .filter_map(|url| { - let model = Box::new(ModelDef(url.join("../models/main.json").ok()?.into())) as BoxModelKey; + let model = Box::new(ModelFromUrl(url.join("../models/main.json").ok()?.into())) as BoxModelKey; Some((element.clone(), model)) }) .collect_vec() diff --git a/crates/object/src/lib.rs b/crates/object/src/lib.rs index a8aef57146..f9c5c1b021 100644 --- a/crates/object/src/lib.rs +++ b/crates/object/src/lib.rs @@ -7,7 +7,7 @@ use kiwi_decals::decal; use kiwi_ecs::{ components, query, query_mut, Debuggable, Description, DeserWorldWithWarnings, EntityId, Name, Networked, Store, SystemGroup, World, }; -use kiwi_model::{model_def, ModelDef}; +use kiwi_model::{model_from_url, ModelFromUrl}; use kiwi_physics::collider::collider; use kiwi_std::{ asset_cache::{AssetCache, AsyncAssetKey, AsyncAssetKeyExt, SyncAssetKeyExt}, @@ -66,8 +66,8 @@ impl AsyncAssetKey, AssetError>> for ObjectFromUrl { let DeserWorldWithWarnings { mut world, warnings } = tokio::task::block_in_place(|| serde_json::from_slice(&data)) .with_context(|| format!("Failed to deserialize object2 from url {}", obj_url))?; warnings.log_warnings(); - for (_id, (url,), _) in query_mut((model_def(),), ()).iter(&mut world, None) { - *url = ModelDef(url.0.resolve(&obj_url).context("Failed to resolve model url")?.into()); + for (_id, (url,), _) in query_mut((model_from_url(),), ()).iter(&mut world, None) { + *url = AssetUrl::parse(&url).context("Invalid model url")?.resolve(&obj_url).context("Failed to resolve model url")?.into(); } for (_id, (def,), _) in query_mut((collider(),), ()).iter(&mut world, None) { def.resolve(&obj_url).context("Failed to resolve collider")?; diff --git a/crates/physics/src/collider.rs b/crates/physics/src/collider.rs index e6ead4263f..7b8b1ba86a 100644 --- a/crates/physics/src/collider.rs +++ b/crates/physics/src/collider.rs @@ -6,24 +6,36 @@ use futures::future::try_join_all; use glam::{vec3, Mat4, Quat, Vec3}; use itertools::Itertools; use kiwi_core::{ - asset_cache, async_ecs::async_run, runtime, transform::{rotation, scale, translation} + asset_cache, + async_ecs::async_run, + runtime, + transform::{rotation, scale, translation}, }; use kiwi_ecs::{ - components, query, Component, ComponentQuery, ComponentValueBase, Debuggable, Description, EntityData, EntityId, MakeDefault, Name, Networked, QueryEvent, QueryState, Store, SystemGroup, TypedReadQuery, World + components, query, Component, ComponentQuery, ComponentValueBase, Debuggable, Description, EntityData, EntityId, MakeDefault, Name, + Networked, QueryEvent, QueryState, Store, SystemGroup, TypedReadQuery, World, }; use kiwi_editor_derive::ElementEditor; -use kiwi_model::model_def; +use kiwi_model::model_from_url; use kiwi_std::{ - asset_cache::{AssetCache, AsyncAssetKey, AsyncAssetKeyExt, SyncAssetKeyExt}, asset_url::{AbsAssetUrl, ColliderAssetType, TypedAssetUrl}, download_asset::{AssetError, JsonFromUrl}, events::EventDispatcher + asset_cache::{AssetCache, AsyncAssetKey, AsyncAssetKeyExt, SyncAssetKeyExt}, + asset_url::{AbsAssetUrl, ColliderAssetType, TypedAssetUrl}, + download_asset::{AssetError, JsonFromUrl}, + events::EventDispatcher, }; use kiwi_ui::Editable; use physxx::{ - AsPxActor, AsPxRigidActor, PxActor, PxActorFlag, PxBase, PxBoxGeometry, PxControllerDesc, PxControllerShapeDesc, PxConvexMeshGeometry, PxGeometry, PxMaterial, PxMeshScale, PxPlaneGeometry, PxRigidActor, PxRigidBody, PxRigidBodyFlag, PxRigidDynamicRef, PxRigidStaticRef, PxShape, PxShapeFlag, PxSphereGeometry, PxTransform, PxTriangleMeshGeometry, PxUserData + AsPxActor, AsPxRigidActor, PxActor, PxActorFlag, PxBase, PxBoxGeometry, PxControllerDesc, PxControllerShapeDesc, PxConvexMeshGeometry, + PxGeometry, PxMaterial, PxMeshScale, PxPlaneGeometry, PxRigidActor, PxRigidBody, PxRigidBodyFlag, PxRigidDynamicRef, PxRigidStaticRef, + PxShape, PxShapeFlag, PxSphereGeometry, PxTransform, PxTriangleMeshGeometry, PxUserData, }; use serde::{Deserialize, Serialize}; use crate::{ - main_controller_manager, make_physics_static, mesh::{PhysxGeometry, PhysxGeometryFromUrl}, physx::{character_controller, physics, physics_controlled, physics_shape, rigid_actor, Physics}, wood_physics_material, ColliderScene, PxActorUserData, PxShapeUserData, PxWoodMaterialKey + main_controller_manager, make_physics_static, + mesh::{PhysxGeometry, PhysxGeometryFromUrl}, + physx::{character_controller, physics, physics_controlled, physics_shape, rigid_actor, Physics}, + wood_physics_material, ColliderScene, PxActorUserData, PxShapeUserData, PxWoodMaterialKey, }; fn one() -> f32 { @@ -201,7 +213,7 @@ pub fn server_systems() -> SystemGroup { } }, ), - query((collider().changed(),)).optional_changed(model_def()).optional_changed(density()).to_system(|q, world, qs, _| { + query((collider().changed(),)).optional_changed(model_from_url()).optional_changed(density()).to_system(|q, world, qs, _| { let all = changed_or_missing(q, world, qs, collider_shapes()); let mut by_collider = HashMap::new(); diff --git a/crates/std/src/uncategorized/asset_url.rs b/crates/std/src/uncategorized/asset_url.rs index 46f5787bd5..1c4166f72f 100644 --- a/crates/std/src/uncategorized/asset_url.rs +++ b/crates/std/src/uncategorized/asset_url.rs @@ -1,5 +1,7 @@ use std::{ - marker::PhantomData, path::{Path, PathBuf}, sync::Arc + marker::PhantomData, + path::{Path, PathBuf}, + sync::Arc, }; use anyhow::Context; @@ -8,12 +10,15 @@ use percent_encoding::percent_decode_str; use rand::seq::SliceRandom; use relative_path::{RelativePath, RelativePathBuf}; use serde::{ - de::{DeserializeOwned, Visitor}, Deserialize, Deserializer, Serialize, Serializer + de::{DeserializeOwned, Visitor}, + Deserialize, Deserializer, Serialize, Serializer, }; use url::Url; use crate::{ - asset_cache::{AssetCache, SyncAssetKey, SyncAssetKeyExt}, download_asset::{download, AssetsCacheDir}, Cb + asset_cache::{AssetCache, SyncAssetKey, SyncAssetKeyExt}, + download_asset::{download, AssetsCacheDir}, + Cb, }; #[derive(Debug, Clone)] @@ -178,6 +183,12 @@ impl From for AbsAssetUrl { } } +impl From for String { + fn from(value: AbsAssetUrl) -> Self { + value.to_string() + } +} + #[test] fn test_abs_asset_url() { assert_eq!(AbsAssetUrl::parse("http://t.c/hello").unwrap().as_directory().to_string(), "http://t.c/hello/");